Skip to content

Commit

Permalink
fixed multilevel objectsa and added back tests and example
Browse files Browse the repository at this point in the history
  • Loading branch information
overthemike committed Sep 1, 2024
1 parent ad1b1c2 commit 5296750
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 177 deletions.
64 changes: 32 additions & 32 deletions examples/react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './App.css'
import { z } from 'zod'
import { schema, useSnapshot } from '../../../dist/index.js'
import './App.css';
import { z } from 'zod';
import { schema, useSnapshot } from '../../../dist/index.js';

const userSchema = z.object({
username: z.string(),
Expand All @@ -10,10 +10,10 @@ const userSchema = z.object({
lastName: z.string(),
address: z.object({
city: z.string(),
country: z.string()
})
})
})
country: z.string(),
}),
}),
});

const userState = schema(userSchema).proxy({
username: 'Alice',
Expand All @@ -23,73 +23,73 @@ const userState = schema(userSchema).proxy({
lastName: 'Smith',
address: {
city: 'Wonderland',
country: 'Fantasy'
}
}
})
country: 'Fantasy',
},
},
});

function App() {
const user = useSnapshot(userState)
const user = useSnapshot(userState);

return (
<div>
<h1>Vite + React</h1>

<label htmlFor='username'>Username</label>
<label htmlFor="username">Username</label>
<input
id='username'
type='text'
id="username"
type="text"
value={user.username}
onChange={(e) => (userState.username = e.target.value)}
/>
<p>Username: {user.username}</p>

<label htmlFor='age'>Age</label>
<label htmlFor="age">Age</label>
<input
id='age'
type='number'
id="age"
type="number"
value={user.age}
onChange={(e) => (userState.age = Number(e.target.value))}
/>
<p>Age: {user.age}</p>

<label htmlFor='lastName'>First Name</label>
<label htmlFor="lastName">First Name</label>
<input
id='firstName'
type='text'
id="firstName"
type="text"
value={user.profile.firstName}
onChange={(e) => (userState.profile.firstName = e.target.value)}
/>
<p>First Name: {user.profile.firstName}</p>

<label htmlFor='lastName'>Last Name</label>
<label htmlFor="lastName">Last Name</label>
<input
id='lastName'
type='text'
id="lastName"
type="text"
value={user.profile.lastName}
onChange={(e) => (userState.profile.lastName = e.target.value)}
/>
<p>Last Name: {user.profile.lastName}</p>

<label htmlFor='lastName'>Last Name</label>
<label htmlFor="lastName">Last Name</label>
<input
id='city'
type='text'
id="city"
type="text"
value={user.profile.address.city}
onChange={(e) => (userState.profile.address.city = e.target.value)}
/>
<p>City: {user.profile.address.city}</p>

<label htmlFor='country'>Last Name</label>
<label htmlFor="country">Last Name</label>
<input
id='country'
type='text'
id="country"
type="text"
value={user.profile.address.country}
onChange={(e) => (userState.profile.address.country = e.target.value)}
/>
<p>Last Name: {user.profile.address.country}</p>
</div>
)
);
}

export default App
export default App;
170 changes: 85 additions & 85 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,174 +1,174 @@
/* eslint-disable */
import { z, ZodType } from 'zod'
import { proxy as vproxy, useSnapshot as vsnap } from 'valtio'
import _ from 'lodash'
import { z, ZodType } from 'zod';
import { proxy as vproxy, useSnapshot as vsnap } from 'valtio';
import _ from 'lodash';

type ValtioProxy<T> = {
[P in keyof T]: T[P]
}
[P in keyof T]: T[P];
};

type SchemaConfig = {
parseAsync?: boolean
safeParse?: boolean
errorHandler?: (error: unknown) => void
}
parseAsync?: boolean;
safeParse?: boolean;
errorHandler?: (error: unknown) => void;
};

const defaultConfig = {
parseAsync: false,
safeParse: false,
errorHandler: (error: unknown) => console.error(error)
}
errorHandler: (error: unknown) => console.error(error),
};

export const vzGlobalConfig = {
safeParse: false,
errorHandler: (error: unknown) => console.error(error)
}
errorHandler: (error: unknown) => console.error(error),
};

const isObject = (x: unknown): x is object =>
typeof x === 'object' && x !== null
typeof x === 'object' && x !== null;

type MergedConfig = Required<SchemaConfig>
type MergedConfig = Required<SchemaConfig>;

type SchemaMeta = SchemaConfig & {
initialState: unknown
}
initialState: unknown;
};

type PropType = string | number | symbol
const schemaMeta = new WeakMap<ZodType<any>, SchemaMeta>()
const pathList = new WeakMap<{}, PropType[]>()
type PropType = string | number | symbol;
const schemaMeta = new WeakMap<ZodType<any>, SchemaMeta>();
const pathList = new WeakMap<{}, PropType[]>();

type SchemaReturn<T extends ZodType<any>> = {
proxy: {
(initialState: any, config?: SchemaConfig): ValtioProxy<z.infer<T>>
}
}
(initialState: any, config?: SchemaConfig): ValtioProxy<z.infer<T>>;
};
};

const valtioStoreSymbol = Symbol('valtioStore')
const valtioStoreSymbol = Symbol('valtioStore');

export const useSnapshot = (store: any) => {
return vsnap(store[valtioStoreSymbol])
}
return vsnap(store[valtioStoreSymbol]);
};

export const schema = <T extends ZodType<any>>(
zodSchema: T
zodSchema: T,
): SchemaReturn<T> => {
const proxy = (
initialState: z.infer<T>,
config: SchemaConfig = {}
config: SchemaConfig = {},
): ValtioProxy<z.infer<T>> => {
if (!isObject(initialState)) {
throw new Error('object required')
throw new Error('object required');
}

const mergedConfig: MergedConfig = { ...defaultConfig, ...config }
const mergedConfig: MergedConfig = { ...defaultConfig, ...config };

const parseAsync = mergedConfig.parseAsync
const safeParse = mergedConfig.safeParse
const errorHandler = mergedConfig.errorHandler
const parseAsync = mergedConfig.parseAsync;
const safeParse = mergedConfig.safeParse;
const errorHandler = mergedConfig.errorHandler;

// before proxying, validate the initial state
if (parseAsync) {
zodSchema.parseAsync(initialState).catch((e) => {
throw e
})
throw e;
});
} else {
zodSchema.parse(initialState)
zodSchema.parse(initialState);
}

const valtioProxy = vproxy(initialState)
const valtioProxy = vproxy(initialState);

const createProxy = (target: any, parentPath: PropType[] = []): any => {
if (!schemaMeta.has(zodSchema)) {
schemaMeta.set(zodSchema, {
safeParse,
parseAsync,
errorHandler,
initialState
})
initialState,
});
}

return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver)
const value = Reflect.get(target, prop, receiver);
if (isObject(value)) {
const newPath = parentPath.concat(prop)
pathList.set(value, newPath)
return createProxy(value, newPath)
const newPath = parentPath.concat(prop);
pathList.set(value, newPath);
return createProxy(value, newPath);
} else {
const pathToSet = [...(pathList.get(target) || []), prop]
return _.get(valtioProxy, pathToSet, value)
const pathToSet = [...(pathList.get(target) || []), prop];
return _.get(valtioProxy, pathToSet, value);
}
},
set(target, prop, value, receiver) {
const originalObject = schemaMeta.get(zodSchema)!
.initialState as z.infer<T>
.initialState as z.infer<T>;

const objectToValidate = _.cloneDeep(originalObject)
const path = (pathList.get(target) || []).concat(prop)
const objectToValidate = _.cloneDeep(originalObject);
const path = (pathList.get(target) || []).concat(prop);

_.set(objectToValidate, path, value)
_.set(objectToValidate, path, value);

const handleAsyncParse = async () => {
try {
const parsedValue = await zodSchema.parseAsync(objectToValidate)
_.set(valtioProxy, value, path)
Reflect.set(target, prop, value, receiver)
return true
const parsedValue = await zodSchema.parseAsync(objectToValidate);
_.set(valtioProxy, value, path);
Reflect.set(target, prop, value, receiver);
return true;
} catch (error) {
errorHandler(error)
errorHandler(error);
if (!safeParse) {
throw error
throw error;
}
return false
return false;
}
}
};

const handleSyncParse = () => {
try {
if (safeParse) {
const result = zodSchema.safeParse(objectToValidate)
const result = zodSchema.safeParse(objectToValidate);
if (result.success) {
valtioProxy[prop] = value
Reflect.set(target, prop, value, receiver)
return true
valtioProxy[prop] = value;
Reflect.set(target, prop, value, receiver);
return true;
} else {
errorHandler(result.error)
return false
errorHandler(result.error);
return false;
}
} else {
const parsedValue = zodSchema.parse(objectToValidate)
Reflect.set(target, prop, value, receiver)
valtioProxy[prop] = value
return true
const parsedValue = zodSchema.parse(objectToValidate);
Reflect.set(target, prop, value, receiver);
valtioProxy[prop] = value;
return true;
}
} catch (error) {
errorHandler(error)
errorHandler(error);
if (!safeParse) {
throw error
throw error;
}
return false
return false;
}
}
};

if (parseAsync) {
handleAsyncParse().catch((error) => {
errorHandler(error)
errorHandler(error);
if (!safeParse) {
throw error
throw error;
}
})
return true
});
return true;
} else {
return handleSyncParse()
return handleSyncParse();
}
}
})
}
},
});
};

const store = createProxy(valtioProxy)
store[valtioStoreSymbol] = valtioProxy
const store = createProxy(valtioProxy);
store[valtioStoreSymbol] = valtioProxy;

return store
}
return { proxy }
}
return store;
};
return { proxy };
};
Loading

0 comments on commit 5296750

Please sign in to comment.