Skip to content

Instantly share code, notes, and snippets.

@artalar
Last active March 19, 2024 05:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save artalar/94d74987f9faaf2a454f14d83db5bf13 to your computer and use it in GitHub Desktop.
Save artalar/94d74987f9faaf2a454f14d83db5bf13 to your computer and use it in GitHub Desktop.
reatomZod
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,
});
};
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>;
};
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