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

入力チェックのやり方のメモ
Schema.filterにおいて取り扱い注意事項が存在する

# effect-ts : Schema

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

## 前回

https://qiita.com/kuangzhixitian3/items/56b122185631a994223f

# エラーが隠されるパターン

チェックNG１はチェックNG２のエラーを全て含むようにしたかったが、
単純なやり方ではできそうになかった。
一旦、できないやり方を残す

### チェック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
```


### チェック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: 'test name',
      age: 50,
      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 }
└─ Predicate refinement failure
   └─ ["confirm_password"]
      └─ 同じパスワードを入力してください
```


# エラーをできるだけ全部検知する方法

:::note alert
Schema.extendを使用するとうまくいかない場合がある
なぜかはよくわからない
:::

decodeを別々に実行して無理やりエラーを検知している
もっとよいやり方がありそうだが、思いつかなかった。


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

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

  const Password_Schema = Schema.Trim.pipe(Schema.minLength(2))

  const email_pattern =
    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
  const Email_Schema = Schema.String.pipe(Schema.pattern(email_pattern))

  const Person_Struct = Schema.Struct({
    name: Name_Schema,
    age: Age_Schema,
    password: Password_Schema,
    confirm_password: Password_Schema,
    email: Email_Schema,
    confirm_email: Email_Schema
  })

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

  const ConfirmEmail_Filter = Person_Struct.pipe(
    Schema.pick('email', 'confirm_email'),
    Schema.filter((input) => {
      if (input.email !== input.confirm_email) {
        return {
          path: ['confirm_email'],
          message: '同じメールアドレスを入力してください'
        }
      }
    })
  )

  // なぜかエラーになる
  // const PersonPassword_Schema = Schema.extend(ConfirmPassword_Filter, Person_Struct)

  const decode_PersonSchema = Schema.decodeEither(Person_Struct)
  const decode_PasswordFilter = Schema.decodeEither(ConfirmPassword_Filter)
  const decode_EmailFilter = Schema.decodeEither(ConfirmEmail_Filter)

  const getErrorArray = (eitherArray: Either.Either<any, ParseResult.ParseError>[]) =>
    pipe(eitherArray, Array.map(getError), Array.flatten)

  const getError = (either: Either.Either<any, ParseResult.ParseError>) =>
    pipe(
      either,
      Either.match({
        onLeft: (left) => ParseResult.ArrayFormatter.formatErrorSync(left),
        onRight: () => []
      })
    )

  const getErrorMap = (errors: ParseResult.ArrayFormatterIssue[]) =>
    pipe(
      errors,
      Array.dedupeWith((a, b) => a.path[0] === b.path[0] && a.message === b.message),
      Array.groupBy((item) => item.path[0].toString())
    )

  it('エラーを極力検知するやり方 - decode', () => {
    const testData: any = {
      name: null,
      age: 100,
      password: '123',
      confirm_password: '1234',
      email: 'a@a.com',
      confirm_email: 'b@a.com'
    }

    const schemaResult = decode_PersonSchema(testData, { errors: 'all' })

    if (Either.isLeft(schemaResult)) {
      console.error('入力エラー')
      const errors = []
      const schemaError = getError(schemaResult)
      errors.push(...schemaError)

      const filterResult = [decode_PasswordFilter, decode_EmailFilter].map((decode) =>
        decode(testData, { errors: 'all' })
      )
      const filterError = getErrorArray(filterResult)
      errors.push(...filterError)

      const errorMap = getErrorMap(errors)
      console.error(errorMap)
    } else {
      console.log('問題なし')
      console.log(schemaResult.right)
    }
  })
})
```

実行結果
```ts
入力エラー
{
  name: [
    {
      _tag: 'Type',
      path: [Array],
      message: 'Expected string, actual null'
    }
  ],
  age: [
    {
      _tag: 'Type',
      path: [Array],
      message: 'Expected a number between 1 and 80, actual 100'
    }
  ],
  confirm_password: [ { _tag: 'Type', path: [Array], message: '同じパスワードを入力してください' } ],
  confirm_email: [ { _tag: 'Type', path: [Array], message: '同じメールアドレスを入力してください' } ]
}
```