@jsonjoy.com/json-type
Version:
High-performance JSON Pointer implementation
481 lines (480 loc) • 21 kB
JavaScript
;
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;