入力チェックのやり方のメモ
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: '同じメールアドレスを入力してください' } ]
}