@flare-ts/lib 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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/schema/index.d.ts +10 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +21 -0
- package/dist/schema/internal/index.d.ts +4 -0
- package/dist/schema/internal/index.d.ts.map +1 -0
- package/dist/schema/internal/index.js +8 -0
- package/dist/schema/internal/parser/array.d.ts +7 -0
- package/dist/schema/internal/parser/array.d.ts.map +1 -0
- package/dist/schema/internal/parser/array.js +33 -0
- package/dist/schema/internal/parser/discriminated.d.ts +11 -0
- package/dist/schema/internal/parser/discriminated.d.ts.map +1 -0
- package/dist/schema/internal/parser/discriminated.js +40 -0
- package/dist/schema/internal/parser/input.d.ts +26 -0
- package/dist/schema/internal/parser/input.d.ts.map +1 -0
- package/dist/schema/internal/parser/input.js +56 -0
- package/dist/schema/internal/parser/object.d.ts +16 -0
- package/dist/schema/internal/parser/object.d.ts.map +1 -0
- package/dist/schema/internal/parser/object.js +81 -0
- package/dist/schema/internal/parser/path.d.ts +5 -0
- package/dist/schema/internal/parser/path.d.ts.map +1 -0
- package/dist/schema/internal/parser/path.js +11 -0
- package/dist/schema/internal/parser/record.d.ts +8 -0
- package/dist/schema/internal/parser/record.d.ts.map +1 -0
- package/dist/schema/internal/parser/record.js +44 -0
- package/dist/schema/internal/token/optionality.d.ts +6 -0
- package/dist/schema/internal/token/optionality.d.ts.map +1 -0
- package/dist/schema/internal/token/optionality.js +11 -0
- package/dist/schema/internal/token/symbols.d.ts +7 -0
- package/dist/schema/internal/token/symbols.d.ts.map +1 -0
- package/dist/schema/internal/token/symbols.js +8 -0
- package/dist/schema/internal/types/inference.d.ts +20 -0
- package/dist/schema/internal/types/inference.d.ts.map +1 -0
- package/dist/schema/internal/types/inference.js +0 -0
- package/dist/schema/json/serializer.d.ts +51 -0
- package/dist/schema/json/serializer.d.ts.map +1 -0
- package/dist/schema/json/serializer.js +265 -0
- package/dist/schema/model.d.ts +54 -0
- package/dist/schema/model.d.ts.map +1 -0
- package/dist/schema/model.js +41 -0
- package/dist/schema/primitives/array.d.ts +14 -0
- package/dist/schema/primitives/array.d.ts.map +1 -0
- package/dist/schema/primitives/array.js +21 -0
- package/dist/schema/primitives/bool.d.ts +7 -0
- package/dist/schema/primitives/bool.d.ts.map +1 -0
- package/dist/schema/primitives/bool.js +12 -0
- package/dist/schema/primitives/date.d.ts +23 -0
- package/dist/schema/primitives/date.d.ts.map +1 -0
- package/dist/schema/primitives/date.js +92 -0
- package/dist/schema/primitives/enum.d.ts +19 -0
- package/dist/schema/primitives/enum.d.ts.map +1 -0
- package/dist/schema/primitives/enum.js +19 -0
- package/dist/schema/primitives/float.d.ts +23 -0
- package/dist/schema/primitives/float.d.ts.map +1 -0
- package/dist/schema/primitives/float.js +33 -0
- package/dist/schema/primitives/index.d.ts +89 -0
- package/dist/schema/primitives/index.d.ts.map +1 -0
- package/dist/schema/primitives/index.js +44 -0
- package/dist/schema/primitives/int.d.ts +24 -0
- package/dist/schema/primitives/int.d.ts.map +1 -0
- package/dist/schema/primitives/int.js +43 -0
- package/dist/schema/primitives/str.d.ts +26 -0
- package/dist/schema/primitives/str.d.ts.map +1 -0
- package/dist/schema/primitives/str.js +30 -0
- package/dist/schema/primitives/text.d.ts +33 -0
- package/dist/schema/primitives/text.d.ts.map +1 -0
- package/dist/schema/primitives/text.js +30 -0
- package/dist/schema/primitives/uuid.d.ts +7 -0
- package/dist/schema/primitives/uuid.d.ts.map +1 -0
- package/dist/schema/primitives/uuid.js +13 -0
- package/dist/schema/schema.d.ts +167 -0
- package/dist/schema/schema.d.ts.map +1 -0
- package/dist/schema/schema.js +74 -0
- package/dist/schema/symbol.d.ts +15 -0
- package/dist/schema/symbol.d.ts.map +1 -0
- package/dist/schema/symbol.js +6 -0
- package/package.json +49 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { SCHEMA_BRAND, SCHEMA_DESCRIPTOR, SCHEMA_REQUIRED } from "../internal/token/symbols.js";
|
|
2
|
+
function isSchemaToken(value) {
|
|
3
|
+
return SCHEMA_BRAND in value;
|
|
4
|
+
}
|
|
5
|
+
function isSchemaArrayToken(value) {
|
|
6
|
+
return isSchemaToken(value) && Array.isArray(value[SCHEMA_DESCRIPTOR]);
|
|
7
|
+
}
|
|
8
|
+
function isDescriptorValueRequired(value) {
|
|
9
|
+
if (isSchemaToken(value)) return value[SCHEMA_REQUIRED] !== false;
|
|
10
|
+
return value._required !== false;
|
|
11
|
+
}
|
|
12
|
+
function buildObjectArrayHelper(helperName, itemCode, closureArgs, closureVals, braceEmbedded = false) {
|
|
13
|
+
const itemAssign = braceEmbedded ? `p[i] = (${itemCode}) + '}';` : `p[i] = '{' + (${itemCode}) + '}';`;
|
|
14
|
+
return new Function(
|
|
15
|
+
...closureArgs,
|
|
16
|
+
`return function ${helperName}(arr){
|
|
17
|
+
const p = new Array(arr.length);
|
|
18
|
+
for (let i = 0; i < arr.length; i++) {
|
|
19
|
+
const item = arr[i];
|
|
20
|
+
${itemAssign}
|
|
21
|
+
}
|
|
22
|
+
return '['+p.join(',')+']';
|
|
23
|
+
}`
|
|
24
|
+
)(...closureVals);
|
|
25
|
+
}
|
|
26
|
+
function buildPrimitiveArrayHelper(helperName, itemExpr, closureArgs = [], closureVals = []) {
|
|
27
|
+
return new Function(
|
|
28
|
+
...closureArgs,
|
|
29
|
+
`return function ${helperName}(arr){
|
|
30
|
+
const p = new Array(arr.length);
|
|
31
|
+
for (let i = 0; i < arr.length; i++) {
|
|
32
|
+
const v = arr[i];
|
|
33
|
+
p[i] = ${itemExpr};
|
|
34
|
+
}
|
|
35
|
+
return '['+p.join(',')+']';
|
|
36
|
+
}`
|
|
37
|
+
)(...closureVals);
|
|
38
|
+
}
|
|
39
|
+
const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
40
|
+
const DIRTY_RE = /[\x00-\x1f"\\]/;
|
|
41
|
+
function compileSerializer(token) {
|
|
42
|
+
const introspectable = token;
|
|
43
|
+
const descriptor = introspectable[SCHEMA_DESCRIPTOR];
|
|
44
|
+
if (Array.isArray(descriptor)) {
|
|
45
|
+
const first = descriptor[0];
|
|
46
|
+
if (first !== null && typeof first === "object" && "$record" in first) {
|
|
47
|
+
return jsonStringifyFallback;
|
|
48
|
+
}
|
|
49
|
+
const itemDescriptor = descriptor[0][SCHEMA_DESCRIPTOR];
|
|
50
|
+
const inner = buildSerializer(itemDescriptor);
|
|
51
|
+
return function serializeArray(doc) {
|
|
52
|
+
const arr = doc;
|
|
53
|
+
const parts = new Array(arr.length);
|
|
54
|
+
for (let i = 0; i < arr.length; i++) parts[i] = inner(arr[i]);
|
|
55
|
+
return "[" + parts.join(",") + "]";
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (isDiscriminatedDescriptor(descriptor)) {
|
|
59
|
+
return jsonStringifyFallback;
|
|
60
|
+
}
|
|
61
|
+
return buildSerializer(descriptor);
|
|
62
|
+
}
|
|
63
|
+
function jsonStringifyFallback(doc) {
|
|
64
|
+
return JSON.stringify(doc);
|
|
65
|
+
}
|
|
66
|
+
function serializeText(str) {
|
|
67
|
+
if (!DIRTY_RE.test(str)) return '"' + str + '"';
|
|
68
|
+
return JSON.stringify(str);
|
|
69
|
+
}
|
|
70
|
+
function serializeDate(value, format) {
|
|
71
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) return "null";
|
|
72
|
+
switch (format) {
|
|
73
|
+
case "DMY":
|
|
74
|
+
case "MDY":
|
|
75
|
+
case "YMD":
|
|
76
|
+
return '"' + value.toISOString().slice(0, 10) + '"';
|
|
77
|
+
case "TIMESTAMP":
|
|
78
|
+
return String(value.getTime());
|
|
79
|
+
default:
|
|
80
|
+
return '"' + value.toISOString() + '"';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildSerializer(descriptor) {
|
|
84
|
+
const closureArgs = ["esc", "date"];
|
|
85
|
+
const closureVals = [serializeText, serializeDate];
|
|
86
|
+
let generatedNameId = 0;
|
|
87
|
+
function nextGeneratedName(prefix, key) {
|
|
88
|
+
generatedNameId += 1;
|
|
89
|
+
return `${prefix}_${key}_${generatedNameId}`;
|
|
90
|
+
}
|
|
91
|
+
function emitFields(fields, acc, wrapBrace = false) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
let firstReq = true;
|
|
94
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
95
|
+
const required = isDescriptorValueRequired(field);
|
|
96
|
+
const isSafeString = !isSchemaToken(field) && (field._type === "string" || field._type === "uuid");
|
|
97
|
+
if (!VALID_IDENTIFIER.test(key)) {
|
|
98
|
+
throw new Error(`flareSchema: invalid field key "${key}" - must be a valid JS identifier`);
|
|
99
|
+
}
|
|
100
|
+
if (required) {
|
|
101
|
+
const sep = firstReq && wrapBrace ? '{"' : firstReq ? '"' : ',"';
|
|
102
|
+
const kPrefix = `'${sep}${key}":`;
|
|
103
|
+
if (isSafeString) {
|
|
104
|
+
parts.push(`${kPrefix}"' + ${acc}.${key} + '"'`);
|
|
105
|
+
} else {
|
|
106
|
+
parts.push(`${kPrefix}' + ${emitValue(key, field, acc)}`);
|
|
107
|
+
}
|
|
108
|
+
firstReq = false;
|
|
109
|
+
} else {
|
|
110
|
+
if (isSafeString) {
|
|
111
|
+
parts.push(`(${acc}.${key} != null ? ',"${key}":"' + ${acc}.${key} + '"' : '')`);
|
|
112
|
+
} else {
|
|
113
|
+
parts.push(`(${acc}.${key} != null ? ',"${key}":' + ${emitValue(key, field, acc)} : '')`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return parts.join("+");
|
|
118
|
+
}
|
|
119
|
+
function emitValue(key, field, acc) {
|
|
120
|
+
if (isSchemaArrayToken(field)) {
|
|
121
|
+
const itemDescriptor = field[SCHEMA_DESCRIPTOR][0][SCHEMA_DESCRIPTOR];
|
|
122
|
+
const firstItemField = Object.values(itemDescriptor)[0];
|
|
123
|
+
const canEmbedBrace = firstItemField != null && isDescriptorValueRequired(firstItemField);
|
|
124
|
+
const itemCode = emitFields(itemDescriptor, "item", canEmbedBrace);
|
|
125
|
+
const helperName = nextGeneratedName("arrHelper", key);
|
|
126
|
+
const snapshot = { args: [...closureArgs], vals: [...closureVals] };
|
|
127
|
+
closureArgs.push(helperName);
|
|
128
|
+
closureVals.push(buildObjectArrayHelper(helperName, itemCode, snapshot.args, snapshot.vals, canEmbedBrace));
|
|
129
|
+
return `${helperName}(${acc}.${key})`;
|
|
130
|
+
}
|
|
131
|
+
if (isSchemaToken(field)) {
|
|
132
|
+
const desc = field[SCHEMA_DESCRIPTOR];
|
|
133
|
+
const firstNestedField = Object.values(desc)[0];
|
|
134
|
+
const canEmbed = firstNestedField != null && isDescriptorValueRequired(firstNestedField);
|
|
135
|
+
const inner = emitFields(desc, `${acc}.${key}`, canEmbed);
|
|
136
|
+
return canEmbed ? `(${inner} + '}')` : `('{' + ${inner} + '}')`;
|
|
137
|
+
}
|
|
138
|
+
switch (field._type) {
|
|
139
|
+
case "int":
|
|
140
|
+
case "float":
|
|
141
|
+
return `(+${acc}.${key})`;
|
|
142
|
+
case "bool":
|
|
143
|
+
return `(${acc}.${key} ? 'true' : 'false')`;
|
|
144
|
+
case "string":
|
|
145
|
+
case "uuid":
|
|
146
|
+
return `'"' + ${acc}.${key} + '"'`;
|
|
147
|
+
case "date":
|
|
148
|
+
return `date(${acc}.${key}, ${JSON.stringify(field._format)})`;
|
|
149
|
+
case "text":
|
|
150
|
+
return `(esc(${acc}.${key}))`;
|
|
151
|
+
case "enum": {
|
|
152
|
+
const lut = field.lut;
|
|
153
|
+
const lutName = nextGeneratedName("lut", key);
|
|
154
|
+
closureArgs.push(lutName);
|
|
155
|
+
closureVals.push(lut);
|
|
156
|
+
return `${lutName}[${acc}.${key}]`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const jsonSchema = field.jsonSchema;
|
|
160
|
+
if ("type" in jsonSchema && jsonSchema.type === "array" && "items" in jsonSchema) {
|
|
161
|
+
const itemType = jsonSchema.items.type;
|
|
162
|
+
const itemPrimitive = field._item;
|
|
163
|
+
const helperName = nextGeneratedName("arrHelper", key);
|
|
164
|
+
if (itemPrimitive?._type === "date") {
|
|
165
|
+
const itemFormat = itemPrimitive._format;
|
|
166
|
+
const snapshot = { args: [...closureArgs], vals: [...closureVals] };
|
|
167
|
+
closureArgs.push(helperName);
|
|
168
|
+
closureVals.push(
|
|
169
|
+
buildPrimitiveArrayHelper(
|
|
170
|
+
helperName,
|
|
171
|
+
`date(v, ${JSON.stringify(itemFormat)})`,
|
|
172
|
+
snapshot.args,
|
|
173
|
+
snapshot.vals
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
return `${helperName}(${acc}.${key})`;
|
|
177
|
+
}
|
|
178
|
+
if (itemType === "string") {
|
|
179
|
+
closureArgs.push(helperName);
|
|
180
|
+
closureVals.push(
|
|
181
|
+
new Function(
|
|
182
|
+
`return function ${helperName}(arr){if(arr.length===0)return'[]';return'["'+arr.join('","')+'"]';}`
|
|
183
|
+
)()
|
|
184
|
+
);
|
|
185
|
+
return `${helperName}(${acc}.${key})`;
|
|
186
|
+
}
|
|
187
|
+
let itemExpr;
|
|
188
|
+
switch (itemType) {
|
|
189
|
+
case "integer":
|
|
190
|
+
case "number":
|
|
191
|
+
itemExpr = `(+v)`;
|
|
192
|
+
break;
|
|
193
|
+
case "boolean":
|
|
194
|
+
itemExpr = `(v ? 'true' : 'false')`;
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
itemExpr = `JSON.stringify(v)`;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
closureArgs.push(helperName);
|
|
201
|
+
closureVals.push(buildPrimitiveArrayHelper(helperName, itemExpr));
|
|
202
|
+
return `${helperName}(${acc}.${key})`;
|
|
203
|
+
}
|
|
204
|
+
return `JSON.stringify(${acc}.${key})`;
|
|
205
|
+
}
|
|
206
|
+
const body = emitFields(descriptor, "o");
|
|
207
|
+
return new Function(...closureArgs, `return function serialize(o){ return '{' + ${body} + '}'; }`)(
|
|
208
|
+
...closureVals
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const DISCRIMINANT_PRIMITIVE = {
|
|
212
|
+
_type: "string",
|
|
213
|
+
_required: true,
|
|
214
|
+
jsonSchema: { type: "string" }
|
|
215
|
+
};
|
|
216
|
+
function toJsonSchema(token) {
|
|
217
|
+
return descriptorToJsonSchema(token[SCHEMA_DESCRIPTOR]);
|
|
218
|
+
}
|
|
219
|
+
function isDiscriminatedDescriptor(value) {
|
|
220
|
+
return "discriminant" in value;
|
|
221
|
+
}
|
|
222
|
+
function schemaTokenToJsonSchema(token) {
|
|
223
|
+
return descriptorToJsonSchema(token[SCHEMA_DESCRIPTOR]);
|
|
224
|
+
}
|
|
225
|
+
function descriptorValueToJsonSchema(value) {
|
|
226
|
+
if (isSchemaToken(value)) return schemaTokenToJsonSchema(value);
|
|
227
|
+
return value.jsonSchema;
|
|
228
|
+
}
|
|
229
|
+
function descriptorToJsonSchema(descriptor) {
|
|
230
|
+
if (Array.isArray(descriptor)) {
|
|
231
|
+
const first = descriptor[0];
|
|
232
|
+
if (first !== null && typeof first === "object" && "$record" in first) {
|
|
233
|
+
const valueSchema = first.$record;
|
|
234
|
+
return {
|
|
235
|
+
type: "object",
|
|
236
|
+
additionalProperties: schemaTokenToJsonSchema(valueSchema)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { type: "array", items: schemaTokenToJsonSchema(descriptor[0]) };
|
|
240
|
+
}
|
|
241
|
+
if (isDiscriminatedDescriptor(descriptor)) {
|
|
242
|
+
return {
|
|
243
|
+
anyOf: Object.entries(descriptor.branches).map(
|
|
244
|
+
([, branchDescriptor]) => descriptorToJsonSchema({
|
|
245
|
+
[descriptor.discriminant]: DISCRIMINANT_PRIMITIVE,
|
|
246
|
+
...branchDescriptor
|
|
247
|
+
})
|
|
248
|
+
)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const properties = {};
|
|
252
|
+
const required = [];
|
|
253
|
+
for (const key in descriptor) {
|
|
254
|
+
const field = descriptor[key];
|
|
255
|
+
properties[key] = descriptorValueToJsonSchema(field);
|
|
256
|
+
if (isDescriptorValueRequired(field)) required.push(key);
|
|
257
|
+
}
|
|
258
|
+
const jsonSchema = { type: "object", properties };
|
|
259
|
+
if (required.length > 0) jsonSchema.required = required;
|
|
260
|
+
return jsonSchema;
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
compileSerializer,
|
|
264
|
+
toJsonSchema
|
|
265
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type BranchShape, type DescriptorValue, type DiscriminantValues, type SchemaToken } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Schema token that also acts as an abstract base class for DTO authoring.
|
|
4
|
+
*
|
|
5
|
+
* Returned by {@link model}. Extend it to declare a named model class:
|
|
6
|
+
* ```ts
|
|
7
|
+
* class UserModel extends model({ id: uuid, name: string }) {}
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Instances are typed as `T`, so properties are accessible directly without
|
|
11
|
+
* any unwrapping. Only model tokens carry a construct signature; {@link SchemaToken}
|
|
12
|
+
* values from {@link schema} are intentionally not extendable.
|
|
13
|
+
*/
|
|
14
|
+
export type ModelTokenBuilder<T> = (abstract new (...args: never[]) => T) & SchemaToken<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Creates an extendable schema token for naming and authoring model classes.
|
|
17
|
+
*
|
|
18
|
+
* Three calling forms are supported:
|
|
19
|
+
*
|
|
20
|
+
* **From a descriptor** - define field shapes inline:
|
|
21
|
+
* ```ts
|
|
22
|
+
* class UserModel extends model({
|
|
23
|
+
* id: uuid,
|
|
24
|
+
* name: string,
|
|
25
|
+
* }) {}
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* **From an existing schema** - promote a {@link schema} token to a model token:
|
|
29
|
+
* ```ts
|
|
30
|
+
* const UserSchema = schema({ id: uuid, name: string });
|
|
31
|
+
* class UserModel extends model(UserSchema) {}
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* **Discriminated union** - branch on a string discriminant key:
|
|
35
|
+
* ```ts
|
|
36
|
+
* class PetModel extends model<Cat | Dog, "union">("kind", {
|
|
37
|
+
* cat: { lives: int },
|
|
38
|
+
* dog: { breed: string },
|
|
39
|
+
* }) {}
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* The returned token also satisfies {@link SchemaToken}`<T>`, so it can be used
|
|
43
|
+
* as a nested field value inside other schema descriptors.
|
|
44
|
+
*/
|
|
45
|
+
export declare function model<T extends object>(schema: SchemaToken<T>): ModelTokenBuilder<T>;
|
|
46
|
+
export declare function model<T extends object>(descriptor: {
|
|
47
|
+
[K in keyof T]: DescriptorValue<T[K]>;
|
|
48
|
+
}): ModelTokenBuilder<T>;
|
|
49
|
+
export declare function model<T extends object, _ extends "union", K extends keyof T & string = keyof T & string>(discriminant: K, branches: {
|
|
50
|
+
[V in DiscriminantValues<T, K>]: {
|
|
51
|
+
[F in keyof BranchShape<T, K, V>]: DescriptorValue<BranchShape<T, K, V>[F]>;
|
|
52
|
+
};
|
|
53
|
+
}): ModelTokenBuilder<T>;
|
|
54
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/schema/model.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAIvB,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAGrB;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,CAAC,QAAQ,MAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;AACtF,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,UAAU,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;AACtH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,EACtG,YAAY,EAAE,CAAC,EACf,QAAQ,EAAE;KACP,CAAC,IAAI,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;SAAG,CAAC,IAAI,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAG;CAClH,GACA,iBAAiB,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SCHEMA_BRAND, SCHEMA_DESCRIPTOR, SCHEMA_REQUIRED } from "./internal/token/symbols.js";
|
|
2
|
+
import { compileSerializer } from "./json/serializer.js";
|
|
3
|
+
import {
|
|
4
|
+
schema
|
|
5
|
+
} from "./schema.js";
|
|
6
|
+
import { COMPILED_SERIALIZER } from "./symbol.js";
|
|
7
|
+
function model(descriptorOrDiscriminant, branches) {
|
|
8
|
+
let token;
|
|
9
|
+
if (typeof descriptorOrDiscriminant === "object" && SCHEMA_BRAND in descriptorOrDiscriminant) {
|
|
10
|
+
token = descriptorOrDiscriminant;
|
|
11
|
+
} else {
|
|
12
|
+
token = schema(
|
|
13
|
+
descriptorOrDiscriminant,
|
|
14
|
+
branches
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const tokenDescriptor = token[SCHEMA_DESCRIPTOR];
|
|
18
|
+
let cachedSerializer;
|
|
19
|
+
const ModelClass = class {
|
|
20
|
+
static [SCHEMA_BRAND] = true;
|
|
21
|
+
static [SCHEMA_REQUIRED] = true;
|
|
22
|
+
static [SCHEMA_DESCRIPTOR] = tokenDescriptor;
|
|
23
|
+
static get [COMPILED_SERIALIZER]() {
|
|
24
|
+
if (cachedSerializer === void 0) cachedSerializer = compileSerializer(token);
|
|
25
|
+
return cachedSerializer;
|
|
26
|
+
}
|
|
27
|
+
static safeParse(raw) {
|
|
28
|
+
return token.safeParse(raw);
|
|
29
|
+
}
|
|
30
|
+
static optional() {
|
|
31
|
+
return token.optional();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return asModelToken(ModelClass);
|
|
35
|
+
}
|
|
36
|
+
function asModelToken(token) {
|
|
37
|
+
return token;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
model
|
|
41
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TypedPrimitive } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a primitive that splits a comma-separated string and coerces each item
|
|
4
|
+
* using the provided `primitive`. An empty or missing input produces an empty array.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const tags = array(string); // TypedPrimitive<string[]>
|
|
9
|
+
* const ids = array(int.min(1)); // TypedPrimitive<number[]>
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
declare function array<T>(primitive: TypedPrimitive<T>): TypedPrimitive<T[]>;
|
|
13
|
+
export { array };
|
|
14
|
+
//# sourceMappingURL=array.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"array.d.ts","sourceRoot":"","sources":["../../../src/schema/primitives/array.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;GASG;AACH,iBAAS,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,EAAE,CAAC,CAiBnE;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function array(primitive) {
|
|
2
|
+
const fn = (v) => {
|
|
3
|
+
if (v === void 0) throw new TypeError("array primitive received undefined");
|
|
4
|
+
if (v === "") return [];
|
|
5
|
+
if (Array.isArray(v)) {
|
|
6
|
+
return v.map((item) => primitive(item.trim()));
|
|
7
|
+
}
|
|
8
|
+
return v.split(",").map((item) => primitive(item.trim()));
|
|
9
|
+
};
|
|
10
|
+
fn._type = `array<${primitive._type}>`;
|
|
11
|
+
fn._required = primitive._required;
|
|
12
|
+
fn.jsonSchema = {
|
|
13
|
+
type: "array",
|
|
14
|
+
items: primitive.jsonSchema
|
|
15
|
+
};
|
|
16
|
+
fn._item = primitive;
|
|
17
|
+
return fn;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
array
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bool.d.ts","sourceRoot":"","sources":["../../../src/schema/primitives/bool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;GAEG;AACH,QAAA,MAAM,IAAI,EAAE,cAAc,CAAC,OAAO,CAQjC,CAAC;AAEF,OAAO,EAAE,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const bool = Object.assign(
|
|
2
|
+
(v) => {
|
|
3
|
+
const lower = v.toLowerCase();
|
|
4
|
+
if (lower === "true" || lower === "1") return true;
|
|
5
|
+
if (lower === "false" || lower === "0") return false;
|
|
6
|
+
throw new Error(`Expected boolean, got "${v}"`);
|
|
7
|
+
},
|
|
8
|
+
{ _type: "bool", _required: true, jsonSchema: { type: "boolean" } }
|
|
9
|
+
);
|
|
10
|
+
export {
|
|
11
|
+
bool
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TypedPrimitive } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format key that controls how {@link date} and {@link parseDate} interpret an input string.
|
|
4
|
+
*/
|
|
5
|
+
type DateFormatKey = "DMY" | "MDY" | "YMD" | "ISO" | "TIMESTAMP";
|
|
6
|
+
type DatePrimitive = TypedPrimitive<Date> & {
|
|
7
|
+
format(format: DateFormatKey): DatePrimitive;
|
|
8
|
+
readonly _format: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Date primitive. Parses a date string and returns a `Date` object.
|
|
12
|
+
* Defaults to ISO 8601. Chain `.format()` to select a different input format.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* date // ISO 8601 (default)
|
|
17
|
+
* date.format("DMY") // DD/MM/YYYY
|
|
18
|
+
* date.format("TIMESTAMP") // Unix seconds / ms / μs
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const date: DatePrimitive;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=date.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../../src/schema/primitives/date.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;GAEG;AACH,KAAK,aAAa,GACd,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,WAAW,CAAC;AAEhB,KAAK,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG;IAC1C,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAuHF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,IAAI,EAAE,aAAmC,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function throwBadInput(format, raw) {
|
|
2
|
+
throw new Error(`Invalid ${format} date: "${raw}"`);
|
|
3
|
+
}
|
|
4
|
+
function utcMidnight(year, month, day) {
|
|
5
|
+
return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
|
6
|
+
}
|
|
7
|
+
function normalizeYear(yy) {
|
|
8
|
+
return yy < 100 ? 2e3 + yy : yy;
|
|
9
|
+
}
|
|
10
|
+
function parseISO(raw) {
|
|
11
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?)?$/;
|
|
12
|
+
if (!ISO_RE.test(raw)) throwBadInput("ISO", raw);
|
|
13
|
+
const normalized = raw.includes("T") ? raw : raw + "T00:00:00Z";
|
|
14
|
+
const d = new Date(normalized);
|
|
15
|
+
if (isNaN(d.getTime())) throwBadInput("ISO", raw);
|
|
16
|
+
return d;
|
|
17
|
+
}
|
|
18
|
+
function parseDayMonthYear(raw, dayFirst) {
|
|
19
|
+
const SEP_RE = /^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/;
|
|
20
|
+
const COMPACT_RE = /^(\d{2})(\d{2})(\d{4})$/;
|
|
21
|
+
const m = SEP_RE.exec(raw) ?? COMPACT_RE.exec(raw);
|
|
22
|
+
const label = dayFirst ? "DMY" : "MDY";
|
|
23
|
+
if (!m) throwBadInput(label, raw);
|
|
24
|
+
const a = parseInt(m[1], 10);
|
|
25
|
+
const b = parseInt(m[2], 10);
|
|
26
|
+
const year = normalizeYear(parseInt(m[3], 10));
|
|
27
|
+
const [day, month] = dayFirst ? [a, b - 1] : [b, a - 1];
|
|
28
|
+
const d = utcMidnight(year, month, day);
|
|
29
|
+
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month || d.getUTCDate() !== day) {
|
|
30
|
+
throwBadInput(label, raw);
|
|
31
|
+
}
|
|
32
|
+
if (isNaN(d.getTime())) throwBadInput(label, raw);
|
|
33
|
+
return d;
|
|
34
|
+
}
|
|
35
|
+
function parseYMD(raw) {
|
|
36
|
+
const SEP_RE = /^(\d{4})[\/\-.](\d{1,2})[\/\-.](\d{1,2})$/;
|
|
37
|
+
const COMPACT_RE = /^(\d{4})(\d{2})(\d{2})$/;
|
|
38
|
+
const m = SEP_RE.exec(raw) ?? COMPACT_RE.exec(raw);
|
|
39
|
+
if (!m) throwBadInput("YMD", raw);
|
|
40
|
+
const year = parseInt(m[1], 10);
|
|
41
|
+
const month = parseInt(m[2], 10) - 1;
|
|
42
|
+
const day = parseInt(m[3], 10);
|
|
43
|
+
const d = utcMidnight(year, month, day);
|
|
44
|
+
if (isNaN(d.getTime())) throwBadInput("YMD", raw);
|
|
45
|
+
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month || d.getUTCDate() !== day) {
|
|
46
|
+
throwBadInput("YMD", raw);
|
|
47
|
+
}
|
|
48
|
+
return d;
|
|
49
|
+
}
|
|
50
|
+
function parseTimestamp(raw) {
|
|
51
|
+
const ts = raw.trim();
|
|
52
|
+
if (!/^\d{10}$|^\d{13}$|^\d{16}$/.test(ts)) throwBadInput("TIMESTAMP", raw);
|
|
53
|
+
const n = parseInt(ts, 10);
|
|
54
|
+
const ms = ts.length === 10 ? n * 1e3 : ts.length === 13 ? n : Math.floor(n / 1e3);
|
|
55
|
+
const d = new Date(ms);
|
|
56
|
+
if (isNaN(d.getTime())) throwBadInput("TIMESTAMP", raw);
|
|
57
|
+
return d;
|
|
58
|
+
}
|
|
59
|
+
const DATE_JSON_SCHEMA = {
|
|
60
|
+
ISO: { type: "string", format: "date-time" },
|
|
61
|
+
YMD: { type: "string", format: "date" },
|
|
62
|
+
DMY: { type: "string" },
|
|
63
|
+
MDY: { type: "string" },
|
|
64
|
+
TIMESTAMP: { type: "string" }
|
|
65
|
+
};
|
|
66
|
+
function parseDate(raw, format = "ISO") {
|
|
67
|
+
switch (format) {
|
|
68
|
+
case "ISO":
|
|
69
|
+
return parseISO(raw);
|
|
70
|
+
case "DMY":
|
|
71
|
+
return parseDayMonthYear(raw, true);
|
|
72
|
+
case "MDY":
|
|
73
|
+
return parseDayMonthYear(raw, false);
|
|
74
|
+
case "YMD":
|
|
75
|
+
return parseYMD(raw);
|
|
76
|
+
case "TIMESTAMP":
|
|
77
|
+
return parseTimestamp(raw);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function makeDatePrimitive(format = "ISO") {
|
|
81
|
+
const fn = (raw) => parseDate(raw, format);
|
|
82
|
+
fn._type = "date";
|
|
83
|
+
fn._required = true;
|
|
84
|
+
fn.jsonSchema = DATE_JSON_SCHEMA[format];
|
|
85
|
+
fn._format = format;
|
|
86
|
+
fn.format = (nextFormat) => makeDatePrimitive(nextFormat);
|
|
87
|
+
return fn;
|
|
88
|
+
}
|
|
89
|
+
const date = makeDatePrimitive();
|
|
90
|
+
export {
|
|
91
|
+
date
|
|
92
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TypedPrimitive } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* String enum primitive carrying a lookup table of pre-quoted JSON literals for each value.
|
|
4
|
+
*/
|
|
5
|
+
type EnumPrimitive<T extends readonly string[]> = TypedPrimitive<T[number]> & {
|
|
6
|
+
/** Lookup of value -> pre-quoted JSON literal, built once at creation for O(1) serializer lookups. */
|
|
7
|
+
lut: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a primitive that accepts any string value present in the provided tuple.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const role = enums(["admin", "user", "guest"]);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare function enums<const T extends readonly string[]>(values: T): EnumPrimitive<T>;
|
|
18
|
+
export { enums };
|
|
19
|
+
//# sourceMappingURL=enum.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enum.d.ts","sourceRoot":"","sources":["../../../src/schema/primitives/enum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;GAEG;AACH,KAAK,aAAa,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG;IAC5E,sGAAsG;IACtG,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B,CAAC;AAEF;;;;;;;GAOG;AACH,iBAAS,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAe7E;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function enums(values) {
|
|
2
|
+
const fn = (v) => {
|
|
3
|
+
if (!values.includes(v)) {
|
|
4
|
+
throw new Error(`Expected one of [${values.join(", ")}], got "${v}"`);
|
|
5
|
+
}
|
|
6
|
+
return v;
|
|
7
|
+
};
|
|
8
|
+
fn._type = "enum";
|
|
9
|
+
fn._required = true;
|
|
10
|
+
fn.jsonSchema = {
|
|
11
|
+
type: "string",
|
|
12
|
+
enum: values
|
|
13
|
+
};
|
|
14
|
+
fn.lut = Object.fromEntries(values.map((v) => [v, `"${v}"`]));
|
|
15
|
+
return fn;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
enums
|
|
19
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TypedPrimitive } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Float primitive with optional chainable range constraints.
|
|
4
|
+
* Each builder method returns a new independent primitive so the original is never mutated.
|
|
5
|
+
*/
|
|
6
|
+
type FloatPrimitive = TypedPrimitive<number> & {
|
|
7
|
+
/** Returns a new primitive that rejects values below `n`. */
|
|
8
|
+
min(n: number): FloatPrimitive;
|
|
9
|
+
/** Returns a new primitive that rejects values above `n`. */
|
|
10
|
+
max(n: number): FloatPrimitive;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Floating-point number primitive with optional chainable range constraints.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* float // any number
|
|
18
|
+
* float.min(0).max(1) // probability range
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const float: FloatPrimitive;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=float.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"float.d.ts","sourceRoot":"","sources":["../../../src/schema/primitives/float.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,KAAK,cAAc,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG;IAC7C,6DAA6D;IAC7D,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC;IAC/B,6DAA6D;IAC7D,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC;CAChC,CAAC;AAkCF;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,EAAE,cAA4B,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function makeFloat(config = {}) {
|
|
2
|
+
const fn = (v) => {
|
|
3
|
+
const raw = v.trim();
|
|
4
|
+
if (!/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw)) {
|
|
5
|
+
throw new Error(`Expected float, got "${v}"`);
|
|
6
|
+
}
|
|
7
|
+
const parsed = Number(raw);
|
|
8
|
+
if (!Number.isFinite(parsed)) {
|
|
9
|
+
throw new Error(`Expected finite float, got "${v}"`);
|
|
10
|
+
}
|
|
11
|
+
if (config.min !== void 0 && parsed < config.min) {
|
|
12
|
+
throw new Error(`Value too small: minimum is ${config.min}, got ${parsed}`);
|
|
13
|
+
}
|
|
14
|
+
if (config.max !== void 0 && parsed > config.max) {
|
|
15
|
+
throw new Error(`Value too large: maximum is ${config.max}, got ${parsed}`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
};
|
|
19
|
+
fn._type = "float";
|
|
20
|
+
fn._required = true;
|
|
21
|
+
fn.jsonSchema = {
|
|
22
|
+
type: "number",
|
|
23
|
+
...config.min !== void 0 ? { minimum: config.min } : {},
|
|
24
|
+
...config.max !== void 0 ? { maximum: config.max } : {}
|
|
25
|
+
};
|
|
26
|
+
fn.min = (n) => makeFloat({ ...config, min: n });
|
|
27
|
+
fn.max = (n) => makeFloat({ ...config, max: n });
|
|
28
|
+
return fn;
|
|
29
|
+
}
|
|
30
|
+
const float = makeFloat();
|
|
31
|
+
export {
|
|
32
|
+
float
|
|
33
|
+
};
|