UNPKG

@jsonjoy.com/json-type

Version:

High-performance JSON Pointer implementation

481 lines (480 loc) 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObjType = exports.ObjectOptionalFieldType = exports.ObjectFieldType = void 0; const tslib_1 = require("tslib"); const normalizeAccessor_1 = require("@jsonjoy.com/util/lib/codegen/util/normalizeAccessor"); const JsExpression_1 = require("@jsonjoy.com/util/lib/codegen/util/JsExpression"); const asString_1 = require("@jsonjoy.com/util/lib/strings/asString"); const printTree_1 = require("tree-dump/lib/printTree"); const schema = tslib_1.__importStar(require("../../schema")); const constants_1 = require("../../constants"); const util_1 = require("../../codegen/validator/util"); const AbsType_1 = require("./AbsType"); const augmentWithComment = (type, node) => { if (type.title || type.description) { let comment = ''; if (type.title) comment += '# ' + type.title; if (type.title && type.description) comment += '\n\n'; if (type.description) comment += type.description; node.comment = comment; } }; class ObjectFieldType extends AbsType_1.AbsType { constructor(key, value) { super(); this.key = key; this.value = value; this.schema = schema.s.prop(key, schema.s.any); } getSchema() { return { ...this.schema, value: this.value.getSchema(), }; } getOptions() { const { kind, key, value, optional, ...options } = this.schema; return options; } toStringTitle() { return `"${this.key}":`; } toString(tab = '') { return super.toString(tab) + (0, printTree_1.printTree)(tab + ' ', [(tab) => this.value.toString(tab)]); } } exports.ObjectFieldType = ObjectFieldType; class ObjectOptionalFieldType extends ObjectFieldType { constructor(key, value) { super(key, value); this.key = key; this.value = value; this.optional = true; this.schema = schema.s.propOpt(key, schema.s.any); } toStringTitle() { return `"${this.key}"?:`; } } exports.ObjectOptionalFieldType = ObjectOptionalFieldType; class ObjType extends AbsType_1.AbsType { constructor(fields) { super(); this.fields = fields; this.schema = schema.s.obj; } _field(field, options) { if (options) field.options(options); field.system = this.system; this.fields.push(field); } /** * Adds a property to the object type. * @param key The key of the property. * @param value The value type of the property. * @param options Optional schema options for the property. * @returns A new object type with the added property. */ prop(key, value, options) { this._field(new ObjectFieldType(key, value), options); return this; } /** * Adds an optional property to the object type. * @param key The key of the property. * @param value The value type of the property. * @param options Optional schema options for the property. * @returns A new object type with the added property. */ opt(key, value, options) { this._field(new ObjectOptionalFieldType(key, value), options); return this; } getSchema() { return { ...this.schema, fields: this.fields.map((f) => f.getSchema()), }; } getOptions() { const { kind, fields, ...options } = this.schema; return options; } getField(key) { return this.fields.find((f) => f.key === key); } extend(o) { const type = new ObjType([...this.fields, ...o.fields]); type.system = this.system; return type; } omit(key) { const type = new ObjType(this.fields.filter((f) => f.key !== key)); type.system = this.system; return type; } pick(key) { const field = this.fields.find((f) => f.key === key); if (!field) throw new Error('FIELD_NOT_FOUND'); const type = new ObjType([field]); type.system = this.system; return type; } codegenValidator(ctx, path, r) { const fields = this.fields; const length = fields.length; const canSkipObjTypeCheck = ctx.options.unsafeMode && length > 0; if (!canSkipObjTypeCheck) { const err = ctx.err(constants_1.ValidationError.OBJ, path); ctx.js(/* js */ `if (typeof ${r} !== 'object' || !${r} || (${r} instanceof Array)) return ${err};`); } const checkExtraKeys = length && !this.schema.unknownFields && !ctx.options.skipObjectExtraFieldsCheck; if (checkExtraKeys) { const rk = ctx.codegen.getRegister(); ctx.js(`for (var ${rk} in ${r}) {`); ctx.js(`switch (${rk}) { case ${fields .map((field) => JSON.stringify(field.key)) .join(': case ')}: break; default: return ${ctx.err(constants_1.ValidationError.KEYS, [...path, { r: rk }])};}`); ctx.js(`}`); } for (let i = 0; i < length; i++) { const field = fields[i]; const rv = ctx.codegen.getRegister(); const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); const keyPath = [...path, field.key]; if (field instanceof ObjectOptionalFieldType) { ctx.js(/* js */ `var ${rv} = ${r}${accessor};`); ctx.js(`if (${rv} !== undefined) {`); field.value.codegenValidator(ctx, keyPath, rv); ctx.js(`}`); } else { if (!(0, util_1.canSkipObjectKeyUndefinedCheck)(field.value.getSchema().kind)) { const err = ctx.err(constants_1.ValidationError.KEY, [...path, field.key]); ctx.js(/* js */ `var ${rv} = ${r}${accessor};`); ctx.js(/* js */ `if (!(${JSON.stringify(field.key)} in ${r})) return ${err};`); } field.value.codegenValidator(ctx, keyPath, `${r}${accessor}`); } } ctx.emitCustomValidators(this, path, r); } codegenJsonTextEncoder(ctx, value) { const { schema, fields } = this; const codegen = ctx.codegen; const r = codegen.getRegister(); ctx.js(/* js */ `var ${r} = ${value.use()};`); const rKeys = ctx.codegen.getRegister(); if (schema.encodeUnknownFields) { ctx.js(/* js */ `var ${rKeys} = new Set(Object.keys(${r}));`); } const requiredFields = fields.filter((field) => !(field instanceof ObjectOptionalFieldType)); const optionalFields = fields.filter((field) => field instanceof ObjectOptionalFieldType); ctx.writeText('{'); for (let i = 0; i < requiredFields.length; i++) { const field = requiredFields[i]; if (i) ctx.writeText(','); ctx.writeText(JSON.stringify(field.key) + ':'); const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); const valueExpression = new JsExpression_1.JsExpression(() => `${r}${accessor}`); if (schema.encodeUnknownFields) ctx.js(/* js */ `${rKeys}.delete(${JSON.stringify(field.key)});`); field.value.codegenJsonTextEncoder(ctx, valueExpression); } const rHasFields = codegen.getRegister(); if (!requiredFields.length) ctx.js(/* js */ `var ${rHasFields} = false;`); for (let i = 0; i < optionalFields.length; i++) { const field = optionalFields[i]; const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); const rValue = codegen.getRegister(); if (schema.encodeUnknownFields) ctx.js(/* js */ `${rKeys}.delete(${JSON.stringify(field.key)});`); ctx.js(/* js */ `var ${rValue} = ${r}${accessor};`); ctx.js(`if (${rValue} !== undefined) {`); if (requiredFields.length) { ctx.writeText(','); } else { ctx.js(`if (${rHasFields}) s += ',';`); ctx.js(/* js */ `${rHasFields} = true;`); } ctx.writeText(JSON.stringify(field.key) + ':'); const valueExpression = new JsExpression_1.JsExpression(() => `${rValue}`); field.value.codegenJsonTextEncoder(ctx, valueExpression); ctx.js(`}`); } if (schema.encodeUnknownFields) { const [rList, ri, rLength, rk] = [codegen.r(), codegen.r(), codegen.r(), codegen.r()]; ctx.js(`var ${rLength} = ${rKeys}.size; if (${rLength}) { var ${rk}, ${rList} = Array.from(${rKeys}.values()); for (var ${ri} = 0; ${ri} < ${rLength}; ${ri}++) { ${rk} = ${rList}[${ri}]; s += ',' + asString(${rk}) + ':' + stringify(${r}[${rk}]); } }`); } ctx.writeText('}'); } codegenCborEncoder(ctx, value) { const codegen = ctx.codegen; const r = codegen.r(); const fields = this.fields; const length = fields.length; const requiredFields = fields.filter((field) => !(field instanceof ObjectOptionalFieldType)); const optionalFields = fields.filter((field) => field instanceof ObjectOptionalFieldType); const requiredLength = requiredFields.length; const optionalLength = optionalFields.length; const encodeUnknownFields = !!this.schema.encodeUnknownFields; const emitRequiredFields = () => { for (let i = 0; i < requiredLength; i++) { const field = requiredFields[i]; ctx.blob(ctx.gen((encoder) => encoder.writeStr(field.key))); const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); field.value.codegenCborEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); } }; const emitOptionalFields = () => { for (let i = 0; i < optionalLength; i++) { const field = optionalFields[i]; const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); codegen.js(`if (${r}${accessor} !== undefined) {`); ctx.blob(ctx.gen((encoder) => encoder.writeStr(field.key))); field.value.codegenCborEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); codegen.js(`}`); } }; const emitUnknownFields = () => { const rKeys = codegen.r(); const rKey = codegen.r(); const ri = codegen.r(); const rLength = codegen.r(); const keys = fields.map((field) => JSON.stringify(field.key)); const rKnownFields = codegen.addConstant(`new Set([${keys.join(',')}])`); codegen.js(`var ${rKeys} = Object.keys(${r}), ${rLength} = ${rKeys}.length, ${rKey};`); codegen.js(`for (var ${ri} = 0; ${ri} < ${rLength}; ${ri}++) {`); codegen.js(`${rKey} = ${rKeys}[${ri}];`); codegen.js(`if (${rKnownFields}.has(${rKey})) continue;`); codegen.js(`encoder.writeStr(${rKey});`); codegen.js(`encoder.writeAny(${r}[${rKey}]);`); codegen.js(`}`); }; ctx.js(/* js */ `var ${r} = ${value.use()};`); if (!encodeUnknownFields && !optionalLength) { ctx.blob(ctx.gen((encoder) => encoder.writeObjHdr(length))); emitRequiredFields(); } else if (!encodeUnknownFields) { ctx.blob(ctx.gen((encoder) => encoder.writeStartObj())); emitRequiredFields(); emitOptionalFields(); ctx.blob(ctx.gen((encoder) => encoder.writeEndObj())); } else { ctx.blob(ctx.gen((encoder) => encoder.writeStartObj())); emitRequiredFields(); emitOptionalFields(); emitUnknownFields(); ctx.blob(ctx.gen((encoder) => encoder.writeEndObj())); } } codegenMessagePackEncoder(ctx, value) { const codegen = ctx.codegen; const r = codegen.r(); const fields = this.fields; const length = fields.length; const requiredFields = fields.filter((field) => !(field instanceof ObjectOptionalFieldType)); const optionalFields = fields.filter((field) => field instanceof ObjectOptionalFieldType); const requiredLength = requiredFields.length; const optionalLength = optionalFields.length; const totalMaxKnownFields = requiredLength + optionalLength; if (totalMaxKnownFields > 0xffff) throw new Error('Too many fields'); const encodeUnknownFields = !!this.schema.encodeUnknownFields; const rFieldCount = codegen.r(); const emitRequiredFields = () => { for (let i = 0; i < requiredLength; i++) { const field = requiredFields[i]; ctx.blob(ctx.gen((encoder) => encoder.writeStr(field.key))); const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); field.value.codegenMessagePackEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); } }; const emitOptionalFields = () => { for (let i = 0; i < optionalLength; i++) { const field = optionalFields[i]; const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); codegen.if(`${r}${accessor} !== undefined`, () => { codegen.js(`${rFieldCount}++;`); ctx.blob(ctx.gen((encoder) => encoder.writeStr(field.key))); field.value.codegenMessagePackEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); }); } }; const emitUnknownFields = () => { const ri = codegen.r(); const rKeys = codegen.r(); const rKey = codegen.r(); const rLength = codegen.r(); const keys = fields.map((field) => JSON.stringify(field.key)); const rKnownFields = codegen.addConstant(`new Set([${keys.join(',')}])`); codegen.js(`var ${rKeys} = Object.keys(${r}), ${rLength} = ${rKeys}.length, ${rKey};`); codegen.js(`for (var ${ri} = 0; ${ri} < ${rLength}; ${ri}++) {`); codegen.js(`${rKey} = ${rKeys}[${ri}];`); codegen.js(`if (${rKnownFields}.has(${rKey})) continue;`); codegen.js(`${rFieldCount}++;`); codegen.js(`encoder.writeStr(${rKey});`); codegen.js(`encoder.writeAny(${r}[${rKey}]);`); codegen.js(`}`); }; ctx.js(/* js */ `var ${r} = ${value.use()};`); if (!encodeUnknownFields && !optionalLength) { ctx.blob(ctx.gen((encoder) => encoder.writeObjHdr(length))); emitRequiredFields(); } else if (!encodeUnknownFields) { codegen.js(`var ${rFieldCount} = ${requiredLength};`); const rHeaderPosition = codegen.var('writer.x'); ctx.blob(ctx.gen((encoder) => encoder.writeObjHdr(0xffff))); emitRequiredFields(); emitOptionalFields(); codegen.js(`view.setUint16(${rHeaderPosition} + 1, ${rFieldCount});`); } else { codegen.js(`var ${rFieldCount} = ${requiredLength};`); const rHeaderPosition = codegen.var('writer.x'); ctx.blob(ctx.gen((encoder) => encoder.writeObjHdr(0xffffffff))); emitRequiredFields(); emitOptionalFields(); emitUnknownFields(); codegen.js(`view.setUint32(${rHeaderPosition} + 1, ${rFieldCount});`); } } codegenJsonEncoder(ctx, value) { const codegen = ctx.codegen; const r = codegen.var(value.use()); const fields = this.fields; const requiredFields = fields.filter((field) => !(field instanceof ObjectOptionalFieldType)); const optionalFields = fields.filter((field) => field instanceof ObjectOptionalFieldType); const requiredLength = requiredFields.length; const optionalLength = optionalFields.length; const encodeUnknownFields = !!this.schema.encodeUnknownFields; const separatorBlob = ctx.gen((encoder) => encoder.writeObjSeparator()); const keySeparatorBlob = ctx.gen((encoder) => encoder.writeObjKeySeparator()); const endBlob = ctx.gen((encoder) => encoder.writeEndObj()); const emitRequiredFields = () => { for (let i = 0; i < requiredLength; i++) { const field = requiredFields[i]; ctx.blob(ctx.gen((encoder) => { encoder.writeStr(field.key); encoder.writeObjKeySeparator(); })); const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); field.value.codegenJsonEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); ctx.blob(separatorBlob); } }; const emitOptionalFields = () => { for (let i = 0; i < optionalLength; i++) { const field = optionalFields[i]; const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key); codegen.if(`${r}${accessor} !== undefined`, () => { ctx.blob(ctx.gen((encoder) => { encoder.writeStr(field.key); })); ctx.blob(keySeparatorBlob); field.value.codegenJsonEncoder(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`)); ctx.blob(separatorBlob); }); } }; const emitUnknownFields = () => { const rKeys = codegen.r(); const rKey = codegen.r(); const ri = codegen.r(); const rLength = codegen.r(); const keys = fields.map((field) => JSON.stringify(field.key)); const rKnownFields = codegen.addConstant(`new Set([${keys.join(',')}])`); codegen.js(`var ${rKeys} = Object.keys(${r}), ${rLength} = ${rKeys}.length, ${rKey};`); codegen.js(`for (var ${ri} = 0; ${ri} < ${rLength}; ${ri}++) {`); codegen.js(`${rKey} = ${rKeys}[${ri}];`); codegen.js(`if (${rKnownFields}.has(${rKey})) continue;`); codegen.js(`encoder.writeStr(${rKey});`); ctx.blob(keySeparatorBlob); codegen.js(`encoder.writeAny(${r}[${rKey}]);`); ctx.blob(separatorBlob); codegen.js(`}`); }; const emitEnding = () => { const rewriteLastSeparator = () => { for (let i = 0; i < endBlob.length; i++) ctx.js(`uint8[writer.x - ${endBlob.length - i}] = ${endBlob[i]};`); }; if (requiredFields.length) { rewriteLastSeparator(); } else { codegen.if(`uint8[writer.x - 1] === ${separatorBlob[separatorBlob.length - 1]}`, () => { rewriteLastSeparator(); }, () => { ctx.blob(endBlob); }); } }; ctx.blob(ctx.gen((encoder) => { encoder.writeStartObj(); })); if (!encodeUnknownFields && !optionalLength) { emitRequiredFields(); emitEnding(); } else if (!encodeUnknownFields) { emitRequiredFields(); emitOptionalFields(); emitEnding(); } else { emitRequiredFields(); emitOptionalFields(); emitUnknownFields(); emitEnding(); } } toJson(value, system = this.system) { const fields = this.fields; const length = fields.length; if (!length) return '{}'; const last = length - 1; let str = '{'; for (let i = 0; i < last; i++) { const field = fields[i]; const key = field.key; const fieldType = field.value; const val = value[key]; if (val === undefined) continue; str += (0, asString_1.asString)(key) + ':' + fieldType.toJson(val, system) + ','; } const key = fields[last].key; const val = value[key]; if (val !== undefined) { str += (0, asString_1.asString)(key) + ':' + fields[last].value.toJson(val, system); } else if (str.length > 1) str = str.slice(0, -1); return (str + '}'); } toString(tab = '') { return (super.toString(tab) + (0, printTree_1.printTree)(tab, this.fields.map((field) => (tab) => field.toString(tab)))); } } exports.ObjType = ObjType;