effect-tsで入力チェック - Schema

入力チェックのやり方のメモ

# effect-ts : Schema

https://effect.website/docs/schema/introduction/


# 単純な入力チェック

### 単体チェックNG

```ts
import { describe, it } from 'vitest'
import { Either, Schema, ParseResult } from 'effect'

describe('effect-ts 入力検証 - person', () => {
  const NameSchema = Schema.String.annotations({
    arbitrary: () => (fc) => fc.constantFrom('山田 太郎', 'Alice Johnson')
  })
  const AgeSchema = Schema.Number.pipe(Schema.between(1, 80), Schema.int())

  const Person = Schema.Struct({
    name: NameSchema,
    age: AgeSchema
  })

  const decode_Person = Schema.decodeEither(Person)

  it('シンプルNG - decode', () => {
    const testData: any = {
      name: null,
      age: 100
    }

    const result = decode_Person(testData, { errors: 'all' })

    if (Either.isLeft(result)) {
      console.error('入力エラー')
      console.error(ParseResult.TreeFormatter.formatErrorSync(result.left))
    } else {
      console.log('問題なし')
    }
  })
})
```

実行結果
```ts
入力エラー
{ readonly name: string; readonly age: between(1, 80) & int }
├─ ["name"]
│  └─ Expected string, actual null
└─ ["age"]
   └─ between(1, 80) & int
      └─ From side refinement failure
         └─ between(1, 80)
            └─ Predicate refinement failure
               └─ Expected a number between 1 and 80, actual 100
```

### 単体チェックOK


```ts
import { describe, it } from 'vitest'
import { Either, Schema, ParseResult } from 'effect'

describe('effect-ts 入力検証 - person', () => {
  const NameSchema = Schema.String.annotations({
    arbitrary: () => (fc) => fc.constantFrom('山田 太郎', 'Alice Johnson')
  })
  const AgeSchema = Schema.Number.pipe(Schema.between(1, 80), Schema.int())

  const Person = Schema.Struct({
    name: NameSchema,
    age: AgeSchema
  })

  const decode_Person = Schema.decodeEither(Person)

  it('シンプルOK - decode', () => {
    const testData: any = {
      name: 'test name',
      age: 50
    }

    const result = decode_Person(testData, { errors: 'all' })

    if (Either.isLeft(result)) {
      console.error('入力エラー')
      console.error(ParseResult.TreeFormatter.formatErrorSync(result.left))
    } else {
      console.log('問題なし')
    }
  })
})

```

実行結果
```ts
問題なし
```


### テストデータ作成（入力データチェック）

```ts
import { describe, it } from 'vitest'
import { Schema, Arbitrary, FastCheck } from 'effect'

describe('effect-ts 入力検証 - person', () => {
  const NameSchema = Schema.String.annotations({
    arbitrary: () => (fc) => fc.constantFrom('山田 太郎', 'Alice Johnson')
  })
  const AgeSchema = Schema.Number.pipe(Schema.between(1, 80), Schema.int())

  const Person = Schema.Struct({
    name: NameSchema,
    age: AgeSchema
  })

  const testDataGenerator_Person = Arbitrary.make(Person)

  it('テストデータ作成 - Arbitrary', () => {
    const result = FastCheck.sample(testDataGenerator_Person, 10)
    console.log(result)
  })
})
```

実行結果
```ts
[
  { name: '山田 太郎', age: 74 },
  { name: 'Alice Johnson', age: 3 },
  { name: '山田 太郎', age: 3 },
  { name: 'Alice Johnson', age: 76 },
  { name: 'Alice Johnson', age: 6 },
  { name: 'Alice Johnson', age: 3 },
  { name: 'Alice Johnson', age: 4 },
  { name: '山田 太郎', age: 2 },
  { name: 'Alice Johnson', age: 76 },
  { name: '山田 太郎', age: 54 }
]
```


# 複雑な入力チェック

### チェックNG

```ts
import { describe, it } from 'vitest'
import { Either, Schema, ParseResult } from 'effect'

describe('effect-ts 入力検証 - myform', () => {
  const Password = Schema.Trim.pipe(Schema.minLength(2))

  const MyForm = Schema.Struct({
    password: Password,
    confirm_password: Password
  }).pipe(
    Schema.filter((input) => {
      if (input.password !== input.confirm_password) {
        return {
          path: ['confirm_password'],
          message: '同じパスワードを入力してください'
        }
      }
    })
  )

  const decode_MyForm = Schema.decodeEither(MyForm)

  it('相関チェック - decode', () => {
    const testData: any = {
      password: '12345',
      confirm_password: '1234'
    }

    const result = decode_MyForm(testData, { errors: 'all' })

    if (Either.isLeft(result)) {
      console.error('入力エラー')
      console.error(ParseResult.TreeFormatter.formatErrorSync(result.left))
    } else {
      console.log('問題なし')
    }
  })
})
```

実行結果
```ts
入力エラー
{ { readonly password: minLength(2); readonly confirm_password: minLength(2) } | filter }
└─ Predicate refinement failure
   └─ ["confirm_password"]
      └─ 同じパスワードを入力してください
```


### テストデータ作成（入力データチェック）

```ts
import { describe, it } from 'vitest'
import { Schema, Arbitrary, FastCheck } from 'effect'

describe('effect-ts 入力検証 - myform', () => {
  const Password = Schema.Trim.pipe(Schema.minLength(2))

  const MyForm = Schema.Struct({
    password: Password,
    confirm_password: Password
  }).pipe(
    Schema.filter((input) => {
      if (input.password !== input.confirm_password) {
        return {
          path: ['confirm_password'],
          message: '同じパスワードを入力してください'
        }
      }
    })
  )

  const testDataGenerator_MyForm = Arbitrary.make(MyForm)

  it('テストデータ作成 - Arbitrary', () => {
    const result = FastCheck.sample(testDataGenerator_MyForm, 10)
    console.log(result)
  })
})
```

実行結果
```ts
[
  { password: '~{', confirm_password: '~{' },
  { password: '$#', confirm_password: '$#' },
  { password: 'z&', confirm_password: 'z&' },
  { password: '&&', confirm_password: '&&' },
  { password: '%#', confirm_password: '%#' },
  { password: '&&', confirm_password: '&&' },
  { password: 'z$', confirm_password: 'z$' },
  { password: '%#', confirm_password: '%#' },
  { password: '#!', confirm_password: '#!' },
  { password: 'e:', confirm_password: 'e:' }
]
```

# 二つの組み合わせ

:::note warn
MyForm の pipe以後のチェック処理が短絡してなくなっている気がする。
ここらへんを全部出す方法は調査中
:::

### チェックNG

```ts
import { describe, it } from 'vitest'
import { Either, Schema, ParseResult } from 'effect'

describe('effect-ts 入力検証 - myform と person', () => {
  const Password = Schema.Trim.pipe(Schema.minLength(2))

  const NameSchema = Schema.String.annotations({
    arbitrary: () => (fc) => fc.constantFrom('山田 太郎', 'Alice Johnson')
  })
  const AgeSchema = Schema.Number.pipe(Schema.between(1, 80), Schema.int())

  const Person = Schema.Struct({
    name: NameSchema,
    age: AgeSchema
  })

  const MyForm = Schema.Struct({
    password: Password,
    confirm_password: Password
  }).pipe(
    Schema.filter((input) => {
      if (input.password !== input.confirm_password) {
        return {
          path: ['confirm_password'],
          message: '同じパスワードを入力してください'
        }
      }
    })
  )

  const PersonForm = Schema.extend(Person, MyForm)

  const decode_PersonForm = Schema.decodeEither(PersonForm)

  it('チェックの合体 - decode', () => {
    const testData: any = {
      name: null,
      age: 100,
      password: '12345',
      confirm_password: '1234'
    }

    const result = decode_PersonForm(testData, { errors: 'all' })

    if (Either.isLeft(result)) {
      console.error('入力エラー')
      console.error(ParseResult.TreeFormatter.formatErrorSync(result.left))
    } else {
      console.log('問題なし')
    }
  })
})
```

実行結果
```ts
入力エラー
{ { readonly name: string; readonly age: between(1, 80) & int; readonly password: minLength(2); readonly confirm_password: minLength(2) } | filter }
└─ From side refinement failure
   └─ { readonly name: string; readonly age: between(1, 80) & int; readonly password: minLength(2); readonly confirm_password: minLength(2) }
      ├─ ["name"]
      │  └─ Expected string, actual null
      └─ ["age"]
         └─ between(1, 80) & int
            └─ From side refinement failure
               └─ between(1, 80)
                  └─ Predicate refinement failure
                     └─ Expected a number between 1 and 80, actual 100
```

### テストデータ作成（入力データチェック）

```ts
import { describe, it } from 'vitest'
import { Schema, Arbitrary, FastCheck } from 'effect'

describe('effect-ts 入力検証 - myform と person', () => {
  const Password = Schema.Trim.pipe(Schema.minLength(2))

  const NameSchema = Schema.String.annotations({
    arbitrary: () => (fc) => fc.constantFrom('山田 太郎', 'Alice Johnson')
  })
  const AgeSchema = Schema.Number.pipe(Schema.between(1, 80), Schema.int())

  const Person = Schema.Struct({
    name: NameSchema,
    age: AgeSchema
  })

  const MyForm = Schema.Struct({
    password: Password,
    confirm_password: Password
  }).pipe(
    Schema.filter((input) => {
      if (input.password !== input.confirm_password) {
        return {
          path: ['confirm_password'],
          message: '同じパスワードを入力してください'
        }
      }
    })
  )

  const PersonForm = Schema.extend(Person, MyForm)

  const testDataGenerator_PersonForm = Arbitrary.make(PersonForm)

  it('テストデータ作成 - Arbitrary', () => {
    const result = FastCheck.sample(testDataGenerator_PersonForm, 10)
    console.log(result)
  })
})
```

実行結果
```ts
[
  {
    name: 'Alice Johnson',
    age: 4,
    password: '$"',
    confirm_password: '$"'
  },
  {
    name: 'Alice Johnson',
    age: 23,
    password: '8\f{',
    confirm_password: '8\f{'
  },
  {
    name: 'Alice Johnson',
    age: 76,
    password: 'y{',
    confirm_password: 'y{'
  },
  {
    name: '山田 太郎',
    age: 3,
    password: '|reft',
    confirm_password: '|reft'
  },
  {
    name: 'Alice Johnson',
    age: 69,
    password: 'y&',
    confirm_password: 'y&'
  },
  { name: '山田 太郎', age: 3, password: '|#', confirm_password: '|#' },
  { name: '山田 太郎', age: 51, password: '!%', confirm_password: '!%' },
  {
    name: '山田 太郎',
    age: 2,
    password: 'xkey"',
    confirm_password: 'xkey"'
  },
  { name: '山田 太郎', age: 6, password: '~#', confirm_password: '~#' },
  { name: '山田 太郎', age: 56, password: '&"', confirm_password: '&"' }
]
```