jsonbinpack
Version:
A space-efficient schema-driven binary JSON serialization format based on JSON Schema
344 lines (289 loc) • 11.7 kB
text/typescript
/*
* Copyright 2021 Juan Cruz Viotti
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
strict as assert
} from 'assert'
import ResizableBuffer from '../resizable-buffer'
import {
bitsetEncode
} from './bitset'
import {
integerListEncode
} from './integer-list'
import {
JSONObject,
JSONValue
} from '../../json'
import {
encode
} from '../index'
import {
Encoding
} from '../../mapper'
import {
TypedKeysOptions,
BoundedTypedOptions,
UnboundedTypedOptions,
RequiredUnboundedTypedOptions,
OptionalBoundedTypedOptions,
OptionalUnboundedTypedOptions,
RequiredBoundedTypedOptions,
PackedUnboundedOptions,
PackedRequiredBoundedOptions,
SizeTypedKeysOptions
} from './options'
import {
FLOOR_MULTIPLE_ENUM_VARINT
} from '../integer/encode'
import {
EncodingContext
} from '../context'
export const REQUIRED_ONLY_BOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject,
options: RequiredBoundedTypedOptions, context: EncodingContext
): number => {
assert(Object.keys(value).length ===
options.requiredProperties.length + options.booleanRequiredProperties.length)
const booleanBits: boolean[] = []
for (const key of options.booleanRequiredProperties) {
const bit: JSONValue = value[key]
assert(typeof bit === 'boolean')
assert(typeof options.propertyEncodings[key] !== 'undefined')
booleanBits.push(bit)
}
const booleanBytes: number = bitsetEncode(buffer, offset, booleanBits)
let cursor: number = offset + booleanBytes
for (const key of options.requiredProperties) {
const encoding: Encoding | undefined = options.propertyEncodings[key]
assert(typeof encoding !== 'undefined')
cursor += encode(buffer, cursor, encoding, value[key], context)
}
return cursor - offset
}
export const NON_REQUIRED_BOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: OptionalBoundedTypedOptions, context: EncodingContext
): number => {
assert(Object.keys(value).length <= options.optionalProperties.length)
const lengthBytes: number = FLOOR_MULTIPLE_ENUM_VARINT(
buffer, offset, options.optionalProperties.length, {
minimum: 0,
multiplier: 1
}, context)
const keys: string[] = []
const bitset: boolean[] = []
for (const property of options.optionalProperties) {
const isPropertySet: boolean = typeof value[property] !== 'undefined'
bitset.push(isPropertySet)
if (isPropertySet) {
keys.push(property)
}
}
const bitsetBytes: number = bitsetEncode(buffer, offset + lengthBytes, bitset)
let cursor = offset + lengthBytes + bitsetBytes
for (const key of keys) {
const encoding: Encoding | undefined = options.propertyEncodings[key]
assert(typeof encoding !== 'undefined')
const bytesWritten: number = encode(buffer, cursor, encoding, value[key], context)
cursor += bytesWritten
}
return cursor - offset
}
export const MIXED_BOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: BoundedTypedOptions, context: EncodingContext
): number => {
assert(Object.keys(value).length <=
options.booleanRequiredProperties.length +
options.requiredProperties.length +
options.optionalProperties.length)
const requiredSubset: JSONObject = {}
for (const key of options.booleanRequiredProperties.concat(options.requiredProperties)) {
Reflect.set(requiredSubset, key, value[key])
}
const optionalSubset: JSONObject = {}
for (const key of options.optionalProperties) {
Reflect.set(optionalSubset, key, value[key])
}
const requiredBytes: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(
buffer, offset, requiredSubset, {
propertyEncodings: options.propertyEncodings,
requiredProperties: options.requiredProperties,
booleanRequiredProperties: options.booleanRequiredProperties
}, context)
return requiredBytes + NON_REQUIRED_BOUNDED_TYPED_OBJECT(
buffer, offset + requiredBytes, optionalSubset, {
propertyEncodings: options.propertyEncodings,
optionalProperties: options.optionalProperties
}, context)
}
export const ARBITRARY_TYPED_KEYS_OBJECT_WITHOUT_LENGTH = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: SizeTypedKeysOptions, context: EncodingContext
): number => {
let cursor: number = offset
for (const [ key, objectValue ] of Object.entries(value)) {
cursor += encode(buffer, cursor, options.keyEncoding, key, context)
cursor += encode(buffer, cursor, options.encoding, objectValue, context)
}
return cursor - offset
}
export const ARBITRARY_TYPED_KEYS_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: TypedKeysOptions, context: EncodingContext
): number => {
const size: number = Object.keys(value).length
const lengthBytes: number = FLOOR_MULTIPLE_ENUM_VARINT(buffer, offset, size, {
minimum: 0,
multiplier: 1
}, context)
return lengthBytes + ARBITRARY_TYPED_KEYS_OBJECT_WITHOUT_LENGTH(
buffer, offset + lengthBytes, value, {
encoding: options.encoding,
keyEncoding: options.keyEncoding,
size
}, context)
}
export const REQUIRED_UNBOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: RequiredUnboundedTypedOptions, context: EncodingContext
): number => {
assert(options.booleanRequiredProperties.length + options.requiredProperties.length > 0)
const required: Set<string> =
new Set<string>(options.booleanRequiredProperties.concat(options.requiredProperties))
const requiredSubset: JSONObject = {}
const rest: JSONObject = {}
for (const key of Object.keys(value)) {
Reflect.set(required.has(key) ? requiredSubset : rest, key, value[key])
}
const requiredBytes: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(
buffer, offset, requiredSubset, {
propertyEncodings: options.propertyEncodings,
requiredProperties: options.requiredProperties,
booleanRequiredProperties: options.booleanRequiredProperties
}, context)
return requiredBytes + ARBITRARY_TYPED_KEYS_OBJECT(buffer, offset + requiredBytes, rest, {
keyEncoding: options.keyEncoding,
encoding: options.encoding
}, context)
}
export const OPTIONAL_UNBOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: OptionalUnboundedTypedOptions, context: EncodingContext
): number => {
assert(Object.keys(options.propertyEncodings).length === options.optionalProperties.length)
const optional: Set<string> = new Set<string>(options.optionalProperties)
const optionalSubset: JSONObject = {}
const rest: JSONObject = {}
for (const key of Object.keys(value)) {
Reflect.set(optional.has(key) ? optionalSubset : rest, key, value[key])
}
const optionalBytes: number = NON_REQUIRED_BOUNDED_TYPED_OBJECT(
buffer, offset, optionalSubset, {
propertyEncodings: options.propertyEncodings,
optionalProperties: options.optionalProperties
}, context)
return optionalBytes + ARBITRARY_TYPED_KEYS_OBJECT(buffer, offset + optionalBytes, rest, {
keyEncoding: options.keyEncoding,
encoding: options.encoding
}, context)
}
export const MIXED_UNBOUNDED_TYPED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject, options: UnboundedTypedOptions, context: EncodingContext
): number => {
const required: Set<string> =
new Set<string>(options.booleanRequiredProperties.concat(options.requiredProperties))
const optional: Set<string> = new Set<string>(options.optionalProperties)
const requiredSubset: JSONObject = {}
const optionalSubset: JSONObject = {}
const rest: JSONObject = {}
for (const key of Object.keys(value)) {
if (required.has(key)) {
Reflect.set(requiredSubset, key, value[key])
} else if (optional.has(key)) {
Reflect.set(optionalSubset, key, value[key])
} else {
Reflect.set(rest, key, value[key])
}
}
const requiredBytes: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(
buffer, offset, requiredSubset, {
propertyEncodings: options.propertyEncodings,
requiredProperties: options.requiredProperties,
booleanRequiredProperties: options.booleanRequiredProperties
}, context)
const optionalBytes: number = NON_REQUIRED_BOUNDED_TYPED_OBJECT(
buffer, offset + requiredBytes, optionalSubset, {
propertyEncodings: options.propertyEncodings,
optionalProperties: options.optionalProperties
}, context)
return requiredBytes + optionalBytes + ARBITRARY_TYPED_KEYS_OBJECT(
buffer, offset + requiredBytes + optionalBytes, rest, {
keyEncoding: options.keyEncoding,
encoding: options.encoding
}, context)
}
export const PACKED_UNBOUNDED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject,
options: PackedUnboundedOptions, context: EncodingContext
): number => {
const packedValues: number[] = []
const unpackedSubset: JSONObject = Object.assign({}, value)
for (const key of options.packedRequiredProperties) {
const integer: JSONValue = value[key]
assert(typeof integer === 'number')
packedValues.push(integer)
Reflect.deleteProperty(unpackedSubset, key)
}
const packedLengthBytes: number = FLOOR_MULTIPLE_ENUM_VARINT(
buffer, offset, packedValues.length, {
minimum: 0,
multiplier: 1
}, context)
const packedBytes: number = integerListEncode(
buffer, offset + packedLengthBytes, packedValues, {
minimum: options.packedEncoding.options.minimum,
maximum: options.packedEncoding.options.maximum
})
return packedLengthBytes + packedBytes + MIXED_UNBOUNDED_TYPED_OBJECT(
buffer, offset + packedLengthBytes + packedBytes, unpackedSubset, {
requiredProperties: options.requiredProperties,
booleanRequiredProperties: options.booleanRequiredProperties,
optionalProperties: options.optionalProperties,
keyEncoding: options.keyEncoding,
encoding: options.encoding,
propertyEncodings: options.propertyEncodings
}, context)
}
export const PACKED_BOUNDED_REQUIRED_OBJECT = (
buffer: ResizableBuffer, offset: number, value: JSONObject,
options: PackedRequiredBoundedOptions, context: EncodingContext
): number => {
const packedValues: number[] = []
const unpackedSubset: JSONObject = Object.assign({}, value)
for (const key of options.packedRequiredProperties) {
const integer: JSONValue = value[key]
assert(typeof integer === 'number')
packedValues.push(integer)
Reflect.deleteProperty(unpackedSubset, key)
}
const packedBytes: number = integerListEncode(
buffer, offset, packedValues, {
minimum: options.packedEncoding.options.minimum,
maximum: options.packedEncoding.options.maximum
})
const requiredBytes: number = REQUIRED_ONLY_BOUNDED_TYPED_OBJECT(
buffer, offset + packedBytes, unpackedSubset, {
propertyEncodings: options.propertyEncodings,
requiredProperties: options.requiredProperties,
booleanRequiredProperties: options.booleanRequiredProperties
}, context)
return packedBytes + requiredBytes
}