0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

effect-ts : Schema

前回

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

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

チェックNG1

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('問題なし')
    }
  })
})

実行結果

入力エラー
{ { 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

チェックNG2、隠されたエラーが出てくる

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('問題なし')
    }
  })
})

実行結果

入力エラー
{ { readonly name: string; readonly age: between(1, 80) & int; readonly password: minLength(2); readonly confirm_password: minLength(2) } | filter }
└─ Predicate refinement failure
   └─ ["confirm_password"]
      └─ 同じパスワードを入力してください

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

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

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

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)
    }
  })
})

実行結果

入力エラー
{
  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: '同じメールアドレスを入力してください' } ]
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?