@deepkit/bson
Version:
Deepkit BSON parser
425 lines (364 loc) • 16.4 kB
text/typescript
/*
* Deepkit Framework
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
*
* You should have received a copy of the MIT License along with this program.
*/
import { ClassType, CompilerContext } from '@deepkit/core';
import { ClassSchema, getClassSchema, getSortedUnionTypes, PropertySchema } from '@deepkit/type';
import { BaseParser, ParserV2 } from './bson-parser';
import { bsonTypeGuards } from './bson-typeguards';
import { seekElementSize } from './continuation';
import { BSONType, BSON_BINARY_SUBTYPE_UUID, digitByteSize } from './utils';
function createPropertyConverter(setter: string, property: PropertySchema, compiler: CompilerContext, parentProperty?: PropertySchema): string {
//we want the isNullable value from the actual property, not the decorated.
const defaultValue = !property.hasDefaultValue && property.defaultValue !== undefined ?
`${compiler.reserveVariable('defaultValue', property.defaultValue)}()`
: 'undefined';
const nullCheck = `
if (elementType === ${BSONType.UNDEFINED}) {
if (${property.isOptional}) ${setter} = ${defaultValue};
} else if (elementType === ${BSONType.NULL}) {
if (${property.isOptional}) ${setter} = ${defaultValue};
if (${property.isNullable}) ${setter} = null;
}`;
const nullOrSeek = `
${nullCheck} else {
seekElementSize(elementType, parser);
}
`;
// if (property.type === 'class' && property.getResolvedClassSchema().decorator) {
// property = property.getResolvedClassSchema().getDecoratedPropertySchema();
// }
const propertyVar = '_property_' + property.name;
compiler.context.set(propertyVar, property);
if (property.type === 'string') {
return `
if (elementType === ${BSONType.STRING}) {
const size = parser.eatUInt32(); //size
${setter} = parser.eatString(size);
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'literal') {
const literalValue = compiler.reserveVariable('literalValue', property.literalValue);
if (property.isOptional || property.isNullable) {
return `
if (${property.isOptional} && (elementType === ${BSONType.UNDEFINED} || elementType === ${BSONType.NULL})) {
${setter} = ${defaultValue};
} else if (${property.isNullable} && elementType === ${BSONType.NULL}) {
${setter} = null;
} else {
${setter} = ${literalValue};
seekElementSize(elementType, parser);
}
`;
} else {
return `
${setter} = ${literalValue};
seekElementSize(elementType, parser);
`;
}
} else if (property.type === 'enum') {
return `
if (elementType === ${BSONType.STRING} || elementType === ${BSONType.NUMBER} || elementType === ${BSONType.INT} || elementType === ${BSONType.LONG}) {
${setter} = parser.parse(elementType);
} else {
${nullOrSeek}
}`;
} else if (property.type === 'boolean') {
return `
if (elementType === ${BSONType.BOOLEAN}) {
${setter} = parser.parseBoolean();
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'date') {
return `
if (elementType === ${BSONType.DATE}) {
${setter} = parser.parseDate();
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'number') {
return `
if (elementType === ${BSONType.INT}) {
${setter} = parser.parseInt();
} else if (elementType === ${BSONType.NUMBER}) {
${setter} = parser.parseNumber();
} else if (elementType === ${BSONType.LONG} || elementType === ${BSONType.TIMESTAMP}) {
${setter} = parser.parseLong();
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'uuid') {
return `
if (elementType === ${BSONType.BINARY}) {
parser.eatUInt32(); //size
const subType = parser.eatByte();
if (subType !== ${BSON_BINARY_SUBTYPE_UUID}) throw new Error('${property.name} BSON binary type invalid. Expected UUID(4), but got ' + subType);
${setter} = parser.parseUUID();
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'objectId') {
return `
if (elementType === ${BSONType.OID}) {
${setter} = parser.parseOid();
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'partial') {
const object = compiler.reserveVariable('partiaObject', {});
const propertyCode: string[] = [];
const schema = property.getResolvedClassSchema();
for (let subProperty of schema.getProperties()) {
//todo, support non-ascii names
const bufferCompare: string[] = [];
for (let i = 0; i < subProperty.name.length; i++) {
bufferCompare.push(`parser.buffer[parser.offset + ${i}] === ${subProperty.name.charCodeAt(i)}`);
}
bufferCompare.push(`parser.buffer[parser.offset + ${subProperty.name.length}] === 0`);
propertyCode.push(`
//partial: property ${subProperty.name} (${subProperty.toString()})
if (${bufferCompare.join(' && ')}) {
parser.offset += ${subProperty.name.length} + 1;
${createPropertyConverter(`${object}.${subProperty.name}`, subProperty, compiler)}
continue;
}
`);
}
return `
if (elementType === ${BSONType.OBJECT}) {
${object} = {};
const end = parser.eatUInt32() + parser.offset;
while (parser.offset < end) {
const elementType = parser.eatByte();
if (elementType === 0) break;
${propertyCode.join('\n')}
//jump over this property when not registered in schema
while (parser.offset < end && parser.buffer[parser.offset++] != 0);
//seek property value
if (parser.offset >= end) break;
seekElementSize(elementType, parser);
}
${setter} = ${object};
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'class') {
const schema = property.getResolvedClassSchema();
if (schema.decorator) {
//we need to create the instance and assign
const forwardProperty = schema.getDecoratedPropertySchema();
const decoratedVar = compiler.reserveVariable('decorated');
const classTypeVar = compiler.reserveVariable('classType', property.classType);
let arg = '';
const propertyAssign: string[] = [];
const check = forwardProperty.isOptional ? `'v' in ${decoratedVar}` : `${decoratedVar}.v !== undefined`;
if (forwardProperty.methodName === 'constructor') {
arg = `(${check} ? ${decoratedVar}.v : undefined)`;
} else {
propertyAssign.push(`if (${check}) ${setter}.${forwardProperty.name} = ${decoratedVar}.v;`);
}
return `
//decorated
if (elementType !== ${BSONType.UNDEFINED} && elementType !== ${BSONType.NULL}) {
${decoratedVar} = {};
${createPropertyConverter(`${decoratedVar}.v`, schema.getDecoratedPropertySchema(), compiler)}
${setter} = new ${classTypeVar}(${arg});
${propertyAssign.join('\n')}
} else {
${nullOrSeek}
}
`;
}
const propertySchema = '_propertySchema_' + property.name;
compiler.context.set('getRawBSONDecoder', getRawBSONDecoder);
compiler.context.set(propertySchema, property.getResolvedClassSchema());
let primaryKeyHandling = '';
if (property.isReference) {
primaryKeyHandling = createPropertyConverter(setter, property.getResolvedClassSchema().getPrimaryField(), compiler);
}
return `
if (elementType === ${BSONType.OBJECT}) {
${setter} = getRawBSONDecoder(${propertySchema})(parser);
} else if (${property.isReference}) {
${primaryKeyHandling}
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'array') {
compiler.context.set('digitByteSize', digitByteSize);
const v = compiler.reserveVariable('v');
return `
if (elementType === ${BSONType.ARRAY}) {
${setter} = [];
parser.seek(4);
for (let i = 0; ; i++) {
const elementType = parser.eatByte();
if (elementType === 0) break;
//arrays are represented as objects, so we skip the key name
parser.seek(digitByteSize(i));
let ${v} = undefined;
${createPropertyConverter(v, property.getSubType(), compiler, property)}
${setter}.push(${v});
}
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'map') {
const name = compiler.reserveVariable('propertyName');
return `
if (elementType === ${BSONType.OBJECT}) {
${setter} = {};
parser.seek(4);
while (true) {
const elementType = parser.eatByte();
if (elementType === 0) break;
${name} = parser.eatObjectPropertyName();
${createPropertyConverter(`${setter}[${name}]`, property.getSubType(), compiler)}
}
} else {
${nullOrSeek}
}
`;
} else if (property.type === 'union') {
let discriminator: string[] = [`if (false) {\n}`];
const discriminants: string[] = [];
for (const unionType of getSortedUnionTypes(property, bsonTypeGuards)) {
discriminants.push(unionType.property.type);
}
for (const unionType of getSortedUnionTypes(property, bsonTypeGuards)) {
const guardVar = compiler.reserveVariable('guard_' + unionType.property.type, unionType.guard);
discriminator.push(`
//guard:${unionType.property.type}
else if (${guardVar}(elementType, parser)) {
${createPropertyConverter(setter, unionType.property, compiler, property)}
}
`);
}
return `
${discriminator.join('\n')}
else {
${nullOrSeek}
}
`;
}
return `
${nullCheck} else {
${setter} = parser.parse(elementType, ${propertyVar});
}
`;
}
interface DecoderFn {
buildId: number;
(parser: BaseParser, offset?: number): any;
}
function createSchemaDecoder(schema: ClassSchema): DecoderFn {
const compiler = new CompilerContext();
compiler.context.set('seekElementSize', seekElementSize);
const setProperties: string[] = [];
const constructorArgumentNames: string[] = [];
const constructorParameter = schema.getMethodProperties('constructor');
const resetDefaultSets: string[] = [];
const setDefaults: string[] = [];
const propertyCode: string[] = [];
for (const property of schema.getProperties()) {
//todo, support non-ascii names
const bufferCompare: string[] = [];
for (let i = 0; i < property.name.length; i++) {
bufferCompare.push(`parser.buffer[parser.offset + ${i}] === ${property.name.charCodeAt(i)}`);
}
bufferCompare.push(`parser.buffer[parser.offset + ${property.name.length}] === 0`);
const valueSetVar = compiler.reserveVariable('valueSetVar', false);
const defaultValue = property.defaultValue !== undefined ? compiler.reserveVariable('defaultValue', property.defaultValue) : 'undefined';
if (property.hasManualDefaultValue() || property.type === 'literal') {
resetDefaultSets.push(`${valueSetVar} = false;`)
if (property.defaultValue !== undefined) {
setDefaults.push(`if (!${valueSetVar}) object.${property.name} = ${defaultValue};`)
} else if (property.type === 'literal' && !property.isOptional) {
setDefaults.push(`if (!${valueSetVar}) object.${property.name} = ${JSON.stringify(property.literalValue)};`)
}
} else if (property.isNullable) {
resetDefaultSets.push(`${valueSetVar} = false;`)
setDefaults.push(`if (!${valueSetVar}) object.${property.name} = null;`)
}
const check = property.isOptional ? `${JSON.stringify(property.name)} in object` : `object.${property.name} !== undefined`;
if (property.methodName === 'constructor') {
constructorArgumentNames[constructorParameter.indexOf(property)] = `(${check} ? object.${property.name} : ${defaultValue})`;
} else {
if (property.isParentReference) {
throw new Error('Parent references not supported in BSON.');
} else {
setProperties.push(`if (${check}) _instance.${property.name} = object.${property.name};`);
}
}
propertyCode.push(`
//property ${property.name} (${property.toString()})
if (${bufferCompare.join(' && ')}) {
parser.offset += ${property.name.length} + 1;
${valueSetVar ? `${valueSetVar} = true;` : ''}
${createPropertyConverter(`object.${property.name}`, property, compiler)}
continue;
} `);
}
let instantiate = '';
if (schema.isCustomClass()) {
compiler.context.set('_classType', schema.classType);
instantiate = `
_instance = new _classType(${constructorArgumentNames.join(', ')});
${setProperties.join('\n')}
return _instance;
`;
}
const functionCode = `
var object = {};
${schema.isCustomClass() ? 'var _instance;' : ''}
const end = parser.eatUInt32() + parser.offset;
${resetDefaultSets.join('\n')}
while (parser.offset < end) {
const elementType = parser.eatByte();
if (elementType === 0) break;
${propertyCode.join('\n')}
//jump over this property when not registered in schema
while (parser.offset < end && parser.buffer[parser.offset++] != 0);
//seek property value
if (parser.offset >= end) break;
seekElementSize(elementType, parser);
}
${setDefaults.join('\n')}
${schema.isCustomClass() ? instantiate : 'return object;'}
`;
const fn = compiler.build(functionCode, 'parser', 'partial');
fn.buildId = schema.buildId;
return fn;
}
export function getRawBSONDecoder<T>(schema: ClassSchema<T> | ClassType<T>): (parser: BaseParser) => T {
schema = schema instanceof ClassSchema ? schema : getClassSchema(schema);
if (schema.jit.rawBson) return schema.jit.rawBson;
schema.jit.rawBson = createSchemaDecoder(schema);
return schema.jit.rawBson;
}
export type BSONDecoder<T> = (bson: Uint8Array, offset?: number) => T;
export function getBSONDecoder<T>(schema: ClassSchema<T> | ClassType<T>): BSONDecoder<T> {
const fn = getRawBSONDecoder(schema);
schema = schema instanceof ClassSchema ? schema : getClassSchema(schema);
if (schema.jit.bsonEncoder) return schema.jit.bsonEncoder;
schema.jit.bsonEncoder = (bson: Uint8Array, offset: number = 0) => {
return fn(new ParserV2(bson, offset));
};
return schema.jit.bsonEncoder;
}