Skip to content

Commit 61d77a1

Browse files
committed
feat: 🎸 support named keys in validation
1 parent cd1aff7 commit 61d77a1

File tree

16 files changed

+71
-39
lines changed

16 files changed

+71
-39
lines changed

src/codegen/AbstractCodege.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
MapType,
1010
NumType,
1111
ObjType,
12+
KeyType,
1213
OrType,
1314
RefType,
1415
StrType,
@@ -27,6 +28,7 @@ export abstract class AbstractCodegen<Fn extends (...deps: any[]) => any = (...d
2728
protected abstract onBin(path: SchemaPath, r: JsExpression, type: BinType): void;
2829
protected abstract onArr(path: SchemaPath, r: JsExpression, type: ArrType): void;
2930
protected abstract onObj(path: SchemaPath, r: JsExpression, type: ObjType): void;
31+
protected abstract onKey(path: SchemaPath, r: JsExpression, type: KeyType<any, any>): void;
3032
protected abstract onMap(path: SchemaPath, r: JsExpression, type: MapType): void;
3133
protected abstract onRef(path: SchemaPath, r: JsExpression, type: RefType): void;
3234
protected abstract onOr(path: SchemaPath, r: JsExpression, type: OrType): void;
@@ -62,6 +64,9 @@ export abstract class AbstractCodegen<Fn extends (...deps: any[]) => any = (...d
6264
case 'obj':
6365
this.onObj(path, r, type as ObjType);
6466
break;
67+
case 'key':
68+
this.onKey(path, r, type as KeyType<any, any>);
69+
break;
6570
case 'map':
6671
this.onMap(path, r, type as MapType);
6772
break;

src/codegen/binary/cbor copy/CborCodegen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
22
import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor';
33
import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder';
4-
import {type MapType, ObjKeyOptType, type ObjType, type Type} from '../../../type';
4+
import {type MapType, KeyOptType, type ObjType, type Type} from '../../../type';
55
import type {CompiledBinaryEncoder, SchemaPath} from '../../types';
66
import {lazyKeyedFactory} from '../../util';
77
import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen';
@@ -23,8 +23,8 @@ export class CborCodegen extends AbstractBinaryCodegen<CborEncoder> {
2323
const r = codegen.r();
2424
const fields = type.keys;
2525
const length = fields.length;
26-
const requiredFields = fields.filter((field) => !(field instanceof ObjKeyOptType));
27-
const optionalFields = fields.filter((field) => field instanceof ObjKeyOptType);
26+
const requiredFields = fields.filter((field) => !(field instanceof KeyOptType));
27+
const optionalFields = fields.filter((field) => field instanceof KeyOptType);
2828
const requiredLength = requiredFields.length;
2929
const optionalLength = optionalFields.length;
3030
const encodeUnknownFields = !!type.schema.encodeUnknownKeys;

src/codegen/binary/cbor/CborCodegen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
22
import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor';
33
import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder';
4-
import {ObjKeyOptType, type ObjType, type Type} from '../../../type';
4+
import {KeyOptType, type ObjType, type Type} from '../../../type';
55
import type {CompiledBinaryEncoder, SchemaPath} from '../../types';
66
import {lazyKeyedFactory} from '../../util';
77
import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen';
@@ -23,8 +23,8 @@ export class CborCodegen extends AbstractBinaryCodegen<CborEncoder> {
2323
const r = codegen.r();
2424
const fields = type.keys;
2525
const length = fields.length;
26-
const requiredFields = fields.filter((field) => !(field instanceof ObjKeyOptType));
27-
const optionalFields = fields.filter((field) => field instanceof ObjKeyOptType);
26+
const requiredFields = fields.filter((field) => !(field instanceof KeyOptType));
27+
const optionalFields = fields.filter((field) => field instanceof KeyOptType);
2828
const requiredLength = requiredFields.length;
2929
const optionalLength = optionalFields.length;
3030
const encodeUnknownFields = !!type.schema.encodeUnknownKeys;

src/codegen/binary/json/JsonCodegen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
22
import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor';
33
import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder';
4-
import {type ArrType, type MapType, ObjKeyOptType, type ObjType, type Type} from '../../../type';
4+
import {type ArrType, type MapType, KeyOptType, type ObjType, type Type} from '../../../type';
55
import type {CompiledBinaryEncoder, SchemaPath} from '../../types';
66
import {lazyKeyedFactory} from '../../util';
77
import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen';
@@ -98,8 +98,8 @@ export class JsonCodegen extends AbstractBinaryCodegen<JsonEncoder> {
9898
const codegen = this.codegen;
9999
const r = codegen.var(value.use());
100100
const fields = type.keys;
101-
const requiredFields = fields.filter((field) => !(field instanceof ObjKeyOptType));
102-
const optionalFields = fields.filter((field) => field instanceof ObjKeyOptType);
101+
const requiredFields = fields.filter((field) => !(field instanceof KeyOptType));
102+
const optionalFields = fields.filter((field) => field instanceof KeyOptType);
103103
const requiredLength = requiredFields.length;
104104
const optionalLength = optionalFields.length;
105105
const encodeUnknownFields = !!type.schema.encodeUnknownKeys;

src/codegen/binary/msgpack/MsgPackCodegen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
22
import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor';
33
import {MsgPackEncoder} from '@jsonjoy.com/json-pack/lib/msgpack/MsgPackEncoder';
4-
import {ObjKeyOptType, type ObjType, type Type} from '../../../type';
4+
import {KeyOptType, type ObjType, type Type} from '../../../type';
55
import type {CompiledBinaryEncoder, SchemaPath} from '../../types';
66
import {lazyKeyedFactory} from '../../util';
77
import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen';
@@ -23,8 +23,8 @@ export class MsgPackCodegen extends AbstractBinaryCodegen<MsgPackEncoder> {
2323
const r = codegen.r();
2424
const fields = type.keys;
2525
const length = fields.length;
26-
const requiredFields = fields.filter((field) => !(field instanceof ObjKeyOptType));
27-
const optionalFields = fields.filter((field) => field instanceof ObjKeyOptType);
26+
const requiredFields = fields.filter((field) => !(field instanceof KeyOptType));
27+
const optionalFields = fields.filter((field) => field instanceof KeyOptType);
2828
const requiredLength = requiredFields.length;
2929
const optionalLength = optionalFields.length;
3030
const totalMaxKnownFields = requiredLength + optionalLength;

src/codegen/capacity/CapacityEstimatorCodegen.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {Codegen, CodegenStepExecJs} from '@jsonjoy.com/codegen';
22
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
33
import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor';
44
import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size';
5-
import {BoolType, ConType, NumType, ObjKeyOptType} from '../../type';
6-
import type {ArrType, MapType, ObjKeyType, ObjType, OrType, RefType, Type} from '../../type';
5+
import {BoolType, ConType, NumType, KeyOptType} from '../../type';
6+
import type {ArrType, MapType, KeyType, ObjType, OrType, RefType, Type} from '../../type';
77
import {DiscriminatorCodegen} from '../discriminator';
88
import {lazyKeyedFactory} from '../util';
99

@@ -113,10 +113,10 @@ export class CapacityEstimatorCodegen {
113113
this.inc(MaxEncodingOverhead.Object);
114114
const fields = objectType.keys;
115115
for (const f of fields) {
116-
const field = f as ObjKeyType<any, any>;
116+
const field = f as KeyType<any, any>;
117117
const accessor = normalizeAccessor(field.key);
118118
const fieldExpression = new JsExpression(() => `${r}${accessor}`);
119-
const isOptional = field instanceof ObjKeyOptType;
119+
const isOptional = field instanceof KeyOptType;
120120
if (isOptional) {
121121
codegen.if(/* js */ `${JSON.stringify(field.key)} in ${r}`, () => {
122122
this.inc(MaxEncodingOverhead.ObjectElement);

src/codegen/json/JsonTextCodegen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor
55
import {stringify} from '@jsonjoy.com/json-pack/lib/json-binary/codec';
66
import type {json_string} from '@jsonjoy.com/util/lib/json-brand';
77
import {asString} from '@jsonjoy.com/util/lib/strings/asString';
8-
import {ObjKeyOptType} from '../../type';
8+
import {KeyOptType} from '../../type';
99
import type {ArrType, ConType, MapType, ObjType, OrType, RefType, StrType, Type} from '../../type';
1010
import {DiscriminatorCodegen} from '../discriminator';
1111
import {lazyKeyedFactory} from '../util';
@@ -107,8 +107,8 @@ export class JsonTextCodegen {
107107
if (schema.encodeUnknownKeys) {
108108
this.js(/* js */ `var ${rKeys} = new Set(Object.keys(${r}));`);
109109
}
110-
const requiredFields = fields.filter((field) => !(field instanceof ObjKeyOptType));
111-
const optionalFields = fields.filter((field) => field instanceof ObjKeyOptType) as ObjKeyOptType<string, Type>[];
110+
const requiredFields = fields.filter((field) => !(field instanceof KeyOptType));
111+
const optionalFields = fields.filter((field) => field instanceof KeyOptType) as KeyOptType<string, Type>[];
112112
this.writeText('{');
113113
for (let i = 0; i < requiredFields.length; i++) {
114114
const field = requiredFields[i];

src/codegen/validator/ValidatorCodegen.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
type ConType,
1212
type MapType,
1313
type NumType,
14-
ObjKeyOptType,
14+
KeyOptType,
1515
type ObjType,
1616
type OrType,
1717
type RefType,
1818
type StrType,
1919
type Type,
20+
KeyType,
2021
} from '../../type';
2122
import {floats, ints, uints} from '../../util';
2223
import {isAscii, isUtf8} from '../../util/stringFormats';
@@ -379,7 +380,7 @@ export class ValidatorCodegen extends AbstractCodegen {
379380
const accessor = normalizeAccessor(field.key);
380381
const keyPath = [...path, field.key];
381382
codegen.js(/* js */ `var ${rv} = ${r.use()}${accessor};`);
382-
if (field instanceof ObjKeyOptType) {
383+
if (field instanceof KeyOptType) {
383384
codegen.js(/* js */ `if (${rv} !== undefined) {`);
384385
this.onNode(keyPath, new JsExpression(() => rv), field.val);
385386
codegen.js(/* js */ `}`);
@@ -394,6 +395,10 @@ export class ValidatorCodegen extends AbstractCodegen {
394395
}
395396
}
396397

398+
protected onKey(path: SchemaPath, r: JsExpression, type: KeyType<any, any>): void {
399+
this.onNode([...path, type.key], r, type.val);
400+
}
401+
397402
protected onMap(path: SchemaPath, r: JsExpression, type: MapType): void {
398403
const codegen = this.codegen;
399404
const err = this.err(ValidationError.MAP, path);

src/codegen/validator/__tests__/codegen.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,24 @@ describe('"arr" type', () => {
841841
});
842842
});
843843

844+
test('named head 2-tuple', () => {
845+
const type = s.Tuple([s.Key('num', s.num), s.Key('str', s.str)]);
846+
exec(type, [0, ''], null);
847+
exec(type, [1, 'x'], null);
848+
exec(type, ['', 'x'], {
849+
code: 'NUM',
850+
errno: ValidationError.NUM,
851+
message: 'Not a number.',
852+
path: [0, 'num'],
853+
});
854+
exec(type, [-1, true], {
855+
code: 'STR',
856+
errno: ValidationError.STR,
857+
message: 'Not a string.',
858+
path: [1, 'str'],
859+
});
860+
});
861+
844862
test('head + elements', () => {
845863
const type = s.Tuple([s.Const<true>(true)], s.num);
846864
exec(type, [true, 123], null);

src/jtd/converter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {type ArrType, ObjKeyOptType, type ObjType, type RefType, type Type} from '../type';
1+
import {type ArrType, KeyOptType, type ObjType, type RefType, type Type} from '../type';
22
import type * as jtd from './types';
33

44
const NUMS_TYPE_MAPPING = new Map<string, jtd.JtdType>([
@@ -86,7 +86,7 @@ export function toJtdForm(type: Type): jtd.JtdForm {
8686
if (fieldType) {
8787
const fieldJtd = toJtdForm(fieldType);
8888
// Check if field is optional
89-
if (field instanceof ObjKeyOptType) {
89+
if (field instanceof KeyOptType) {
9090
form.optionalProperties[fieldName] = fieldJtd;
9191
} else {
9292
form.properties[fieldName] = fieldJtd;

0 commit comments

Comments
 (0)