0.1.15 (#16)
All checks were successful
Publish / publish (push) Successful in 17s
Lint / lint (push) Successful in 12s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
Julien Valverdé
2024-09-04 00:33:45 +02:00
parent 714450d0bb
commit 24d4ab6e46
11 changed files with 247 additions and 26 deletions

12
src/Schema/Email.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Schema } from "@effect/schema"
export const Email = Schema.pattern(
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
{
identifier: "Email",
title: "email",
message: () => "Not an email address",
},
)

View File

@@ -8,19 +8,19 @@ interface ObservableClassSelf {
readonly fields: { readonly [K in keyof Schema.Struct.Fields]: Schema.Struct.Fields[K] }
}
export const ObservableClass = <Self extends ObservableClassSelf>(
self: Self,
options?: Omit<CreateObservableOptions, "proxy">,
) =>
class Observable extends self {
declare ["constructor"]: typeof Observable
interface ObservableClassOptions extends Omit<CreateObservableOptions, "proxy"> {}
constructor(...args: any[]) {
super(...args)
export const ObservableClass = (options?: ObservableClassOptions) =>
<Self extends ObservableClassSelf>(self: Self) =>
class Observable extends self {
declare ["constructor"]: typeof Observable
makeObservable(this,
mapValues(this.constructor.fields, () => observable),
options,
)
}
} as Self
constructor(...args: any[]) {
super(...args)
makeObservable(this,
mapValues(this.constructor.fields, () => observable),
options,
)
}
} as Self

View File

@@ -0,0 +1 @@
export * from "./makeSchemaFormValidator"

View File

@@ -0,0 +1,138 @@
import { ArrayFormatter, Schema } from "@effect/schema"
import type { ParseOptions } from "@effect/schema/AST"
import type { DeepKeys, DeepValue, FieldApi, FormApi, FormValidationError, ValidationError, Validator } from "@tanstack/form-core"
import { Array, Effect, Either, flow, Layer, ManagedRuntime, pipe } from "effect"
import { mapValues } from "remeda"
export const makeSchemaFormValidator = <A, I>(
schema: Schema.Schema<A, I, never>,
options?: ParseOptions,
) => {
const decodeEither = Schema.decodeEither(schema, options)
return <
TFormData extends I,
TFormValidator extends Validator<TFormData, unknown> | undefined,
>(props: {
value: TFormData
formApi: FormApi<TFormData, TFormValidator>
}): FormValidationError<TFormData> =>
decodeEither(props.value).pipe(result =>
Either.isLeft(result)
? {
form: "Please check the fields",
fields: issuesToFieldsRecord(ArrayFormatter.formatErrorSync(result.left), props.formApi) as any,
}
: null
)
}
export const makeSchemaAsyncFormValidator = <A, I>(
schema: Schema.Schema<A, I, never>,
options?: ParseOptions,
) => {
const runtime = ManagedRuntime.make(Layer.empty)
const decode = Schema.decode(schema, options)
return <
TFormData extends I,
TFormValidator extends Validator<TFormData, unknown> | undefined,
>(props: {
value: TFormData
formApi: FormApi<TFormData, TFormValidator>
signal: AbortSignal
}): Promise<FormValidationError<TFormData>> =>
decode(props.value).pipe(
Effect.matchEffect({
onSuccess: () => Effect.succeed(null),
onFailure: e => ArrayFormatter.formatError(e).pipe(
Effect.map(issues => ({
form: "Please check the fields",
fields: issuesToFieldsRecord(issues, props.formApi) as any,
}))
),
}),
prgm => runtime.runPromise(prgm, { signal: props.signal }),
)
}
/**
* TODO: handle nested array fields
*/
const issuesToFieldsRecord = <
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
>(
issues: readonly ArrayFormatter.Issue[],
_formApi: FormApi<TFormData, TFormValidator>,
) => pipe(issues,
Array.groupBy(issue => issue.path.join(".")),
mapValues(flow(
Array.map(issue => issue.message),
Array.join("\n"),
)),
)
export const makeSchemaFieldValidator = <A, I>(
schema: Schema.Schema<A, I, never>,
options?: ParseOptions,
) => {
const decodeEither = Schema.decodeEither(schema, options)
return <
TParentData,
TName extends DeepKeys<TParentData>,
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
TFormValidator extends Validator<TParentData, unknown> | undefined,
TData extends I & DeepValue<TParentData, TName>,
>(props: {
value: TData
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
}): ValidationError =>
decodeEither(props.value).pipe(result =>
Either.isLeft(result)
? ArrayFormatter.formatErrorSync(result.left)
.map(issue => issue.message)
.join("\n")
: null
)
}
export const makeSchemaAsyncFieldValidator = <A, I>(
schema: Schema.Schema<A, I, never>,
options?: ParseOptions,
) => {
const runtime = ManagedRuntime.make(Layer.empty)
const decode = Schema.decode(schema, options)
return <
TParentData,
TName extends DeepKeys<TParentData>,
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
TFormValidator extends Validator<TParentData, unknown> | undefined,
TData extends I & DeepValue<TParentData, TName>,
>(props: {
value: TData
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
signal: AbortSignal
}): Promise<ValidationError> =>
decode(props.value).pipe(
Effect.matchEffect({
onSuccess: () => Effect.succeed(null),
onFailure: e => ArrayFormatter.formatError(e).pipe(
Effect.map(flow(
Array.map(issue => issue.message),
Array.join("\n"),
))
),
}),
prgm => runtime.runPromise(prgm, { signal: props.signal }),
)
}

View File

@@ -1,5 +1,6 @@
export * from "./Class"
export * from "./DateTime"
export * from "./Email"
export * from "./encodedAsPrismaJsonValue"
export * from "./Kind"
export * as MobX from "./MobX"

View File

@@ -10,4 +10,4 @@ export const toJsonifiable = <
jsonifiableSchema: Schema.Schema<JsonifiableA, JsonifiableI, JsonifiableR>
) =>
<A, R>(decodedSchema: Schema.Schema<A, JsonifiableA, R>) =>
Schema.compose(jsonifiableSchema, decodedSchema)
Schema.compose(jsonifiableSchema, decodedSchema, { strict: true })