Last active
March 19, 2024 05:13
-
-
Save artalar/94d74987f9faaf2a454f14d83db5bf13 to your computer and use it in GitHub Desktop.
reatomZod
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
type Atom, | |
type Action, | |
type Ctx, | |
type Rec, | |
atom, | |
action, | |
type Fn, | |
type CtxSpy, | |
isCausedBy, | |
omit, | |
} from '@reatom/framework'; | |
export interface ListAtom< | |
Params extends any[] = any[], | |
Model extends Rec = Rec, | |
Key = any, | |
> extends Atom<Array<Model>> { | |
create: Action<Params, Model>; | |
createMany: Action<Array<Params>, Array<Model>>; | |
remove: Action<[Key], undefined | Model>; | |
removeMany: Action<[Array<Key>], Array<Model>>; | |
move: Action<[from: number, to: number], Array<Model>>; | |
clear: Action<[], void>; | |
get: (ctx: Ctx, key: Key) => undefined | Model; | |
spy: (ctx: CtxSpy, key: Key) => undefined | Model; | |
reatomMap: <T>(cb: (ctx: Ctx, el: Model) => T) => Atom<Array<T>>; | |
} | |
export const reatomList: { | |
/** an index will be used as a key */ | |
<Params extends any[], Model extends Rec>( | |
create: (ctx: Ctx, ...params: Params) => Model, | |
name: string, | |
): ListAtom<Params, Model, number>; | |
/** an index will be used as a key */ | |
<Params extends any[], Model extends Rec>( | |
options: { | |
create: (ctx: Ctx, ...params: Params) => Model; | |
initState?: Array<Model>; | |
}, | |
name: string, | |
): ListAtom<Params, Model, number>; | |
/** a model property will be used as a key if the model is an object */ | |
<Params extends any[], Model extends Rec, Key extends keyof Model>( | |
options: { | |
create: (ctx: Ctx, ...params: Params) => Model; | |
initState?: Array<Model>; | |
find: Key; | |
}, | |
name: string, | |
): ListAtom<Params, Model, Model[Key]>; | |
<Params extends any[], Model extends Rec, Key>( | |
options: { | |
create: (ctx: Ctx, ...params: Params) => Model; | |
find: (ctx: Ctx, list: Array<Model>, key: Key) => undefined | Model; | |
initState?: Array<Model>; | |
}, | |
name: string, | |
): ListAtom<Params, Model, Key>; | |
} = ( | |
options: | |
| Fn | |
| { | |
create: Fn; | |
initState?: Array<any>; | |
find?: Fn; | |
}, | |
name: string, | |
): ListAtom => { | |
const { | |
create: createFn, | |
initState = [], | |
find = (ctx: Ctx, list: any[], key: any) => list[key as number], | |
} = typeof options === 'function' ? { create: options } : options; | |
const _find = | |
typeof find === 'string' | |
? (ctx: Ctx, list: any[], key: any) => list.find((el) => el[find] === key) | |
: find; | |
const listAtom = atom(initState, name); | |
const create = action((ctx, ...params: any[]) => { | |
const model = createFn(ctx, ...params); | |
const list = ctx.get(listAtom); | |
if (isCausedBy(ctx, createMany)) { | |
list.push(model); | |
} else { | |
listAtom(ctx, (list) => [...list, model]); | |
} | |
return model; | |
}, `${name}.create`); | |
const createMany = action((ctx, paramsList: any[][]) => { | |
listAtom(ctx, (list) => [...list]); | |
return paramsList.map((params) => create(ctx, ...params)); | |
}, `${name}.createMany`); | |
const remove = action((ctx, key: any) => { | |
const list = ctx.get(listAtom); | |
const model = _find(ctx, list, key); | |
if (model) { | |
if (isCausedBy(ctx, removeMany)) { | |
list.splice(list.indexOf(model), 1); | |
} else { | |
listAtom(ctx, (list) => list.filter((el) => el !== model)); | |
} | |
} | |
return model; | |
}, `${name}.remove`); | |
const removeMany = action((ctx, keys: any[]) => { | |
listAtom(ctx, (list) => [...list]); | |
return keys.map((key) => remove(ctx, key)).filter(Boolean); | |
}, `${name}.removeMany`); | |
const move = action((ctx, from: number, to: number) => { | |
const list = ctx.get(listAtom); | |
if ([from, to].some((index) => index < 0 || index >= list.length)) { | |
throw new Error('Invalid range'); | |
} | |
const newList = []; | |
for (let i = 0; i < list.length; i++) { | |
if (i === to) newList.push(list[from]); | |
if (i === from) continue; | |
newList.push(list[i]); | |
} | |
return listAtom(ctx, newList); | |
}, `${name}.move`); | |
const clear = action((ctx) => { | |
listAtom(ctx, []); | |
}, `${name}.clear`); | |
const get = (ctx: Ctx, key: any) => _find(ctx, ctx.get(listAtom), key); | |
const spy = (ctx: CtxSpy, key: any) => _find(ctx, ctx.spy(listAtom), key); | |
// TODO @artalar optimize it | |
const reatomMap = <T>(cb: (ctx: Ctx, el: any) => T) => | |
atom((ctx, state: any[] = []) => { | |
const list = ctx.spy(listAtom); | |
const cbCtx = omit(ctx, ['spy']); | |
for (let i = 0; i < list.length; i++) { | |
ctx.spy(list[i], (value) => { | |
if (ctx.cause.state === state) state = state.slice(); | |
state[i] = cb(cbCtx, value); | |
}); | |
} | |
return state; | |
}); | |
return Object.assign(listAtom, { | |
create, | |
createMany, | |
remove, | |
removeMany, | |
move, | |
clear, | |
get, | |
spy, | |
reatomMap, | |
}); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
atom, | |
type Rec, | |
__count, | |
type Fn, | |
reatomEnum, | |
type Ctx, | |
type Atom, | |
reatomBoolean, | |
reatomNumber, | |
reatomRecord, | |
reatomMap, | |
reatomSet, | |
action, | |
isCausedBy, | |
} from '@reatom/framework'; | |
import { reatomList } from 'src/reatom-list'; | |
import { z } from 'zod'; | |
import { type PartialDeep, type ZodAtomization } from './reatomZod/types'; | |
export const silentUpdate = action((ctx, cb: Fn<[Ctx]>) => { | |
cb(ctx); | |
}); | |
export const reatomZod = <Schema extends z.ZodFirstPartySchemaTypes>( | |
{ _def: def }: Schema, | |
{ | |
sync, | |
initState, | |
name = __count(`reatomZod.${def.typeName}`), | |
}: { | |
sync?: Fn<[Ctx]>; | |
initState?: PartialDeep<z.infer<Schema>>; | |
name?: string; | |
} = {}, | |
): ZodAtomization<Schema> => { | |
let state: any = initState; | |
let theAtom: Atom; | |
switch (def.typeName) { | |
case z.ZodFirstPartyTypeKind.ZodNever: { | |
throw new Error('Never type'); | |
} | |
case z.ZodFirstPartyTypeKind.ZodNaN: { | |
return NaN as ZodAtomization<Schema>; | |
} | |
// @ts-expect-error TODO | |
case z.ZodFirstPartyTypeKind.ZodReadonly: | |
case z.ZodFirstPartyTypeKind.ZodVoid: { | |
return initState as ZodAtomization<Schema>; | |
} | |
case z.ZodFirstPartyTypeKind.ZodUnknown: | |
case z.ZodFirstPartyTypeKind.ZodUndefined: { | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodNull: { | |
state ??= null; | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodLiteral: { | |
return state ?? def.value; | |
} | |
case z.ZodFirstPartyTypeKind.ZodString: { | |
if (state === undefined) state = ''; | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodNumber: { | |
theAtom = reatomNumber(state, name); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodDate: { | |
if (typeof state === 'number') { | |
state = new Date(state); | |
} else { | |
if (state === undefined) state = new Date(); | |
} | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodBoolean: { | |
theAtom = reatomBoolean(state, name); | |
break; | |
} | |
// case z.ZodFirstPartyTypeKind.ZodSymbol: { | |
// if (state === undefined) state = Symbol(); | |
// break; | |
// } | |
case z.ZodFirstPartyTypeKind.ZodObject: { | |
const obj = {} as Rec; | |
for (const [key, child] of Object.entries(def.shape())) { | |
obj[key] = reatomZod(child as z.ZodFirstPartySchemaTypes, { | |
sync, | |
initState: (initState as any)?.[key], | |
name: `${name}.${key}`, | |
}); | |
} | |
return obj as ZodAtomization<Schema>; | |
} | |
case z.ZodFirstPartyTypeKind.ZodTuple: { | |
if (state === undefined) { | |
state = def.items.map((item: z.ZodFirstPartySchemaTypes, i: number) => | |
reatomZod(item, { sync, name: `${name}#${i}` }), | |
); | |
} | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodArray: { | |
// TODO @artalar generate a better name, instead of using `__count` | |
theAtom = reatomList( | |
{ | |
create: (ctx, initState) => | |
reatomZod(def.type, { sync, initState, name: __count(name) }), | |
initState: (initState as any[] | undefined)?.map((initState: any) => | |
reatomZod(def.type, { sync, initState, name: __count(name) }), | |
), | |
}, | |
name, | |
); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodRecord: { | |
theAtom = reatomRecord(state ?? {}, name); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodMap: { | |
theAtom = reatomMap(state ? new Map(state) : new Map(), name); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodSet: { | |
theAtom = reatomSet(state ? new Set(state) : new Set(), name); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodEnum: { | |
theAtom = reatomEnum(def.values, { initState, name }); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodNativeEnum: { | |
theAtom = reatomEnum(Object.values(def.values), { initState, name }); | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodUnion: { | |
// TODO @artalar not sure about this logic | |
state = | |
def.options.find( | |
(type: z.ZodDefault<any>) => type._def.defaultValue?.(), | |
) ?? initState; | |
break; | |
} | |
case z.ZodFirstPartyTypeKind.ZodOptional: { | |
return reatomZod(def.innerType, { sync, initState, name }); | |
} | |
case z.ZodFirstPartyTypeKind.ZodNullable: { | |
return reatomZod(def.innerType, { | |
sync, | |
initState: initState ?? null, | |
name, | |
}); | |
} | |
case z.ZodFirstPartyTypeKind.ZodDefault: { | |
return reatomZod(def.innerType, { | |
sync, | |
initState: initState ?? def.defaultValue(), | |
name, | |
}); | |
} | |
default: { | |
// @ts-expect-error // TODO | |
const typeName: never = def.typeName; | |
if (typeName) throw new TypeError(`Unsupported Zod type: ${typeName}`); | |
theAtom = atom(initState, name); | |
} | |
} | |
theAtom ??= atom(state, name); | |
theAtom.onChange((ctx, value) => { | |
if (isCausedBy(ctx, silentUpdate)) return; | |
// TODO @artalar the parse is required for using the default values | |
// type.parse(parseAtoms(ctx, value)); | |
sync?.(ctx); | |
}); | |
return theAtom as ZodAtomization<Schema>; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { type AtomMut, type BooleanAtom, type NumberAtom, type EnumAtom, type RecordAtom, type MapAtom, type SetAtom } from "@reatom/framework"; | |
import { type ListAtom } from "src/reatom-list"; | |
import { type z } from "zod"; | |
export type ZodAtomization<T extends z.ZodFirstPartySchemaTypes, Union = never> = T extends z.ZodAny | |
? AtomMut<any | Union> | |
: T extends z.ZodUnknown | |
? AtomMut<unknown | Union> | |
: T extends z.ZodNever | |
? never | |
: T extends z.ZodReadonly<infer Type> | |
? z.infer<Type> | Union | |
: T extends z.ZodUndefined | |
? AtomMut<undefined | Union> | |
: T extends z.ZodVoid | |
? undefined | Union | |
: T extends z.ZodNaN | |
? number | Union | |
: T extends z.ZodNull | |
? AtomMut<null | Union> | |
: T extends z.ZodLiteral<infer T> | |
? T | Union | |
: T extends z.ZodBoolean | |
? never extends Union | |
? BooleanAtom | |
: AtomMut<boolean | Union> | |
: T extends z.ZodNumber | |
? never extends Union | |
? NumberAtom | |
: AtomMut<number | Union> | |
: T extends z.ZodBigInt | |
? AtomMut<bigint | Union> | |
: T extends z.ZodString | |
? AtomMut<string | Union> | |
: T extends z.ZodSymbol | |
? AtomMut<symbol | Union> | |
: T extends z.ZodDate | |
? AtomMut<Date | Union> | |
: T extends z.ZodArray<infer T> | |
? ListAtom<[void | Partial<z.infer<T>>], ZodAtomization<T>, number> // FIXME Union | |
: T extends z.ZodTuple<infer Tuple> | |
? AtomMut<z.infer<Tuple[number]> | Union> | |
: T extends z.ZodObject<infer Shape> | |
? never extends Union | |
? { | |
[K in keyof Shape]: ZodAtomization<Shape[K]>; | |
} | |
: AtomMut<Shape | Union> | |
: T extends z.ZodRecord<infer KeyType, infer ValueType> | |
? never extends Union | |
? RecordAtom<Record<z.infer<KeyType>, ZodAtomization<ValueType>>> | |
: AtomMut<Record<z.infer<KeyType>, ZodAtomization<ValueType>> | Union> | |
: T extends z.ZodMap<infer KeyType, infer ValueType> | |
? never extends Union | |
? MapAtom<z.infer<KeyType>, ZodAtomization<ValueType>> | |
: AtomMut<Map<z.infer<KeyType>, ZodAtomization<ValueType>> | Union> | |
: T extends z.ZodSet<infer ValueType> | |
? never extends Union | |
? SetAtom<z.infer<ValueType>> | |
: AtomMut<Set<z.infer<ValueType>> | Union> | |
: T extends z.ZodEnum<infer Enum> | |
? never extends Union | |
? EnumAtom<Enum[number]> | |
: AtomMut<Enum[number] | Union> | |
: T extends z.ZodNativeEnum<infer Enum> | |
? never extends Union | |
? // @ts-expect-error шо? | |
EnumAtom<Enum[keyof Enum]> | |
: AtomMut<Enum[keyof Enum] | Union> | |
: T extends z.ZodDefault<infer T> | |
? ZodAtomization<T, Union extends undefined ? never : Union> | |
: T extends z.ZodOptional<infer T> | |
? ZodAtomization<T, undefined | Union> | |
: T extends z.ZodNullable<infer T> | |
? ZodAtomization<T, null | Union> | |
: T extends z.ZodUnion<infer T> | |
? AtomMut<z.infer<T[number]> | Union> | |
: T; | |
type Primitive = null | undefined | string | number | boolean | symbol | bigint; | |
type BuiltIns = Primitive | Date | RegExp; | |
export type PartialDeep<T> = T extends BuiltIns | |
? T | undefined | |
: T extends object | |
? T extends ReadonlyArray<any> | |
? T | |
: { | |
[K in keyof T]?: PartialDeep<T[K]>; | |
} | |
: unknown; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment