0.1.15 (#16)
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:
12
src/Schema/Email.ts
Normal file
12
src/Schema/Email.ts
Normal 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",
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
|
||||
1
src/Schema/TanStackForm/index.ts
Normal file
1
src/Schema/TanStackForm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./makeSchemaFormValidator"
|
||||
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal file
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal 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 }),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./Class"
|
||||
export * from "./DateTime"
|
||||
export * from "./Email"
|
||||
export * from "./encodedAsPrismaJsonValue"
|
||||
export * from "./Kind"
|
||||
export * as MobX from "./MobX"
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user