fp-ts 로 데이터 검증하기 3 - 데이터의 오류 확인하기
on Typescript, Fp-ts
지난 글에서
지난 글에서는 데이터에 Option
을 이용해 오류 유무만을 확인했다.
이번 글에서는 정확히 어떤 오류가 있는지 확인하는 방법을 알아보자.
Either
Either
는 Option
과 비슷하지만, 오류가 발생했을 때 오류의 내용을 담을 수 있다.
Option
이 Either
의 일종이라고 생각하면 된다.
Either
는 두 가지의 타입을 받는데, 보통 성공적으로 처리된 경우를 Right
, 오류가 발생한 경우를 Left
라고 한다. Option
은 이 중 Left
가 None
으로 고정된 Either
라고 생각하면 된다.
Either
적용
공식 문서에 나와있는 예시를 보고 Either
를 어떻게 사용할 수 있는지 알아보자.
예를 들어 number[]
에서 첫 번째 요소를 가져와 역수를 취하는 함수를 만들어보자.
이 때 두 가지를 검사해야하는데, 배열이 비어있지 않은지, 첫 번째 요소가 0이 아닌지이다.
fp-ts
를 사용하지 않는다면 다음과 같이 구현할 수 있을 것이다.
const inverse = (n: number): number => {
if (n === 0) {
throw new Error('cannot divide by zero')
}
return 1 / n
}
const head = (as: Array<number>): number => {
if (as.length === 0) {
throw new Error('empty array')
}
return as[0]
}
export const imperative = (as: ReadonlyArray<number>): string => {
try {
return `Result is ${inverse(head(as))}`
} catch (err: any) {
return `Error is ${err.message}`
}
}
익숙하긴 하지만 매번 오류를 throw
하고 try-catch
로 잡아내는 것은 번거롭다.
대신 올바른 값을 Right
라는 타입을 만들어서 담아보자.
interface Right<A> {
readonly _tag: 'Right'
readonly right: A
}
const head = (e: Right<number[]> | any): number => {
if (e._tag === "Right") {
if (e.right.length === 0) return {}
return {
_tag: "Right"
right: e.right[0]
}
}
return {}
}
const inverse = (e: Right<number> | any): Right<number> => {
if (e._tag === "Right") {
if (e.right === 0) return {}
return {
_tag: "Right"
right: 1 / e.right
}
}
return {}
}
export const useRight = (as: ReadonlyArray<number>): string => {
const result = inverse(head(as))
return result._tag === "Right" ? `Result is ${result.right}` : `Error`
}
하지만 이렇게만 하면 어디서 에러가 발생했는지 알 수 없다.
이를 위해 에러를 담기 위한 Left
타입을 만들어보자.
interface Left<E> {
readonly _tag: 'Left'
readonly left: E
}
const head = (e: Left<string> | Right<number[]>): Left<string> | Right<number> => {
if (e._tag === "Right") {
if (e.right.length === 0) return { _tag: "Left", left: "empty array" }
return { _tag: "Right" right: e.right[0] }
}
return e as Left<string>
}
const inverse = (e: Left<string> | Right<number>): Left<string> | Right<number> => {
if (e._tag === "Right") {
if (e.right === 0) return { _tag: "Left", left: "cannot divide by zero" }
return { _tag: "Right", right: 1 / e.right }
}
return e as Left<string>
}
export const useLeftRight = (as: ReadonlyArray<number>): string => {
const result = inverse(head(as))
return result._tag === "Right" ? `Result is ${result.right}` : `Error is ${result.left}`
}
이제 공통적인 부분을 분리해보자.
먼저 Left
와 Right
를 만드는 함수를 만들어보자.
const left = <E>(e: E): Left<E> => ({ _tag: "Left", left: e })
const right = <A>(a: A): Right<A> => ({ _tag: "Right", right: a })
const head = (e: Left<string> | Right<number[]>): Left<string> | Right<number> => {
if (e._tag === "Right") {
return e.right.length === 0 ? left("empty array") : right(e.right[0])
}
return e as Left<string>
}
const inverse = (e: Left<string> | Right<number>): Left<string> | Right<number> => {
if (e._tag === "Right") {
return e.right === 0 ? left("cannot divide by zero") : right(1 / e.right)
}
return e as Left<string>
}
그리고 if (e._tag === "Right") { ... } return e
를 chain
으로 바꿔보자.
pipe
는 fp-ts
의 pipe
함수이다.
const chain = <E, A, B>(f: (a: A) => Left<E> | Right<B>) => (ma:Left<E> | Right<A>): Left<E> | Right<B> =>
ma._tag === "Right" ? f(ma.right) : ma
const head = (e: Left<string> | Right<number[]>): Left<string> | Right<number> => {
return (as: number[]) => as.length === 0 ? left("empty array") : right(as[0])
}
const inverse = (e: Left<string> | Right<number>): Left<string> | Right<number> => {
return (n: number) => n === 0 ? left("cannot divide by zero") : right(1 / n)
}
const useChain = (as: number[]) => {
const result = pipe(
as,
right,
chain(head),
chain(inverse)
)
return result._tag === "Right" ? `Result is ${result.right}` : `Error is ${result.left}`
}
그리고 받은 인자를 right
로 만들어주는 of
와 Left,
Right
인 경우 동작이 달라지는 match
를 만들어보자.
const of = <E, A>(a: A): Left<E> | Right<A> => right(a);
const match = <E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B) => (ma: Left<E> | Right<A>): B =>
ma._tag === "Right" ? onRight(ma.right) : onLeft(ma.left)
const useOfMatch = (as: number[]) => pipe(
as,
of,
chain(head),
chain(inverse),
match(
(err) => `Error is ${err}`,
(head) => `Result is ${head}`
)
)
그리고 이 Left
와 Right
를 합친 타입 Either
를 만들어보자.
type Either<E, A> = Left<E> | Right<A>
const chain = <E, A, B>(f: (a: A) => Either<E, B>) => (ma: Either<E, A>) => Either<E, B>
const head = (e: Either<string, number[]>): Either<string, number> => {
return (as: number[]) => as.length === 0 ? left("empty array") : right(as[0])
}
const inverse = (e: Either<string, number>): Either<string, number> => {
return (n: number) => n === 0 ? left("cannot divide by zero") : right(1 / n)
}
const of = <E, A>(a: A): Either<E, A> => right(a);
const match = <E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B) => (ma: Either<E, A>): B =>
ma._tag === "Right" ? onRight(ma.right) : onLeft(ma.left)
const useEither = (as: number[]) => pipe(
as,
of,
chain(head),
chain(inverse),
match(
(err) => `Error is ${err}`,
(head) => `Result is ${head}`
)
)
fp-ts
의 Either
를 사용하면 다음과 같이 구현할 수 있다.
import * as E from "fp-ts/Either"
const head = (as: number[]): Either<string, number> =>
as.length === 0 ? E.left("empty array") : E.right(as[0]);
const inverse = (n: number): Either<string, number> =>
n === 0 ? E.left("cannot divide by zero") : E.right(1 / n);
const functional = (as: number[]) =>
pipe(
as,
E.of<string, number[]>,
E.chain(head),
E.chain(inverse),
E.match(
(err) => `Error is ${err}`,
(head) => `Result is ${head}`
)
);
훨씬 더 간결해졌다.
여러 개의 에러 처리하기
위와 같은 경우에는 하나의 에러만 처리해도 되지만 동시에 여러가지 에러가 발생할 수도 있다.
지난 글에서 했던 UserInput
검사를 Either
로 바꿔보자.
먼저 간단하게 최소 글자수 검사만 해보자.
const isPasswordValid = (body: Body) =>
minLength(8)(body.password) ? E.right(body) : E.left("password is too short");
const isUsernameValid = (body: Body) =>
minLength(6)(body.username) ? E.right(body) : E.left("username is too short");
const isUserInputValid = (body: Body) =>
pipe(
body,
E.of,
E.chain(isPasswordValid),
E.chain(isUsernameValid),
);
const stringify = E.match<string[], UserInput, string>(
(err) => `Error: ${err.join(", ")}`,
({ username, password }) =>
`Result: username is ${username}, password is ${password}`
);
const exampleUserInput = [
{ username: "Giulio", password: "password" },
{ username: "Giulio", password: "pass" },
{ username: "Giu", password: "password" },
{ username: "Giu", password: "pass" },
];
console.log(exampleUserInput.map(isUserInputValid).map(stringify).join("\n"));
/* Output:
Result is username: Giulio, password: password
Error is password is too short
Error is username is too short
Error is password is too short
*/
우리가 최종적으로 만들고 싶은 것은 4번째 예제에서 username
과 password
모두 에러가 발생했을 때, 두 개의 에러를 모두 출력하는 것이다.
그러기 위해서는 각각의 에러문을 배열로 합치는 것이 이상적일 것이다.
즉 다음과 반환값이 Either<string[], T>
를 합치는 함수를 만들어야한다.
Either1 | Either2 | 결과 |
---|---|---|
Right<A> | Right<B> | Right<A & B> |
Right<A> | Left<B> | Left<B> |
Left<A> | Right<B> | Left<A> |
Left<A> | Left<B> | Left<A + B> |
이를 직접 구현한다면 다음과 같은 모습일 것이다.
const concatEithers = (
e1: E.Either<string[], { username: string }>,
e2: E.Either<string[], { password: string }>
) =>
E.isLeft(e1)
? E.isLeft(e2)
? E.left([...e1.left, ...e2.left])
: e1
: E.isLeft(e2)
? e2
: E.right({ ...e1.right, ...e2.right });
getApplicativeValidation
다행히 fp-ts
에서는 이를 위한 함수가 존재한다.
Either
의 getApplicativeValidation
라는 함수이다.
다만 사용법을 조금 숙지해야한다.
먼저 getApplicativeValidation
는 Semigroup
을 받아 Applicative
를 반환하는 함수이다.
입력하는 Semigroup
은 Either
의 left
에 담긴 값들을 합치는 Semigroup
이다.
반환는 Applicative
는 ap
를 통해 사용하는데 지난 글에서 설명했듯, ap
는 컨테이너에 담긴 함수와 값을 받아 함수에 값을 넣어 반환하는 함수이다.
따라서 다음과 같은 방식으로 사용해야한다.
const { ap } = E.getApplicativeValidation(A.getMonoid<string>());
ap(
E.right((a: number) => a + 1),
E.right(2)
); // Right(3)
ap(
E.right((a: number) => a + 1),
E.left(["error"])
); // Left(["error"])
ap(
E.left(["error1"]),
E.left(["error2"])
); // Left(["error1", "error2"])
이를 이용하면 username
을 합치는 부분을 이렇게 바꿀 수 있다.
const errorsApplicative = E.getApplicativeValidation(A.getMonoid<string>());
const validUsername = (username: string) =>
minLength(6)(username) ? E.right(username) : E.left(["username too short"]);
const validUserInput = (input: Body) => pipe(
...
(ea) =>
errorsApplicative.ap(
E.isLeft(ea)
? ea
: E.right((b: string) => ({ ...ea.right, username: b })),
validUsername(input.username)
),
...
)
Applicative
는 Functor
이므로 map
을 사용할 수 있다.
이를 통해 더 간결하게 쓸 수 있다.
const validUserInput = (input: Body) => pipe(
...
(ea) =>
errorsApplicative.ap(
errorsApplicative.map(ea, (a) => (b: string) => ({ ...a, username: b })),
validUsername(input.username)
),
...
)
하지만 이를 더 쉽게 사용할 수 있는 방법이 있다.
apS
Apply
에는 apS
라는 함수가 존재한다.
apS
는 컨테이너에 담긴 레코드 값에 새로운 성분을 추가한 레코드를 반환하는 함수이다.
세 번이나 호출을 해야하는 고차함수이나, 그만큼 편리하게 사용할 수 있다.
첫 번째 호출 시에는 컨테이너의 Apply
를 넣는다.
두 번째 호출 시에는 성분명과 해당 성분에 넣을 값이 담긴 컨테이너를 넣는다.
세 번째 호출 시에는 컨테이너에 담긴 레코드를 넣는다.
apS
의 최종 반환값의 타입은 해당 성분의 존재와 타입을 보장한다.
Applicative
는 당연히 Apply
이므로 apS
를 사용할 수 있다.
import { apS } from "fp-ts/Apply";
const errorsApplicative = E.getApplicativeValidation(A.getMonoid<string>());
const errorsApS = apS(errorsApplicative);
const validUserInput = ({ username, password }: UserInput) =>
pipe(
E.right({}),
errorsApS("username", validUsername(username)),
errorsApS("password", validPassword(password)),
);
Do notation
추가적으로 E.right({})
는 자주 쓰이기 때문에 E.Do
라는 이름으로 사용할 수 있다.
const validUserInput = ({ username, password }: UserInput) =>
pipe(
E.Do,
errorsApS("username", validUsername(username)),
errorsApS("password", validPassword(password)),
E.match(
(err) => `Error is ${err.join(", ")}`,
({ username, password }) =>
`Result is username: ${username}, password: ${password}`
)
);
const examples = [
{ username: "Giulio", password: "password" },
{ username: "Giulio", password: "pass" },
{ username: "Giu", password: "password" },
{ username: "Giu", password: "pass" },
];
console.log(examples.map(validUserInput).join("\n"));
/* Output:
Result is username: Giulio, password: password
Error is password too short
Error is username too short
Error is username too short, password too short
*/
특히 방금 쓰였던 apS
과 bind
, exists
등의 함수는 E.Do
와 자주 쓰인다.
이와 같은 방법론을 “Do notation” 이라고 한다.
validate
이제 지난 글에서 만들었던 validate
함수를 Either
로 바꿔보자.
지난 글에서는 Option
을 사용했기 때문에 검사만 하면 됐었다.
이번 글에선 Either
로 에러 메시지를 담아야하기 때문에 검사함수 pred
와 오류 시 반환할 메시지 error
를 같이 받아야한다.
검사할 함수들을 모두 문제 없이 통과한다면 기존 값을 담고 있는 Right
를 반환해야한다.
하지만 검사 중 하나라도 문제가 있다면 문제가 있는 모든 에러문이 담긴 배열이 담긴 Left
를 반환해야한다.
간단하게 명령형으로 먼저 구현해보자.
const validate_ =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const errors: string[] = [];
for (const [pred, error] of predAndError) {
if (!pred(v)) {
errors.push(error);
}
}
if (errors.length > 0) {
return E.left(errors);
}
return E.of(v);
};
눈에 보이는 것부터 천천히 바꿔보자.
먼저 for (...) { if (...) {...} }
는 filter
로 바꿀 수 있다.
그리고 그 중에서 두번째 인자인 error
를 가져오기 위해 map
을 사용하자.
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const errors = predAndError.filter(([pred]) => !pred(v)).map(([, error]) => error);
return errors.length > 0 ? E.left(errors) : E.of(v);
};
filter
과 map
에서 사용된 특정 인자를 갖고 오는 함수는 Tuple
의 fst
, snd
라는 함수로 대체할 수 있다.
import * as T from "fp-ts/Tuple";
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const errors = predAndError.filter((p) => !T.fst(p)(v)).map(T.snd);
return errors.length > 0 ? E.left(errors) : E.of(v);
};
부정문 !
과 인자 적용은 각각 fp-ts/function
의 not
과 apply
으로 대체할 수 있다.
import { apply, not } from "fp-ts/function";
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const errors = predAndError.filter((p) => apply(v)(not(T.fst(p)))).map(T.snd);
return errors.length > 0 ? E.left(errors) : E.of(v);
};
그리고 이를 flow
로 묶어 인자를 명시하지 않는 함수로 만들 수 있다.
import { flow } from "fp-ts/function";
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const errors = predAndError.filter(flow(T.fst, not, apply(v))).map(T.snd);
return errors.length > 0 ? E.left(errors) : E.of(v);
};
여기에 Array.prototype.filter
, Array.prototype.map
을 fp-ts/Array
의 filter
, map
으로 대체히자.
길이를 확인하는 errors.length > 0
은 fp-ts/Array
의 isNonEmpty
로 대체하자.
import * as A from "fp-ts/Array";
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const failed = A.filter<[Predicate<T>, string]>(flow(T.fst, not, apply(v)))(predAndError);
const errors = A.map(T.snd)(failed);
return A.isNonEmpty(errors) ? E.left(errors) : E.of(v);
};
이제 반환부를 바꿔보자.
먼저 fromPredicate
를 통해 Predicate
를 Either
로 바꾸자.
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const failed = A.filter<[Predicate<T>, string]>(flow(T.fst, not, apply(v)))(predAndError);
const errors = A.map(T.snd)(failed);
return E.fromPredicate(A.isNonEmpty, () => errors)(errors);
};
하지만 이렇게 되면 Right(v)
가 아닌 Right(errors)
가 반환된다.
생각해보면 지금 필요한 것은 조건 함수가 true
면 그 값을 Left
에 담고 아닐 경우 Right(v)
를 반환하는 함수가 필요하다.
공식 문서를 뒤져봤지만 이런 함수는 없었다.
그래서 대신 fold
를 이용했다.
Either
의 fold
는 onLeft
와 onRight
를 받아 각각의 경우 실행하는 함수이다.
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const failed = A.filter<[Predicate<T>, string]>(flow(T.fst, not, apply(v)))(predAndError);
const errors = A.map(T.snd)(failed);
const rightErrors = E.fromPredicate(A.isNonEmpty, () => null)(errors);
return E.fold(() => E.of(v), E.left<string[], T>)(rightErrors);
};
여기에 fp-ts/function
의 constant
와 constVoid
를 사용해 인자를 받지 않는 함수를 대체하자.
constant
는 첫 인자를 받아서 저장해 둔 뒤 호출할 때마다 그 값을 반환하는 함수이다.
이 때 호출 시에 들어온 인자는 모두 무시된다.
constVoid
는 constant(undefined)
와 같은 함수이다.
import { constant, constVoid } from "fp-ts/function";
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) => {
const failed = A.filter<[Predicate<T>, string]>(flow(T.fst, not, apply(v)))(predAndError);
const errors = A.map(T.snd)(failed);
const rightErrors = E.fromPredicate(A.isNonEmpty, constVoid)(errors);
return E.fold(constant(E.of(v)), E.left<string[], T>)(rightErrors);
};
마지막으로 pipe
를 통해 깔끔하게 만들어주자.
const validate =
<T>(predAndError: [Predicate<T>, string][]) =>
(v: T) =>
pipe(
predAndError,
A.filter(flow(T.fst, not, apply(v))),
A.map(T.snd),
E.fromPredicate(A.isNonEmpty, constVoid),
E.fold(constant(E.of(v)), E.left<string[], T>)
);
이를 이용해 지난 글에서 만들었던 validateUsername
를 다시 만들어보자.
const validateUsername = (username: unknown) =>
pipe(
username,
validate([
[minLength(6), "username is too short"],
[maxLength(20), "username is too long"],
[isAlphaNumeric, "username must be alphanumeric"],
])
);
물론 문제가 발생할 것이다.
username
은 unknown
이지만 이를 검사하는 함수들은 string
을 받는다.
이를 해결하기 위해서는 fp-ts/string
의 isString
을 사용해 unknown
을 string
임을 보장해야한다.
import * as S from "fp-ts/string";
const validateUsername = (username: unknown) =>
pipe(
username,
validate([
[S.isString, "username must be a string"],
[minLength(6), "username is too short"],
[maxLength(20), "username is too long"],
[isAlphaNumeric, "username must be alphanumeric"],
])
);
여전히 문제가 발생한다.
S.isString
을 통과하지 못하면 username
은 unknown
이므로 다른 검사를 진행해서는 안 된다.
하지만 validate
는 각각의 검사를 독립적으로 시행하기 때문에 S.isString
을 통과하지 못해도 다른 검사를 진행한다.
그렇기 떄문에 먼저 S.isString
검사는 따로 진행한 뒤, 통과 후 다른 검사를 진행해야한다.
그리고 그 결과값은 Either
이므로 flatMap
을 통해 다른 검사를 시행해야한다.
const validateUsername = (username: unknown) =>
pipe(
username,
validate([[S.isString, "username must be a string"]]),
E.flatMap(
validate([
[minLength(6), "username is too short"],
[maxLength(20), "username is too long"],
[isAlphaNumeric, "username must be alphanumeric"],
])
)
);
하지만 여전히 문제가 발생할 것이다.
이는 validate
의 predAndError
타입이 [Predicate<T>, string][]
인데 S.isString
는 Refinment<unknown, string>
이기 때문이다.
따라서 validate
를 Refinment
도 받을 수 있도록 바꿔주자.
const validate =
<U, T extends U>(predAndError: [Predicate<T> | Refinement<U, T>, string][]) =>
(v: U) =>
pipe(
predAndError,
A.filter(flow(T.fst, not, apply(v as T))),
A.map(T.snd),
E.fromPredicate(A.isNonEmpty, constVoid),
E.fold(constant(E.of(v as T)), E.left<string[], T>)
);
그럼 문제없이 동작할 것이다.
이제 마찬가지로 validatePassword
를 만들어보자.
const validatePassword = flow(
validate([[S.isString, "password must be a string"]]),
E.chain(
validate([
[minLength(8), "password is too short"],
[maxLength(20), "password is too long"],
[hasAlphaAndNumeric, "password has at least one letter and one number"],
])
)
);
이를 통해 validateUserInput
를 만들면 다음과 같을 것이다.
const validUserInput = ({ username, password }: Body) =>
pipe(
E.Do,
errorsApS("username", validateUsername(username)),
errorsApS("password", validatePassword(password)),
);
복합적인 에러를 가진 예제를 만들어 확인해보자.
const examples: Record<string, Body> = {
valid: {
username: "username",
password: "password123",
},
["Has not username and password"]: {},
["Too short username, not alphanumeric username"]: {
username: "user!",
password: "password123",
},
["Has not password, not alphanumeric username"]: {
username: "username!",
},
["Too long username, too long password"]: {
username: "usernameusernameusernameusername",
password: "passwordpasswordpasswordpassword123",
},
["Has not username, too short password, alphabet only password"]: {
password: "pas",
},
};
console.log(R.map(flow(validUserInput, stringify))(examples));
/* Output:
{
valid: 'Result: username is username, password is password123',
'Has not username and password': 'Error: username must be a string, password must be a string',
'Too short username, not alphanumeric username': 'Error: username is too short, username must be alphanumeric',
'Has not password, not alphanumeric username': 'Error: username must be alphanumeric, password must be a string',
'Too long username, too long password': 'Error: username is too long, password is too long',
'Has not username, too short password, alphabet only password': 'Error: username must be a string, password is too short, password has at least one letter and one number'
}
*/
문제 없이 작동하는 것을 확인할 수 있다.
최종 코드
import * as E from "fp-ts/Either";
import * as S from "fp-ts/string";
import * as T from "fp-ts/Tuple";
import * as A from "fp-ts/Array";
import * as R from "fp-ts/Record";
import * as Ap from "fp-ts/Apply";
import { pipe, apply, flow, constant, constVoid } from "fp-ts/function";
import { Predicate, not } from "fp-ts/Predicate";
import { Refinement } from "fp-ts/Refinement";
interface Body extends Record<string, string> {}
interface Username extends Body {
username: string;
}
interface Password extends Body {
password: string;
}
interface UserInput extends Username, Password {}
const minLength = (n: number) => (s: string) => s.length >= n;
const maxLength = (n: number) => (s: string) => s.length <= n;
const includes = (s: RegExp) => (str: string) => s.test(str);
const isAlphaNumeric = includes(/^[a-zA-Z0-9]+$/);
const hasAlphaAndNumeric = includes(/^(?=.*?\d)(?=.*?[a-zA-Z]).+$/);
const errorsApplicative = E.getApplicativeValidation(A.getMonoid<string>());
const errorsApS = Ap.apS(errorsApplicative);
const validate =
<U, T extends U>(predAndError: [Predicate<T> | Refinement<U, T>, string][]) =>
(v: U) =>
pipe(
predAndError,
A.filter(flow(T.fst, not, apply(v as T))),
A.map(T.snd),
E.fromPredicate(A.isNonEmpty, constVoid),
E.fold(constant(E.of(v as T)), E.left<string[], T>)
);
const validateUsername = (username: unknown) =>
pipe(
username,
validate([[S.isString, "username must be a string"]]),
E.flatMap(
validate([
[minLength(6), "username is too short"],
[maxLength(20), "username is too long"],
[isAlphaNumeric, "username must be alphanumeric"],
])
)
);
const validatePassword = flow(
validate([[S.isString, "password must be a string"]]),
E.chain(
validate([
[minLength(8), "password is too short"],
[maxLength(20), "password is too long"],
[hasAlphaAndNumeric, "password has at least one letter and one number"],
])
)
);
const validUserInput = ({ username, password }: Body) =>
pipe(
E.Do,
errorsApS("username", validateUsername(username)),
errorsApS("password", validatePassword(password))
);
const stringify = E.match<string[], UserInput, string>(
(err) => `Error: ${err.join(", ")}`,
({ username, password }) =>
`Result: username is ${username}, password is ${password}`
);
const examples: Record<string, Body> = {
valid: {
username: "username",
password: "password123",
},
["Has not username and password"]: {},
["Too short username, not alphanumeric username"]: {
username: "user!",
password: "password123",
},
["Has not password, not alphanumeric username"]: {
username: "username!",
},
["Too long username, too long password"]: {
username: "usernameusernameusernameusername",
password: "passwordpasswordpasswordpassword123",
},
["Has not username, too short password, alphabet only password"]: {
password: "pas",
},
};
console.log(R.map(flow(validUserInput, stringify))(examples));