Skip to content

Commit 7cbde4f

Browse files
authored
Merge pull request #474 from streamich/json-type-map
JSON Type "map" type
2 parents e8421d6 + 1102d58 commit 7cbde4f

File tree

15 files changed

+399
-13
lines changed

15 files changed

+399
-13
lines changed

src/json-schema/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface JsonSchemaObject extends JsonSchemaGenericKeywords {
3636
};
3737
required?: string[];
3838
additionalProperties?: boolean | JsonSchemaNode;
39+
patternProperties?: {
40+
[key: string]: JsonSchemaNode;
41+
};
3942
const?: object;
4043
}
4144

src/json-type/codegen/binary/__tests__/testBinaryCodegen.ts

+34
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,40 @@ export const testBinaryCodegen = (transcode: (system: TypeSystem, type: Type, va
369369
});
370370
});
371371

372+
describe('"map" type', () => {
373+
test('can encode empty map', () => {
374+
const system = new TypeSystem();
375+
const t = system.t;
376+
const type = t.map;
377+
const value: {} = {};
378+
expect(transcode(system, type, value)).toStrictEqual(value);
379+
});
380+
381+
test('can encode empty map with one key', () => {
382+
const system = new TypeSystem();
383+
const t = system.t;
384+
const type = t.map;
385+
const value: {} = {a: 'asdf'};
386+
expect(transcode(system, type, value)).toStrictEqual(value);
387+
});
388+
389+
test('can encode typed map with two keys', () => {
390+
const system = new TypeSystem();
391+
const t = system.t;
392+
const type = t.Map(t.bool);
393+
const value: {} = {x: true, y: false};
394+
expect(transcode(system, type, value)).toStrictEqual(value);
395+
});
396+
397+
test('can encode nested maps', () => {
398+
const system = new TypeSystem();
399+
const t = system.t;
400+
const type = t.Map(t.Map(t.bool));
401+
const value: {} = {a: {x: true, y: false}};
402+
expect(transcode(system, type, value)).toStrictEqual(value);
403+
});
404+
});
405+
372406
describe('"ref" type', () => {
373407
test('can encode a simple reference', () => {
374408
const system = new TypeSystem();

src/json-type/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts

+32
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,38 @@ describe('object', () => {
155155
});
156156
});
157157

158+
describe('map', () => {
159+
test('empty', () => {
160+
const system = new TypeSystem();
161+
const type = system.t.map;
162+
const estimator = type.compileCapacityEstimator({});
163+
expect(estimator(123)).toBe(maxEncodingCapacity({}));
164+
});
165+
166+
test('with one field', () => {
167+
const system = new TypeSystem();
168+
const type = system.t.Map(system.t.bool);
169+
const estimator = type.compileCapacityEstimator({});
170+
expect(estimator({foo: true})).toBe(maxEncodingCapacity({foo: true}));
171+
});
172+
173+
test('three number fields', () => {
174+
const system = new TypeSystem();
175+
const type = system.t.Map(system.t.num);
176+
const estimator = type.compileCapacityEstimator({});
177+
const data = {foo: 1, bar: 2, baz: 3};
178+
expect(estimator(data)).toBe(maxEncodingCapacity(data));
179+
});
180+
181+
test('nested maps', () => {
182+
const system = new TypeSystem();
183+
const type = system.t.Map(system.t.Map(system.t.str));
184+
const estimator = type.compileCapacityEstimator({});
185+
const data = {foo: {bar: 'baz'}, baz: {bar: 'foo'}};
186+
expect(estimator(data)).toBe(maxEncodingCapacity(data));
187+
});
188+
});
189+
158190
describe('ref', () => {
159191
test('two hops', () => {
160192
const system = new TypeSystem();

src/json-type/codegen/json/__tests__/json.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,28 @@ describe('"obj" type', () => {
121121
});
122122
});
123123

124+
describe('"map" type', () => {
125+
test('serializes a map', () => {
126+
const type = s.Map(s.num);
127+
exec(type, {a: 1, b: 2, c: 3});
128+
});
129+
130+
test('serializes empty map', () => {
131+
const type = s.Map(s.num);
132+
exec(type, {});
133+
});
134+
135+
test('serializes a map with a single key', () => {
136+
const type = s.Map(s.num);
137+
exec(type, {'0': 0});
138+
});
139+
140+
test('serializes a map in a map', () => {
141+
const type = s.Map(s.Map(s.bool));
142+
exec(type, {a: {b: true}});
143+
});
144+
});
145+
124146
describe('general', () => {
125147
test('serializes according to schema a POJO object', () => {
126148
const type = s.Object({

src/json-type/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum ValidationError {
66
ARR,
77
TUP,
88
OBJ,
9+
MAP,
910
KEY,
1011
KEYS,
1112
BIN,
@@ -32,6 +33,7 @@ export const ValidationErrorMessage = {
3233
[ValidationError.ARR]: 'Not an array.',
3334
[ValidationError.TUP]: 'Not a tuple.',
3435
[ValidationError.OBJ]: 'Not an object.',
36+
[ValidationError.MAP]: 'Not a map.',
3537
[ValidationError.KEY]: 'Missing key.',
3638
[ValidationError.KEYS]: 'Too many or missing object keys.',
3739
[ValidationError.BIN]: 'Not a binary.',

src/json-type/schema/__tests__/type.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ test('can generate any type', () => {
1414
s.prop('address', address),
1515
s.prop('timeCreated', s.Number()),
1616
s.prop('tags', s.Array(s.Or(s.Number(), s.String()))),
17-
s.prop('elements', s.Map(s.str))
17+
s.prop('elements', s.Map(s.str)),
1818
);
1919

2020
expect(userType).toMatchObject({

src/json-type/type/TypeBuilder.ts

+12
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export class TypeBuilder {
4646
return this.Object();
4747
}
4848

49+
get map() {
50+
return this.Map(this.any);
51+
}
52+
4953
get fn() {
5054
return this.Function(this.any, this.any);
5155
}
@@ -129,6 +133,12 @@ export class TypeBuilder {
129133
return field;
130134
}
131135

136+
public Map<T extends Type>(type: T, options?: schema.Optional<schema.MapSchema>) {
137+
const map = new classes.MapType<T>(type, options);
138+
map.system = this.system;
139+
return map;
140+
}
141+
132142
public Or<F extends Type[]>(...types: F) {
133143
const or = new classes.OrType<F>(types);
134144
or.system = this.system;
@@ -176,6 +186,8 @@ export class TypeBuilder {
176186
),
177187
).options(node);
178188
}
189+
case 'map':
190+
return this.Map(this.import(node.type), node);
179191
case 'const':
180192
return this.Const(node.value).options(node);
181193
case 'or':

src/json-type/type/__tests__/fixtures.ts

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const everyType = t.Object(
1717
t.prop('emptyArray', t.arr.options({max: 0})),
1818
t.prop('oneItemArray', t.arr.options({min: 1, max: 1})),
1919
t.prop('objWithArray', t.Object(t.propOpt('arr', t.arr), t.propOpt('arr2', t.arr))),
20+
t.prop('emptyMap', t.map),
21+
t.prop('mapWithOneNumField', t.Map(t.num)),
22+
t.prop('mapOfStr', t.Map(t.str)),
2023
);
2124

2225
export const everyTypeValue: TypeOf<SchemaOf<typeof everyType>> = {
@@ -37,4 +40,12 @@ export const everyTypeValue: TypeOf<SchemaOf<typeof everyType>> = {
3740
objWithArray: {
3841
arr: [1, 2, 3],
3942
},
43+
emptyMap: {},
44+
mapWithOneNumField: {
45+
a: 1,
46+
},
47+
mapOfStr: {
48+
a: 'a',
49+
b: 'b',
50+
},
4051
};

src/json-type/type/__tests__/getJsonSchema.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ test('can print a type', () => {
3636
)
3737
.options({format: 'cbor'}),
3838
),
39+
t.prop('map', t.Map(t.str)),
3940
)
4041
.options({unknownFields: true});
4142
// console.log(JSON.stringify(type.toJsonSchema(), null, 2));
@@ -83,6 +84,14 @@ test('can print a type', () => {
8384
"id": {
8485
"type": "string",
8586
},
87+
"map": {
88+
"patternProperties": {
89+
".*": {
90+
"type": "string",
91+
},
92+
},
93+
"type": "object",
94+
},
8695
"numberProperty": {
8796
"exclusiveMinimum": 3.14,
8897
"type": "number",
@@ -178,6 +187,7 @@ test('can print a type', () => {
178187
"unionProperty",
179188
"operation",
180189
"binaryOperation",
190+
"map",
181191
],
182192
"type": "object",
183193
}

src/json-type/type/__tests__/random.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ test('generates random JSON', () => {
1313
t.prop('name', t.str),
1414
t.prop('tags', t.Array(t.str)),
1515
t.propOpt('scores', t.Array(t.num)),
16+
t.prop('refs', t.Map(t.str)),
1617
);
1718
const json = type.random();
1819
expect(json).toMatchInlineSnapshot(`
1920
{
2021
"id": "",
2122
"name": "1",
23+
"refs": {
24+
"259<@CGK": "UY\\\`c",
25+
";>BEILPT": "^beimp",
26+
"HKORVY]\`": "korvy}#",
27+
"LOSWZ^ae": "pswz #'*",
28+
"_cfjmqtx": "гггггг诶诶诶诶",
29+
"nquy|"%)": "4",
30+
"w{ $'+/2": "=@",
31+
"гггг诶诶诶诶": "MQTX",
32+
},
2233
"tags": [
2334
"@CG",
2435
"QUY\\\`",

src/json-type/type/__tests__/toString.spec.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test('can print a type', () => {
3535
)
3636
.options({format: 'cbor'}),
3737
),
38+
t.prop('map', t.Map(t.num)),
3839
)
3940
.options({unknownFields: true});
4041
// console.log(type + '');
@@ -83,11 +84,14 @@ test('can print a type', () => {
8384
│ │ └─ str
8485
│ └─ "value":
8586
│ └─ any
86-
└─ "binaryOperation":
87-
└─ bin { format = "cbor" }
88-
└─ tup { description = "Should always have 3 elements" }
89-
├─ const { description = "7 is the magic number" } → 7
90-
├─ str
91-
└─ any"
87+
├─ "binaryOperation":
88+
│ └─ bin { format = "cbor" }
89+
│ └─ tup { description = "Should always have 3 elements" }
90+
│ ├─ const { description = "7 is the magic number" } → 7
91+
│ ├─ str
92+
│ └─ any
93+
└─ "map":
94+
└─ map
95+
└─ num"
9296
`);
9397
});

src/json-type/type/__tests__/toTypeScriptAst.spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,31 @@ describe('obj', () => {
207207
});
208208
});
209209

210+
describe('map', () => {
211+
test('can emit tuple AST', () => {
212+
const system = new TypeSystem();
213+
const {t} = system;
214+
const type = system.t.Map(t.num).options({
215+
title: 'title',
216+
description: 'description',
217+
});
218+
expect(type.toTypeScriptAst()).toMatchInlineSnapshot(`
219+
{
220+
"node": "TypeReference",
221+
"typeArguments": [
222+
{
223+
"node": "StringKeyword",
224+
},
225+
{
226+
"node": "NumberKeyword",
227+
},
228+
],
229+
"typeName": "Record",
230+
}
231+
`);
232+
});
233+
});
234+
210235
describe('ref', () => {
211236
test('can emit reference AST', () => {
212237
const system = new TypeSystem();

src/json-type/type/__tests__/validateTestSuite.ts

+31
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,37 @@ export const validateTestSuite = (validate: (type: Type, value: unknown) => void
417417
});
418418
});
419419

420+
describe('map', () => {
421+
test('accepts empty object as input', () => {
422+
const type = t.map;
423+
validate(type, {});
424+
});
425+
426+
test('does not accept empty array as input', () => {
427+
const type = t.map;
428+
expect(() => validate(type, [])).toThrow();
429+
});
430+
431+
test('validates "any" map', () => {
432+
const type = t.map;
433+
validate(type, {
434+
a: 'str',
435+
b: 123,
436+
c: true,
437+
});
438+
});
439+
440+
test('validates contained type', () => {
441+
const type = t.Map(t.str);
442+
validate(type, {});
443+
validate(type, {a: ''});
444+
validate(type, {b: 'asdf'});
445+
expect(() => validate(type, {c: 123})).toThrowErrorMatchingInlineSnapshot(`"STR"`);
446+
expect(() => validate(type, {c: false})).toThrowErrorMatchingInlineSnapshot(`"STR"`);
447+
expect(() => validate(type, [])).toThrowErrorMatchingInlineSnapshot(`"MAP"`);
448+
});
449+
});
450+
420451
describe('ref', () => {
421452
test('validates after recursively resolving', () => {
422453
const t = system.t;

0 commit comments

Comments
 (0)