Skip to content

Commit

Permalink
Merge pull request #474 from streamich/json-type-map
Browse files Browse the repository at this point in the history
JSON Type "map" type
  • Loading branch information
streamich authored Dec 6, 2023
2 parents e8421d6 + 1102d58 commit 7cbde4f
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 13 deletions.
3 changes: 3 additions & 0 deletions src/json-schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export interface JsonSchemaObject extends JsonSchemaGenericKeywords {
};
required?: string[];
additionalProperties?: boolean | JsonSchemaNode;
patternProperties?: {
[key: string]: JsonSchemaNode;
};
const?: object;
}

Expand Down
34 changes: 34 additions & 0 deletions src/json-type/codegen/binary/__tests__/testBinaryCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,40 @@ export const testBinaryCodegen = (transcode: (system: TypeSystem, type: Type, va
});
});

describe('"map" type', () => {
test('can encode empty map', () => {
const system = new TypeSystem();
const t = system.t;
const type = t.map;
const value: {} = {};
expect(transcode(system, type, value)).toStrictEqual(value);
});

test('can encode empty map with one key', () => {
const system = new TypeSystem();
const t = system.t;
const type = t.map;
const value: {} = {a: 'asdf'};
expect(transcode(system, type, value)).toStrictEqual(value);
});

test('can encode typed map with two keys', () => {
const system = new TypeSystem();
const t = system.t;
const type = t.Map(t.bool);
const value: {} = {x: true, y: false};
expect(transcode(system, type, value)).toStrictEqual(value);
});

test('can encode nested maps', () => {
const system = new TypeSystem();
const t = system.t;
const type = t.Map(t.Map(t.bool));
const value: {} = {a: {x: true, y: false}};
expect(transcode(system, type, value)).toStrictEqual(value);
});
});

describe('"ref" type', () => {
test('can encode a simple reference', () => {
const system = new TypeSystem();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,38 @@ describe('object', () => {
});
});

describe('map', () => {
test('empty', () => {
const system = new TypeSystem();
const type = system.t.map;
const estimator = type.compileCapacityEstimator({});
expect(estimator(123)).toBe(maxEncodingCapacity({}));
});

test('with one field', () => {
const system = new TypeSystem();
const type = system.t.Map(system.t.bool);
const estimator = type.compileCapacityEstimator({});
expect(estimator({foo: true})).toBe(maxEncodingCapacity({foo: true}));
});

test('three number fields', () => {
const system = new TypeSystem();
const type = system.t.Map(system.t.num);
const estimator = type.compileCapacityEstimator({});
const data = {foo: 1, bar: 2, baz: 3};
expect(estimator(data)).toBe(maxEncodingCapacity(data));
});

test('nested maps', () => {
const system = new TypeSystem();
const type = system.t.Map(system.t.Map(system.t.str));
const estimator = type.compileCapacityEstimator({});
const data = {foo: {bar: 'baz'}, baz: {bar: 'foo'}};
expect(estimator(data)).toBe(maxEncodingCapacity(data));
});
});

describe('ref', () => {
test('two hops', () => {
const system = new TypeSystem();
Expand Down
22 changes: 22 additions & 0 deletions src/json-type/codegen/json/__tests__/json.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@ describe('"obj" type', () => {
});
});

describe('"map" type', () => {
test('serializes a map', () => {
const type = s.Map(s.num);
exec(type, {a: 1, b: 2, c: 3});
});

test('serializes empty map', () => {
const type = s.Map(s.num);
exec(type, {});
});

test('serializes a map with a single key', () => {
const type = s.Map(s.num);
exec(type, {'0': 0});
});

test('serializes a map in a map', () => {
const type = s.Map(s.Map(s.bool));
exec(type, {a: {b: true}});
});
});

describe('general', () => {
test('serializes according to schema a POJO object', () => {
const type = s.Object({
Expand Down
2 changes: 2 additions & 0 deletions src/json-type/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum ValidationError {
ARR,
TUP,
OBJ,
MAP,
KEY,
KEYS,
BIN,
Expand All @@ -32,6 +33,7 @@ export const ValidationErrorMessage = {
[ValidationError.ARR]: 'Not an array.',
[ValidationError.TUP]: 'Not a tuple.',
[ValidationError.OBJ]: 'Not an object.',
[ValidationError.MAP]: 'Not a map.',
[ValidationError.KEY]: 'Missing key.',
[ValidationError.KEYS]: 'Too many or missing object keys.',
[ValidationError.BIN]: 'Not a binary.',
Expand Down
2 changes: 1 addition & 1 deletion src/json-type/schema/__tests__/type.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test('can generate any type', () => {
s.prop('address', address),
s.prop('timeCreated', s.Number()),
s.prop('tags', s.Array(s.Or(s.Number(), s.String()))),
s.prop('elements', s.Map(s.str))
s.prop('elements', s.Map(s.str)),
);

expect(userType).toMatchObject({
Expand Down
12 changes: 12 additions & 0 deletions src/json-type/type/TypeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export class TypeBuilder {
return this.Object();
}

get map() {
return this.Map(this.any);
}

get fn() {
return this.Function(this.any, this.any);
}
Expand Down Expand Up @@ -129,6 +133,12 @@ export class TypeBuilder {
return field;
}

public Map<T extends Type>(type: T, options?: schema.Optional<schema.MapSchema>) {
const map = new classes.MapType<T>(type, options);
map.system = this.system;
return map;
}

public Or<F extends Type[]>(...types: F) {
const or = new classes.OrType<F>(types);
or.system = this.system;
Expand Down Expand Up @@ -176,6 +186,8 @@ export class TypeBuilder {
),
).options(node);
}
case 'map':
return this.Map(this.import(node.type), node);
case 'const':
return this.Const(node.value).options(node);
case 'or':
Expand Down
11 changes: 11 additions & 0 deletions src/json-type/type/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const everyType = t.Object(
t.prop('emptyArray', t.arr.options({max: 0})),
t.prop('oneItemArray', t.arr.options({min: 1, max: 1})),
t.prop('objWithArray', t.Object(t.propOpt('arr', t.arr), t.propOpt('arr2', t.arr))),
t.prop('emptyMap', t.map),
t.prop('mapWithOneNumField', t.Map(t.num)),
t.prop('mapOfStr', t.Map(t.str)),
);

export const everyTypeValue: TypeOf<SchemaOf<typeof everyType>> = {
Expand All @@ -37,4 +40,12 @@ export const everyTypeValue: TypeOf<SchemaOf<typeof everyType>> = {
objWithArray: {
arr: [1, 2, 3],
},
emptyMap: {},
mapWithOneNumField: {
a: 1,
},
mapOfStr: {
a: 'a',
b: 'b',
},
};
10 changes: 10 additions & 0 deletions src/json-type/type/__tests__/getJsonSchema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test('can print a type', () => {
)
.options({format: 'cbor'}),
),
t.prop('map', t.Map(t.str)),
)
.options({unknownFields: true});
// console.log(JSON.stringify(type.toJsonSchema(), null, 2));
Expand Down Expand Up @@ -83,6 +84,14 @@ test('can print a type', () => {
"id": {
"type": "string",
},
"map": {
"patternProperties": {
".*": {
"type": "string",
},
},
"type": "object",
},
"numberProperty": {
"exclusiveMinimum": 3.14,
"type": "number",
Expand Down Expand Up @@ -178,6 +187,7 @@ test('can print a type', () => {
"unionProperty",
"operation",
"binaryOperation",
"map",
],
"type": "object",
}
Expand Down
11 changes: 11 additions & 0 deletions src/json-type/type/__tests__/random.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ test('generates random JSON', () => {
t.prop('name', t.str),
t.prop('tags', t.Array(t.str)),
t.propOpt('scores', t.Array(t.num)),
t.prop('refs', t.Map(t.str)),
);
const json = type.random();
expect(json).toMatchInlineSnapshot(`
{
"id": "",
"name": "1",
"refs": {
"259<@CGK": "UY\\\`c",
";>BEILPT": "^beimp",
"HKORVY]\`": "korvy}#",
"LOSWZ^ae": "pswz #'*",
"_cfjmqtx": "гггггг诶诶诶诶",
"nquy|"%)": "4",
"w{ $'+/2": "=@",
"гггг诶诶诶诶": "MQTX",
},
"tags": [
"@CG",
"QUY\\\`",
Expand Down
16 changes: 10 additions & 6 deletions src/json-type/type/__tests__/toString.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ test('can print a type', () => {
)
.options({format: 'cbor'}),
),
t.prop('map', t.Map(t.num)),
)
.options({unknownFields: true});
// console.log(type + '');
Expand Down Expand Up @@ -83,11 +84,14 @@ test('can print a type', () => {
│ │ └─ str
│ └─ "value":
│ └─ any
└─ "binaryOperation":
└─ bin { format = "cbor" }
└─ tup { description = "Should always have 3 elements" }
├─ const { description = "7 is the magic number" } → 7
├─ str
└─ any"
├─ "binaryOperation":
│ └─ bin { format = "cbor" }
│ └─ tup { description = "Should always have 3 elements" }
│ ├─ const { description = "7 is the magic number" } → 7
│ ├─ str
│ └─ any
└─ "map":
└─ map
└─ num"
`);
});
25 changes: 25 additions & 0 deletions src/json-type/type/__tests__/toTypeScriptAst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,31 @@ describe('obj', () => {
});
});

describe('map', () => {
test('can emit tuple AST', () => {
const system = new TypeSystem();
const {t} = system;
const type = system.t.Map(t.num).options({
title: 'title',
description: 'description',
});
expect(type.toTypeScriptAst()).toMatchInlineSnapshot(`
{
"node": "TypeReference",
"typeArguments": [
{
"node": "StringKeyword",
},
{
"node": "NumberKeyword",
},
],
"typeName": "Record",
}
`);
});
});

describe('ref', () => {
test('can emit reference AST', () => {
const system = new TypeSystem();
Expand Down
31 changes: 31 additions & 0 deletions src/json-type/type/__tests__/validateTestSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,37 @@ export const validateTestSuite = (validate: (type: Type, value: unknown) => void
});
});

describe('map', () => {
test('accepts empty object as input', () => {
const type = t.map;
validate(type, {});
});

test('does not accept empty array as input', () => {
const type = t.map;
expect(() => validate(type, [])).toThrow();
});

test('validates "any" map', () => {
const type = t.map;
validate(type, {
a: 'str',
b: 123,
c: true,
});
});

test('validates contained type', () => {
const type = t.Map(t.str);
validate(type, {});
validate(type, {a: ''});
validate(type, {b: 'asdf'});
expect(() => validate(type, {c: 123})).toThrowErrorMatchingInlineSnapshot(`"STR"`);
expect(() => validate(type, {c: false})).toThrowErrorMatchingInlineSnapshot(`"STR"`);
expect(() => validate(type, [])).toThrowErrorMatchingInlineSnapshot(`"MAP"`);
});
});

describe('ref', () => {
test('validates after recursively resolving', () => {
const t = system.t;
Expand Down
Loading

0 comments on commit 7cbde4f

Please sign in to comment.