@jsonjoy.com/json-type
Version:
High-performance JSON Pointer implementation
555 lines (554 loc) • 24.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObjectType = exports.ObjectOptionalFieldType = exports.ObjectFieldType = void 0;
const tslib_1 = require("tslib");
const normalizeAccessor_1 = require("@jsonjoy.com/util/lib/codegen/util/normalizeAccessor");
const json_size_1 = require("@jsonjoy.com/util/lib/json-size");
const JsExpression_1 = require("@jsonjoy.com/util/lib/codegen/util/JsExpression");
const asString_1 = require("@jsonjoy.com/util/lib/strings/asString");
const json_random_1 = require("@jsonjoy.com/util/lib/json-random");
const printTree_1 = require("tree-dump/lib/printTree");
const schema = tslib_1.__importStar(require("../../schema"));
const validate_1 = require("../../schema/validate");
const constants_1 = require("../../constants");
const util_1 = require("../../codegen/validator/util");
const AbstractType_1 = require("./AbstractType");
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 AbstractType_1.AbstractType {
constructor(key, value) {
super();
this.key = key;
this.value = value;
this.schema = schema.s.prop(key, schema.s.any);
}
getSchema() {
return {
...this.schema,
type: this.value.getSchema(),
};
}
getOptions() {
const { kind, key, type, optional, ...options } = this.schema;
return options;
}
validateSchema() {
const schema = this.getSchema();
(0, validate_1.validateTType)(schema, 'field');
const { key, optional } = schema;
if (typeof key !== 'string')
throw new Error('KEY_TYPE');
if (optional !== undefined && typeof optional !== 'boolean')
throw new Error('OPTIONAL_TYPE');
this.value.validateSchema();
}
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 ObjectType extends AbstractType_1.AbstractType {
constructor(fields) {
super();
this.fields = fields;
this.schema = schema.s.obj;
}
getSchema() {
return {
...this.schema,
fields: this.fields.map((f) => f.getSchema()),
};
}
toJsonSchema(ctx) {
const jsonSchema = {
type: 'object',
properties: {},
...super.toJsonSchema(ctx),
};
const required = [];
for (const field of this.fields) {
jsonSchema.properties[field.key] = field.value.toJsonSchema(ctx);
if (!(field instanceof ObjectOptionalFieldType))
required.push(field.key);
}
if (required.length)
jsonSchema.required = required;
if (this.schema.unknownFields === false)
jsonSchema.additionalProperties = false;
return jsonSchema;
}
getOptions() {
const { kind, fields, ...options } = this.schema;
return options;
}
getField(key) {
return this.fields.find((f) => f.key === key);
}
extend(o) {
const type = new ObjectType([...this.fields, ...o.fields]);
type.system = this.system;
return type;
}
omit(key) {
const type = new ObjectType(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 ObjectType([field]);
type.system = this.system;
return type;
}
validateSchema() {
const schema = this.getSchema();
(0, validate_1.validateTType)(schema, 'obj');
(0, validate_1.validateWithValidator)(schema);
const { fields, unknownFields } = schema;
if (!Array.isArray(fields))
throw new Error('FIELDS_TYPE');
if (unknownFields !== undefined && typeof unknownFields !== 'boolean')
throw new Error('UNKNOWN_FIELDS_TYPE');
for (const field of this.fields)
field.validateSchema();
}
codegenValidator(ctx, path, r) {
const fields = this.fields;
const length = fields.length;
const canSkipObjectTypeCheck = ctx.options.unsafeMode && length > 0;
if (!canSkipObjectTypeCheck) {
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 (${rv} === undefined) 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();
}
}
codegenCapacityEstimator(ctx, value) {
const codegen = ctx.codegen;
const r = codegen.var(value.use());
const encodeUnknownFields = !!this.schema.encodeUnknownFields;
if (encodeUnknownFields) {
codegen.js(`size += maxEncodingCapacity(${r});`);
return;
}
const fields = this.fields;
const overhead = 5 /* MaxEncodingOverhead.Object */ + fields.length * 2 /* MaxEncodingOverhead.ObjectElement */;
ctx.inc(overhead);
for (const field of fields) {
ctx.inc((0, json_size_1.maxEncodingCapacity)(field.key));
const accessor = (0, normalizeAccessor_1.normalizeAccessor)(field.key);
const isOptional = field instanceof ObjectOptionalFieldType;
const block = () => field.value.codegenCapacityEstimator(ctx, new JsExpression_1.JsExpression(() => `${r}${accessor}`));
if (isOptional) {
codegen.if(`${r}${accessor} !== undefined`, block);
}
else
block();
}
}
random() {
const schema = this.schema;
const obj = schema.unknownFields ? json_random_1.RandomJson.genObject() : {};
for (const field of this.fields) {
if (field instanceof ObjectOptionalFieldType)
if (Math.random() > 0.5)
continue;
obj[field.key] = field.value.random();
}
return obj;
}
toTypeScriptAst() {
const node = {
node: 'TypeLiteral',
members: [],
};
const fields = this.fields;
for (const field of fields) {
const member = {
node: 'PropertySignature',
name: field.key,
type: field.value.toTypeScriptAst(),
};
if (field instanceof ObjectOptionalFieldType)
member.optional = true;
augmentWithComment(field.getSchema(), member);
node.members.push(member);
}
if (this.schema.unknownFields || this.schema.encodeUnknownFields)
node.members.push({
node: 'IndexSignature',
type: { node: 'UnknownKeyword' },
});
augmentWithComment(this.schema, node);
return node;
}
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.ObjectType = ObjectType;