@dxos/effect 0.7.0 → 0.7.1-staging.7f6f91c
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/dist/lib/browser/index.mjs +115 -14
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +123 -17
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +115 -14
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/ast.d.ts +28 -2
- package/dist/types/src/ast.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/ast.test.ts +71 -9
- package/src/ast.ts +140 -23
package/src/ast.test.ts
CHANGED
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
findNode,
|
|
13
13
|
findProperty,
|
|
14
14
|
getAnnotation,
|
|
15
|
+
getDiscriminatingProps,
|
|
16
|
+
getDiscriminatedType,
|
|
15
17
|
getSimpleType,
|
|
18
|
+
isOption,
|
|
16
19
|
isSimpleType,
|
|
17
20
|
visit,
|
|
18
21
|
type JsonPath,
|
|
@@ -89,20 +92,39 @@ describe('AST', () => {
|
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
test('findAnnotation', ({ expect }) => {
|
|
92
|
-
const
|
|
95
|
+
const TestSchema = S.NonEmptyString.pipe(S.pattern(/^\d{5}$/)).annotations({
|
|
93
96
|
[AST.TitleAnnotationId]: 'original title',
|
|
94
97
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
const ContactSchema = S.Struct({
|
|
100
|
+
p1: TestSchema.annotations({ [AST.TitleAnnotationId]: 'new title' }),
|
|
101
|
+
p2: TestSchema.annotations({ [AST.TitleAnnotationId]: 'new title' }).pipe(S.optional),
|
|
102
|
+
p3: S.optional(TestSchema.annotations({ [AST.TitleAnnotationId]: 'new title' })),
|
|
99
103
|
});
|
|
100
104
|
|
|
101
|
-
{
|
|
102
|
-
const prop = findProperty(
|
|
105
|
+
for (const p of ['p1', 'p2', 'p3']) {
|
|
106
|
+
const prop = findProperty(ContactSchema, p as JsonPath);
|
|
103
107
|
invariant(prop);
|
|
104
108
|
const value = findAnnotation(prop, AST.TitleAnnotationId);
|
|
105
|
-
expect(value).to.eq('new title');
|
|
109
|
+
expect(value, `invalid title for ${p}`).to.eq('new title');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('findAnnotation skips defaults', ({ expect }) => {
|
|
114
|
+
const annotation = findAnnotation(
|
|
115
|
+
S.String.annotations({ [AST.TitleAnnotationId]: 'test' }).ast,
|
|
116
|
+
AST.TitleAnnotationId,
|
|
117
|
+
{ noDefault: true },
|
|
118
|
+
);
|
|
119
|
+
expect(annotation).to.eq('test');
|
|
120
|
+
|
|
121
|
+
const annotationIds = [AST.TitleAnnotationId, AST.DescriptionAnnotationId];
|
|
122
|
+
const schemas = [S.Object, S.String, S.Number, S.Boolean];
|
|
123
|
+
for (const schema of schemas) {
|
|
124
|
+
for (const annotationId of annotationIds) {
|
|
125
|
+
const annotation = findAnnotation(schema.ast, annotationId, { noDefault: true });
|
|
126
|
+
expect(annotation, schema.ast._tag).to.eq(undefined);
|
|
127
|
+
}
|
|
106
128
|
}
|
|
107
129
|
});
|
|
108
130
|
|
|
@@ -119,6 +141,46 @@ describe('AST', () => {
|
|
|
119
141
|
|
|
120
142
|
const props: string[] = [];
|
|
121
143
|
visit(TestSchema.ast, (_, path) => props.push(path.join('.')));
|
|
122
|
-
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('discriminated unions', ({ expect }) => {
|
|
147
|
+
const TestUnionSchema = S.Union(
|
|
148
|
+
S.Struct({ kind: S.Literal('a'), label: S.String }),
|
|
149
|
+
S.Struct({ kind: S.Literal('b'), count: S.Number, active: S.Boolean }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
type TestUnionType = S.Schema.Type<typeof TestUnionSchema>;
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
expect(isOption(TestUnionSchema.ast)).to.be.false;
|
|
156
|
+
expect(getDiscriminatingProps(TestUnionSchema.ast)).to.deep.eq(['kind']);
|
|
157
|
+
|
|
158
|
+
const node = findNode(TestUnionSchema.ast, isSimpleType);
|
|
159
|
+
expect(node).to.eq(TestUnionSchema.ast);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
invariant(AST.isUnion(TestUnionSchema.ast));
|
|
164
|
+
const [a, b] = TestUnionSchema.ast.types;
|
|
165
|
+
|
|
166
|
+
const obj1: TestUnionType = {
|
|
167
|
+
kind: 'a',
|
|
168
|
+
label: 'test',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const obj2: TestUnionType = {
|
|
172
|
+
kind: 'b',
|
|
173
|
+
count: 100,
|
|
174
|
+
active: true,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
expect(getDiscriminatedType(TestUnionSchema.ast, obj1)?.toJSON()).to.deep.eq(a.toJSON());
|
|
178
|
+
expect(getDiscriminatedType(TestUnionSchema.ast, obj2)?.toJSON()).to.deep.eq(b.toJSON());
|
|
179
|
+
expect(getDiscriminatedType(TestUnionSchema.ast)?.toJSON()).to.deep.eq(
|
|
180
|
+
S.Struct({
|
|
181
|
+
kind: S.Literal('a', 'b'),
|
|
182
|
+
}).ast.toJSON(),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
123
185
|
});
|
|
124
186
|
});
|
package/src/ast.ts
CHANGED
|
@@ -6,20 +6,25 @@ import { AST, Schema as S } from '@effect/schema';
|
|
|
6
6
|
import { Option, pipe } from 'effect';
|
|
7
7
|
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { nonNullable } from '@dxos/util';
|
|
9
10
|
|
|
10
11
|
//
|
|
11
12
|
// Refs
|
|
12
|
-
// https://effect.website/docs/
|
|
13
|
+
// https://effect.website/docs/schema/introduction
|
|
13
14
|
// https://www.npmjs.com/package/@effect/schema
|
|
14
15
|
// https://effect-ts.github.io/effect/schema/AST.ts.html
|
|
15
16
|
//
|
|
16
17
|
|
|
17
18
|
export type SimpleType = 'object' | 'string' | 'number' | 'boolean' | 'enum' | 'literal';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Get the base type; e.g., traverse through refinements.
|
|
22
|
+
*/
|
|
19
23
|
export const getSimpleType = (node: AST.AST): SimpleType | undefined => {
|
|
20
|
-
if (AST.isObjectKeyword(node)) {
|
|
24
|
+
if (AST.isObjectKeyword(node) || AST.isTypeLiteral(node) || isDiscriminatedUnion(node)) {
|
|
21
25
|
return 'object';
|
|
22
26
|
}
|
|
27
|
+
|
|
23
28
|
if (AST.isStringKeyword(node)) {
|
|
24
29
|
return 'string';
|
|
25
30
|
}
|
|
@@ -29,15 +34,17 @@ export const getSimpleType = (node: AST.AST): SimpleType | undefined => {
|
|
|
29
34
|
if (AST.isBooleanKeyword(node)) {
|
|
30
35
|
return 'boolean';
|
|
31
36
|
}
|
|
37
|
+
|
|
32
38
|
if (AST.isEnums(node)) {
|
|
33
39
|
return 'enum';
|
|
34
40
|
}
|
|
41
|
+
|
|
35
42
|
if (AST.isLiteral(node)) {
|
|
36
43
|
return 'literal';
|
|
37
44
|
}
|
|
38
45
|
};
|
|
39
46
|
|
|
40
|
-
export const isSimpleType = (node: AST.AST) => !!getSimpleType(node);
|
|
47
|
+
export const isSimpleType = (node: AST.AST): boolean => !!getSimpleType(node);
|
|
41
48
|
|
|
42
49
|
//
|
|
43
50
|
// Branded types
|
|
@@ -136,7 +143,7 @@ const visitNode = (
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
// Branching union.
|
|
146
|
+
// Branching union (e.g., optional, discriminated unions).
|
|
140
147
|
else if (AST.isUnion(node)) {
|
|
141
148
|
for (const type of node.types) {
|
|
142
149
|
const result = visitNode(type, test, visitor, path, depth);
|
|
@@ -154,13 +161,13 @@ const visitNode = (
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
157
|
-
// TODO(burdon):
|
|
164
|
+
// TODO(burdon): Transforms?
|
|
158
165
|
};
|
|
159
166
|
|
|
160
167
|
/**
|
|
161
168
|
* Recursively descend into AST to find first node that passes the test.
|
|
162
169
|
*/
|
|
163
|
-
// TODO(burdon):
|
|
170
|
+
// TODO(burdon): Rewrite using visitNode?
|
|
164
171
|
export const findNode = (node: AST.AST, test: (node: AST.AST) => boolean): AST.AST | undefined => {
|
|
165
172
|
if (test(node)) {
|
|
166
173
|
return node;
|
|
@@ -176,7 +183,7 @@ export const findNode = (node: AST.AST, test: (node: AST.AST) => boolean): AST.A
|
|
|
176
183
|
}
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
//
|
|
186
|
+
// Tuple.
|
|
180
187
|
else if (AST.isTupleType(node)) {
|
|
181
188
|
for (const [_, element] of node.elements.entries()) {
|
|
182
189
|
const child = findNode(element.type, test);
|
|
@@ -186,12 +193,14 @@ export const findNode = (node: AST.AST, test: (node: AST.AST) => boolean): AST.A
|
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
// Branching union.
|
|
196
|
+
// Branching union (e.g., optional, discriminated unions).
|
|
190
197
|
else if (AST.isUnion(node)) {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
if (isOption(node)) {
|
|
199
|
+
for (const type of node.types) {
|
|
200
|
+
const child = findNode(type, test);
|
|
201
|
+
if (child) {
|
|
202
|
+
return child;
|
|
203
|
+
}
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
}
|
|
@@ -200,8 +209,6 @@ export const findNode = (node: AST.AST, test: (node: AST.AST) => boolean): AST.A
|
|
|
200
209
|
else if (AST.isRefinement(node)) {
|
|
201
210
|
return findNode(node.from, test);
|
|
202
211
|
}
|
|
203
|
-
|
|
204
|
-
return undefined;
|
|
205
212
|
};
|
|
206
213
|
|
|
207
214
|
/**
|
|
@@ -221,33 +228,143 @@ export const findProperty = (schema: S.Schema<any>, path: JsonPath | JsonProp):
|
|
|
221
228
|
}
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
|
-
|
|
225
|
-
return undefined;
|
|
226
231
|
};
|
|
227
232
|
|
|
228
233
|
return getProp(schema.ast, path.split('.') as JsonProp[]);
|
|
229
234
|
};
|
|
230
235
|
|
|
236
|
+
//
|
|
237
|
+
// Annotations
|
|
238
|
+
//
|
|
239
|
+
|
|
240
|
+
const defaultAnnotations: Record<string, AST.Annotated> = {
|
|
241
|
+
['ObjectKeyword' as const]: AST.objectKeyword,
|
|
242
|
+
['StringKeyword' as const]: AST.stringKeyword,
|
|
243
|
+
['NumberKeyword' as const]: AST.numberKeyword,
|
|
244
|
+
['BooleanKeyword' as const]: AST.booleanKeyword,
|
|
245
|
+
};
|
|
246
|
+
|
|
231
247
|
/**
|
|
232
|
-
* Recursively descend into AST to find first matching annotations
|
|
248
|
+
* Recursively descend into AST to find first matching annotations.
|
|
249
|
+
* Optionally skips default annotations for basic types (e.g., 'a string').
|
|
233
250
|
*/
|
|
234
|
-
export const findAnnotation = <T>(
|
|
251
|
+
export const findAnnotation = <T>(
|
|
252
|
+
node: AST.AST,
|
|
253
|
+
annotationId: symbol,
|
|
254
|
+
options?: { noDefault: boolean },
|
|
255
|
+
): T | undefined => {
|
|
235
256
|
const getAnnotationById = getAnnotation(annotationId);
|
|
257
|
+
|
|
236
258
|
const getBaseAnnotation = (node: AST.AST): T | undefined => {
|
|
237
259
|
const value = getAnnotationById(node);
|
|
238
260
|
if (value !== undefined) {
|
|
261
|
+
if (options?.noDefault && value === defaultAnnotations[node._tag]?.annotations[annotationId]) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
239
265
|
return value as T;
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
if (AST.isUnion(node)) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (value !== undefined) {
|
|
246
|
-
return value as T;
|
|
247
|
-
}
|
|
269
|
+
if (isOption(node)) {
|
|
270
|
+
return getAnnotationById(node.types[0]) as T;
|
|
248
271
|
}
|
|
249
272
|
}
|
|
250
273
|
};
|
|
251
274
|
|
|
252
275
|
return getBaseAnnotation(node);
|
|
253
276
|
};
|
|
277
|
+
|
|
278
|
+
//
|
|
279
|
+
// Unions
|
|
280
|
+
//
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Effect S.optional creates a union type with undefined as the second type.
|
|
284
|
+
*/
|
|
285
|
+
export const isOption = (node: AST.AST): boolean => {
|
|
286
|
+
return AST.isUnion(node) && node.types.length === 2 && AST.isUndefinedKeyword(node.types[1]);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Determines if the node is a union of literal types.
|
|
291
|
+
*/
|
|
292
|
+
export const isLiteralUnion = (node: AST.AST): boolean => {
|
|
293
|
+
return AST.isUnion(node) && node.types.every(AST.isLiteral);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Determines if the node is a discriminated union.
|
|
298
|
+
*/
|
|
299
|
+
export const isDiscriminatedUnion = (node: AST.AST): boolean => {
|
|
300
|
+
return AST.isUnion(node) && !!getDiscriminatingProps(node)?.length;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get the discriminating properties for the given union type.
|
|
305
|
+
*/
|
|
306
|
+
export const getDiscriminatingProps = (node: AST.AST): string[] | undefined => {
|
|
307
|
+
invariant(AST.isUnion(node));
|
|
308
|
+
if (isOption(node)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get common literals across all types.
|
|
313
|
+
return node.types.reduce<string[]>((shared, type) => {
|
|
314
|
+
const props = AST.getPropertySignatures(type)
|
|
315
|
+
// TODO(burdon): Should check each literal is unique.
|
|
316
|
+
.filter((p) => AST.isLiteral(p.type))
|
|
317
|
+
.map((p) => p.name.toString());
|
|
318
|
+
|
|
319
|
+
// Return common literals.
|
|
320
|
+
return shared.length === 0 ? props : shared.filter((prop) => props.includes(prop));
|
|
321
|
+
}, []);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the discriminated type for the given value.
|
|
326
|
+
*/
|
|
327
|
+
export const getDiscriminatedType = (node: AST.AST, value: Record<string, any> = {}): AST.AST | undefined => {
|
|
328
|
+
invariant(AST.isUnion(node));
|
|
329
|
+
invariant(value);
|
|
330
|
+
const props = getDiscriminatingProps(node);
|
|
331
|
+
if (!props?.length) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Match provided values.
|
|
336
|
+
for (const type of node.types) {
|
|
337
|
+
const match = AST.getPropertySignatures(type)
|
|
338
|
+
.filter((prop) => props?.includes(prop.name.toString()))
|
|
339
|
+
.every((prop) => {
|
|
340
|
+
invariant(AST.isLiteral(prop.type));
|
|
341
|
+
return prop.type.literal === value[prop.name.toString()];
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (match) {
|
|
345
|
+
return type;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Create union of discriminating properties.
|
|
350
|
+
// NOTE: This may not work with non-overlapping variants.
|
|
351
|
+
// TODO(burdon): Iterate through props and knock-out variants that don't match.
|
|
352
|
+
const fields = Object.fromEntries(
|
|
353
|
+
props
|
|
354
|
+
.map((prop) => {
|
|
355
|
+
const literals = node.types
|
|
356
|
+
.map((type) => {
|
|
357
|
+
const literal = AST.getPropertySignatures(type).find((p) => p.name.toString() === prop)!;
|
|
358
|
+
invariant(AST.isLiteral(literal.type));
|
|
359
|
+
return literal.type.literal;
|
|
360
|
+
})
|
|
361
|
+
.filter(nonNullable);
|
|
362
|
+
|
|
363
|
+
return literals.length ? [prop, S.Literal(...literals)] : undefined;
|
|
364
|
+
})
|
|
365
|
+
.filter(nonNullable),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const schema = S.Struct(fields);
|
|
369
|
+
return schema.ast;
|
|
370
|
+
};
|