jotai-state-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Primitive types implementation
3
+ * string, number, boolean, integer, Date, identifier, etc.
4
+ */
5
+
6
+ import type {
7
+ ISimpleType,
8
+ IType,
9
+ IValidationContext,
10
+ IValidationResult,
11
+ IIdentifierType,
12
+ IIdentifierNumberType,
13
+ ILiteralType,
14
+ IEnumerationType,
15
+ IFrozenType,
16
+ } from './types';
17
+
18
+ // ============================================================================
19
+ // Base Simple Type
20
+ // ============================================================================
21
+
22
+ function createSimpleType<T>(
23
+ name: string,
24
+ validator: (value: unknown) => boolean,
25
+ defaultValue?: T
26
+ ): ISimpleType<T> {
27
+ return {
28
+ name,
29
+ _kind: 'simple',
30
+ _C: undefined as unknown as T,
31
+ _S: undefined as unknown as T,
32
+ _T: undefined as unknown as T,
33
+
34
+ create(snapshot?: T): T {
35
+ if (snapshot === undefined) {
36
+ if (defaultValue !== undefined) {
37
+ return defaultValue;
38
+ }
39
+ throw new Error(`[jotai-state-tree] A value of type '${name}' is required`);
40
+ }
41
+ if (!validator(snapshot)) {
42
+ throw new Error(
43
+ `[jotai-state-tree] Value '${String(snapshot)}' is not a valid '${name}'`
44
+ );
45
+ }
46
+ return snapshot;
47
+ },
48
+
49
+ is(value: unknown): value is T {
50
+ return validator(value);
51
+ },
52
+
53
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
54
+ if (validator(value)) {
55
+ return { valid: true, errors: [] };
56
+ }
57
+ return {
58
+ valid: false,
59
+ errors: [
60
+ {
61
+ context,
62
+ value,
63
+ message: `Value '${String(value)}' is not a valid '${name}'`,
64
+ },
65
+ ],
66
+ };
67
+ },
68
+ };
69
+ }
70
+
71
+ // ============================================================================
72
+ // Primitive Types
73
+ // ============================================================================
74
+
75
+ /** String type */
76
+ export const string: ISimpleType<string> = createSimpleType<string>(
77
+ 'string',
78
+ (value): value is string => typeof value === 'string'
79
+ );
80
+
81
+ /** Number type (includes floats) */
82
+ export const number: ISimpleType<number> = createSimpleType<number>(
83
+ 'number',
84
+ (value): value is number => typeof value === 'number' && !isNaN(value)
85
+ );
86
+
87
+ /** Integer type */
88
+ export const integer: ISimpleType<number> = createSimpleType<number>(
89
+ 'integer',
90
+ (value): value is number =>
91
+ typeof value === 'number' && !isNaN(value) && Number.isInteger(value)
92
+ );
93
+
94
+ /** Boolean type */
95
+ export const boolean: ISimpleType<boolean> = createSimpleType<boolean>(
96
+ 'boolean',
97
+ (value): value is boolean => typeof value === 'boolean'
98
+ );
99
+
100
+ /** Date type - stores as number, exposes as Date */
101
+ export const DatePrimitive: IType<number | Date, number, Date> = {
102
+ name: 'Date',
103
+ _kind: 'simple' as const,
104
+ _C: undefined as unknown as number | Date,
105
+ _S: undefined as unknown as number,
106
+ _T: undefined as unknown as Date,
107
+
108
+ create(snapshot?: number | Date): Date {
109
+ if (snapshot === undefined) {
110
+ return new Date();
111
+ }
112
+ if (snapshot instanceof Date) {
113
+ return snapshot;
114
+ }
115
+ if (typeof snapshot === 'number') {
116
+ return new Date(snapshot);
117
+ }
118
+ throw new Error(`[jotai-state-tree] Value is not a valid Date`);
119
+ },
120
+
121
+ is(value: unknown): value is Date {
122
+ return value instanceof Date;
123
+ },
124
+
125
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
126
+ if (value instanceof Date || typeof value === 'number') {
127
+ return { valid: true, errors: [] };
128
+ }
129
+ return {
130
+ valid: false,
131
+ errors: [
132
+ {
133
+ context,
134
+ value,
135
+ message: 'Value is not a valid Date',
136
+ },
137
+ ],
138
+ };
139
+ },
140
+ };
141
+
142
+ /** Null type */
143
+ export const nullType: ISimpleType<null> = createSimpleType<null>(
144
+ 'null',
145
+ (value): value is null => value === null
146
+ );
147
+
148
+ /** Undefined type */
149
+ export const undefinedType: ISimpleType<undefined> = createSimpleType<undefined>(
150
+ 'undefined',
151
+ (value): value is undefined => value === undefined
152
+ );
153
+
154
+ // ============================================================================
155
+ // Identifier Types
156
+ // ============================================================================
157
+
158
+ /** String identifier type */
159
+ export const identifier: IIdentifierType = {
160
+ ...createSimpleType<string>(
161
+ 'identifier',
162
+ (value): value is string => typeof value === 'string'
163
+ ),
164
+ _kind: 'identifier' as const,
165
+ identifierAttribute: 'id',
166
+ };
167
+
168
+ /** Number identifier type */
169
+ export const identifierNumber: IIdentifierNumberType = {
170
+ ...createSimpleType<number>(
171
+ 'identifierNumber',
172
+ (value): value is number => typeof value === 'number' && !isNaN(value)
173
+ ),
174
+ _kind: 'identifierNumber' as const,
175
+ identifierAttribute: 'id',
176
+ };
177
+
178
+ // ============================================================================
179
+ // Literal Type
180
+ // ============================================================================
181
+
182
+ export function literal<T extends string | number | boolean>(value: T): ILiteralType<T> {
183
+ return {
184
+ name: `literal(${JSON.stringify(value)})`,
185
+ _kind: 'literal',
186
+ _value: value,
187
+ _C: undefined as unknown as T,
188
+ _S: undefined as unknown as T,
189
+ _T: undefined as unknown as T,
190
+
191
+ create(snapshot?: T): T {
192
+ if (snapshot === undefined) {
193
+ return value;
194
+ }
195
+ if (snapshot !== value) {
196
+ throw new Error(
197
+ `[jotai-state-tree] Value '${String(snapshot)}' is not the literal '${String(value)}'`
198
+ );
199
+ }
200
+ return snapshot;
201
+ },
202
+
203
+ is(v: unknown): v is T {
204
+ return v === value;
205
+ },
206
+
207
+ validate(v: unknown, context: IValidationContext[]): IValidationResult {
208
+ if (v === value) {
209
+ return { valid: true, errors: [] };
210
+ }
211
+ return {
212
+ valid: false,
213
+ errors: [
214
+ {
215
+ context,
216
+ value: v,
217
+ message: `Value '${String(v)}' is not the literal '${String(value)}'`,
218
+ },
219
+ ],
220
+ };
221
+ },
222
+ };
223
+ }
224
+
225
+ // ============================================================================
226
+ // Enumeration Type
227
+ // ============================================================================
228
+
229
+ export function enumeration<E extends string>(
230
+ name: string,
231
+ options: readonly E[]
232
+ ): IEnumerationType<E>;
233
+ export function enumeration<E extends string>(options: readonly E[]): IEnumerationType<E>;
234
+ export function enumeration<E extends string>(
235
+ nameOrOptions: string | readonly E[],
236
+ maybeOptions?: readonly E[]
237
+ ): IEnumerationType<E> {
238
+ const name = typeof nameOrOptions === 'string' ? nameOrOptions : 'enumeration';
239
+ const options = typeof nameOrOptions === 'string' ? maybeOptions! : nameOrOptions;
240
+
241
+ const optionSet = new Set(options);
242
+
243
+ return {
244
+ name,
245
+ _kind: 'enumeration',
246
+ _options: options,
247
+ _C: undefined as unknown as E,
248
+ _S: undefined as unknown as E,
249
+ _T: undefined as unknown as E,
250
+
251
+ create(snapshot?: E): E {
252
+ if (snapshot === undefined) {
253
+ throw new Error(`[jotai-state-tree] A value for enumeration '${name}' is required`);
254
+ }
255
+ if (!optionSet.has(snapshot)) {
256
+ throw new Error(
257
+ `[jotai-state-tree] Value '${snapshot}' is not a valid option for enumeration '${name}'. ` +
258
+ `Expected one of: ${options.join(', ')}`
259
+ );
260
+ }
261
+ return snapshot;
262
+ },
263
+
264
+ is(value: unknown): value is E {
265
+ return typeof value === 'string' && optionSet.has(value as E);
266
+ },
267
+
268
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
269
+ if (typeof value === 'string' && optionSet.has(value as E)) {
270
+ return { valid: true, errors: [] };
271
+ }
272
+ return {
273
+ valid: false,
274
+ errors: [
275
+ {
276
+ context,
277
+ value,
278
+ message: `Value '${String(value)}' is not a valid option. Expected one of: ${options.join(', ')}`,
279
+ },
280
+ ],
281
+ };
282
+ },
283
+ };
284
+ }
285
+
286
+ // ============================================================================
287
+ // Frozen Type
288
+ // ============================================================================
289
+
290
+ export function frozen<T = unknown>(): IFrozenType<T>;
291
+ export function frozen<T>(defaultValue: T): IFrozenType<T>;
292
+ export function frozen<T>(defaultValue?: T): IFrozenType<T> {
293
+ return {
294
+ name: 'frozen',
295
+ _kind: 'frozen',
296
+ _C: undefined as unknown as T,
297
+ _S: undefined as unknown as T,
298
+ _T: undefined as unknown as T,
299
+
300
+ create(snapshot?: T): T {
301
+ if (snapshot === undefined) {
302
+ if (defaultValue !== undefined) {
303
+ // Deep freeze if object
304
+ return deepFreeze(structuredClone(defaultValue));
305
+ }
306
+ return undefined as T;
307
+ }
308
+ // Deep freeze the snapshot
309
+ return deepFreeze(structuredClone(snapshot));
310
+ },
311
+
312
+ is(value: unknown): value is T {
313
+ // Frozen accepts any value
314
+ return true;
315
+ },
316
+
317
+ validate(): IValidationResult {
318
+ // Frozen accepts any value
319
+ return { valid: true, errors: [] };
320
+ },
321
+ };
322
+ }
323
+
324
+ function deepFreeze<T>(obj: T): T {
325
+ if (obj === null || typeof obj !== 'object') {
326
+ return obj;
327
+ }
328
+
329
+ Object.freeze(obj);
330
+
331
+ if (Array.isArray(obj)) {
332
+ obj.forEach(deepFreeze);
333
+ } else {
334
+ Object.values(obj).forEach(deepFreeze);
335
+ }
336
+
337
+ return obj;
338
+ }
339
+
340
+ // ============================================================================
341
+ // Custom Type
342
+ // ============================================================================
343
+
344
+ export interface CustomTypeOptions<C, S, T> {
345
+ name: string;
346
+ fromSnapshot(snapshot: S): T;
347
+ toSnapshot(value: T): S;
348
+ isTargetType(value: unknown): value is T;
349
+ getValidationMessage(value: unknown): string;
350
+ }
351
+
352
+ export function custom<C, S, T>(options: CustomTypeOptions<C, S, T>): IType<C, S, T> {
353
+ return {
354
+ name: options.name,
355
+ _kind: 'simple' as const,
356
+ _C: undefined as unknown as C,
357
+ _S: undefined as unknown as S,
358
+ _T: undefined as unknown as T,
359
+
360
+ create(snapshot?: C): T {
361
+ if (snapshot === undefined) {
362
+ throw new Error(`[jotai-state-tree] A value for custom type '${options.name}' is required`);
363
+ }
364
+ return options.fromSnapshot(snapshot as unknown as S);
365
+ },
366
+
367
+ is(value: unknown): value is T {
368
+ return options.isTargetType(value);
369
+ },
370
+
371
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
372
+ if (options.isTargetType(value)) {
373
+ return { valid: true, errors: [] };
374
+ }
375
+ return {
376
+ valid: false,
377
+ errors: [
378
+ {
379
+ context,
380
+ value,
381
+ message: options.getValidationMessage(value),
382
+ },
383
+ ],
384
+ };
385
+ },
386
+ };
387
+ }
388
+
389
+ // ============================================================================
390
+ // Finite Number Type
391
+ // ============================================================================
392
+
393
+ /** Finite number type (excludes Infinity and -Infinity) */
394
+ export const finite: ISimpleType<number> = createSimpleType<number>(
395
+ 'finite',
396
+ (value): value is number => typeof value === 'number' && isFinite(value)
397
+ );
398
+
399
+ /** Float type (alias for number) */
400
+ export const float = number;