/* * 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 tap from 'tap' import * as fc from 'fast-check' import * as util from 'util' import { JSONObject } from '../../lib/json' import { getEncoding } from '../../lib/mapper' import { TypedKeysOptions, RequiredUnboundedTypedOptions, OptionalUnboundedTypedOptions, BoundedTypedOptions, UnboundedTypedOptions, RequiredBoundedTypedOptions, OptionalBoundedTypedOptions, PackedUnboundedOptions, PackedRequiredBoundedOptions } from '../../lib/encoder/object/options' import { ARBITRARY_TYPED_KEYS_OBJECT as ENCODE_ARBITRARY_TYPED_KEYS_OBJECT, NON_REQUIRED_BOUNDED_TYPED_OBJECT as ENCODE_NON_REQUIRED_BOUNDED_TYPED_OBJECT, REQUIRED_ONLY_BOUNDED_TYPED_OBJECT as ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT, MIXED_BOUNDED_TYPED_OBJECT as ENCODE_MIXED_BOUNDED_TYPED_OBJECT, REQUIRED_UNBOUNDED_TYPED_OBJECT as ENCODE_REQUIRED_UNBOUNDED_TYPED_OBJECT, OPTIONAL_UNBOUNDED_TYPED_OBJECT as ENCODE_OPTIONAL_UNBOUNDED_TYPED_OBJECT, MIXED_UNBOUNDED_TYPED_OBJECT as ENCODE_MIXED_UNBOUNDED_TYPED_OBJECT, PACKED_UNBOUNDED_OBJECT as ENCODE_PACKED_UNBOUNDED_OBJECT, PACKED_BOUNDED_REQUIRED_OBJECT as ENCODE_PACKED_BOUNDED_REQUIRED_OBJECT } from '../../lib/encoder/object/encode' import { ObjectResult, ARBITRARY_TYPED_KEYS_OBJECT as DECODE_ARBITRARY_TYPED_KEYS_OBJECT, NON_REQUIRED_BOUNDED_TYPED_OBJECT as DECODE_NON_REQUIRED_BOUNDED_TYPED_OBJECT, REQUIRED_ONLY_BOUNDED_TYPED_OBJECT as DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT, MIXED_BOUNDED_TYPED_OBJECT as DECODE_MIXED_BOUNDED_TYPED_OBJECT, REQUIRED_UNBOUNDED_TYPED_OBJECT as DECODE_REQUIRED_UNBOUNDED_TYPED_OBJECT, OPTIONAL_UNBOUNDED_TYPED_OBJECT as DECODE_OPTIONAL_UNBOUNDED_TYPED_OBJECT, MIXED_UNBOUNDED_TYPED_OBJECT as DECODE_MIXED_UNBOUNDED_TYPED_OBJECT, PACKED_UNBOUNDED_OBJECT as DECODE_PACKED_UNBOUNDED_OBJECT, PACKED_BOUNDED_REQUIRED_OBJECT as DECODE_PACKED_BOUNDED_REQUIRED_OBJECT } from '../../lib/encoder/object/decode' import { StringEncoding, getStringEncoding } from '../../lib/mapper/string' import { ResizableBuffer, EncodingContext, getDefaultEncodingContext, EncodingType } from '../../lib/encoder' tap.test('ARBITRARY_TYPED_KEYS_OBJECT: scalars values', (test) => { const options: TypedKeysOptions = { keyEncoding: getStringEncoding({ type: 'string' }, 1), encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } } fc.assert(fc.property(fc.nat(10), fc.dictionary(fc.string(), fc.oneof( fc.constant(null), fc.boolean(), fc.integer(), fc.float(), fc.double(), fc.string({ maxLength: 10 }) )), (offset: number, value: JSONObject): boolean => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(2048)) const bytesWritten: number = ENCODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, offset, value, options, context) const result: ObjectResult = DECODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, offset, options) return bytesWritten > 0 && result.bytes === bytesWritten && util.isDeepStrictEqual(result.value, value) }), { verbose: false }) test.end() }) tap.test('ARBITRARY_TYPED_KEYS_OBJECT: untyped {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(16)) const value: JSONObject = { foo: 'bar', baz: 1 } const keyEncoding: StringEncoding = getStringEncoding({ type: 'string' }, 1) const bytesWritten: number = ENCODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, 0, value, { keyEncoding, encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } }, context) const result: ObjectResult = DECODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, 0, { keyEncoding, encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } }) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('ARBITRARY_TYPED_KEYS_OBJECT: typed {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(16)) const value: JSONObject = { foo: 'bar', baz: 1 } const keyEncoding: StringEncoding = getStringEncoding({ type: 'string', minLength: 3 }, 1) const bytesWritten: number = ENCODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, 0, value, { keyEncoding, encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } }, context) const result: ObjectResult = DECODE_ARBITRARY_TYPED_KEYS_OBJECT( buffer, 0, { keyEncoding, encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } }) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('NON_REQUIRED_BOUNDED_TYPED_OBJECT: typed {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(7)) const value: JSONObject = { foo: 'bar', baz: 1 } const options: OptionalBoundedTypedOptions = { optionalProperties: [ 'baz', 'bar', 'foo', 'qux' ], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), baz: getEncoding({ type: 'integer', minimum: 0 }, 1), bar: getEncoding({}, 1), qux: getEncoding({}, 1) } } const bytesWritten: number = ENCODE_NON_REQUIRED_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_NON_REQUIRED_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: typed {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(5)) const value: JSONObject = { foo: 'bar', baz: 1 } const options: RequiredBoundedTypedOptions = { requiredProperties: [ 'baz', 'foo' ], booleanRequiredProperties: [], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), baz: getEncoding({ type: 'integer', minimum: 0 }, 1) } } const bytesWritten: number = ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('MIXED_BOUNDED_TYPED_OBJECT: typed {foo:"bar",baz:1} with one required', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(7)) const value: JSONObject = { foo: 'bar', baz: 1 } const options: BoundedTypedOptions = { requiredProperties: [ 'foo' ], booleanRequiredProperties: [], optionalProperties: [ 'baz' ], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), baz: getEncoding({ type: 'integer', minimum: 0 }, 1) } } const bytesWritten: number = ENCODE_MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, value, options, context) const result: ObjectResult = DECODE_MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('MIXED_BOUNDED_TYPED_OBJECT: {foo:"bar",baz:1} with one missing optional', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(6)) const value: JSONObject = { foo: 'bar' } const options: BoundedTypedOptions = { requiredProperties: [ 'foo' ], booleanRequiredProperties: [], optionalProperties: [ 'baz' ], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), baz: getEncoding({ type: 'integer', minimum: 0 }, 1) } } const bytesWritten: number = ENCODE_MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, value, options, context) const result: ObjectResult = DECODE_MIXED_BOUNDED_TYPED_OBJECT(buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_UNBOUNDED_TYPED_OBJECT: semityped {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(11)) const value: JSONObject = { foo: 'bar', baz: 1 } const options: RequiredUnboundedTypedOptions = { requiredProperties: [ 'foo' ], booleanRequiredProperties: [], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1) }, keyEncoding: getStringEncoding({ type: 'string' }, 1), encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } } const bytesWritten: number = ENCODE_REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_UNBOUNDED_TYPED_OBJECT: typed {foo:"bar"}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(5)) const value: JSONObject = { foo: 'bar' } const options: RequiredUnboundedTypedOptions = { requiredProperties: [ 'foo' ], booleanRequiredProperties: [], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1) }, keyEncoding: getStringEncoding({ type: 'string' }, 1), encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } } const bytesWritten: number = ENCODE_REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_UNBOUNDED_TYPED_OBJECT(buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('OPTIONAL_UNBOUNDED_TYPED_OBJECT: semityped {foo:"bar",baz:1}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(13)) const value: JSONObject = { foo: 'bar', baz: 1 } const options: OptionalUnboundedTypedOptions = { optionalProperties: [ 'foo' ], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1) }, keyEncoding: getStringEncoding({ type: 'string' }, 1), encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} } } const bytesWritten: number = ENCODE_OPTIONAL_UNBOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_OPTIONAL_UNBOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('MIXED_UNBOUNDED_TYPED_OBJECT: mixed {foo:"bar",baz:1,qux:null}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(13)) const value: JSONObject = { foo: 'bar', baz: 1, qux: null } const options: UnboundedTypedOptions = { requiredProperties: [ 'foo' ], booleanRequiredProperties: [], optionalProperties: [ 'baz' ], keyEncoding: getStringEncoding({ type: 'string' }, 1), encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} }, propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), baz: getEncoding({ type: 'integer', minimum: 0 }, 1) } } const bytesWritten: number = ENCODE_MIXED_UNBOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_MIXED_UNBOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: typed {foo:"bar",baz:1,baz:true,qux:false}', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(6)) const value: JSONObject = { foo: 'bar', bar: 1, baz: true, qux: false } const options: RequiredBoundedTypedOptions = { requiredProperties: [ 'bar', 'foo' ], booleanRequiredProperties: [ 'baz', 'qux' ], propertyEncodings: { foo: getEncoding({ type: 'string' }, 1), bar: getEncoding({ type: 'integer', minimum: 0 }, 1), baz: getEncoding({ type: 'boolean' }, 1), qux: getEncoding({ type: 'boolean' }, 1) } } const bytesWritten: number = ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: three boolean properties', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(1)) const value: JSONObject = { foo: true, bar: false, baz: true } const options: RequiredBoundedTypedOptions = { requiredProperties: [], booleanRequiredProperties: [ 'foo', 'bar', 'baz' ], propertyEncodings: { foo: getEncoding({ type: 'boolean' }, 1), bar: getEncoding({ type: 'boolean' }, 1), baz: getEncoding({ type: 'boolean' }, 1) } } const bytesWritten: number = ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: eight boolean properties', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(1)) const value: JSONObject = { foo: true, bar: false, baz: true, qux: true, xxx: false, yyy: false, zzz: true, qqq: true } const options: RequiredBoundedTypedOptions = { requiredProperties: [], booleanRequiredProperties: [ 'foo', 'bar', 'baz', 'qux', 'xxx', 'yyy', 'zzz', 'qqq' ], propertyEncodings: { foo: getEncoding({ type: 'boolean' }, 1), bar: getEncoding({ type: 'boolean' }, 1), baz: getEncoding({ type: 'boolean' }, 1), qux: getEncoding({ type: 'boolean' }, 1), xxx: getEncoding({ type: 'boolean' }, 1), yyy: getEncoding({ type: 'boolean' }, 1), zzz: getEncoding({ type: 'boolean' }, 1), qqq: getEncoding({ type: 'boolean' }, 1) } } const bytesWritten: number = ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('REQUIRED_ONLY_BOUNDED_TYPED_OBJECT: nine boolean properties', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(2)) const value: JSONObject = { foo: true, bar: false, baz: true, qux: true, xxx: false, yyy: false, zzz: true, qqq: true, ppp: false } const options: RequiredBoundedTypedOptions = { requiredProperties: [], booleanRequiredProperties: [ 'foo', 'bar', 'baz', 'qux', 'xxx', 'yyy', 'zzz', 'qqq', 'ppp' ], propertyEncodings: { foo: getEncoding({ type: 'boolean' }, 1), bar: getEncoding({ type: 'boolean' }, 1), baz: getEncoding({ type: 'boolean' }, 1), qux: getEncoding({ type: 'boolean' }, 1), xxx: getEncoding({ type: 'boolean' }, 1), yyy: getEncoding({ type: 'boolean' }, 1), zzz: getEncoding({ type: 'boolean' }, 1), qqq: getEncoding({ type: 'boolean' }, 1), ppp: getEncoding({ type: 'boolean' }, 1) } } const bytesWritten: number = ENCODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_REQUIRED_ONLY_BOUNDED_TYPED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('PACKED_UNBOUNDED_OBJECT: complex object', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(10)) const value: JSONObject = { foo: 1, bar: 2, baz: 0, qux: 2, extra: 1, name: 'john', flag: true, random: 1 } const options: PackedUnboundedOptions = { packedRequiredProperties: [ 'bar', 'baz', 'extra', 'foo', 'qux' ], packedEncoding: { type: EncodingType.Integer, encoding: 'BOUNDED_MULTIPLE_8BITS_ENUM_FIXED', options: { minimum: 0, maximum: 2, multiplier: 1 } }, encoding: { type: EncodingType.Any, encoding: 'ANY_PACKED_TYPE_TAG_BYTE_PREFIX', options: {} }, propertyEncodings: { name: getEncoding({ type: 'string' }, 1), age: getEncoding({ type: 'integer', minimum: 0 }, 1), flag: getEncoding({ type: 'boolean' }, 1) }, optionalProperties: [ 'age' ], requiredProperties: [ 'name' ], booleanRequiredProperties: [ 'flag' ], keyEncoding: getStringEncoding({ type: 'string' }, 1) } const bytesWritten: number = ENCODE_PACKED_UNBOUNDED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_PACKED_UNBOUNDED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() }) tap.test('PACKED_BOUNDED_REQUIRED_OBJECT: complex object', (test) => { const context: EncodingContext = getDefaultEncodingContext() const buffer: ResizableBuffer = new ResizableBuffer(Buffer.allocUnsafe(9)) const value: JSONObject = { foo: 1, bar: 2, baz: 0, qux: 2, extra: 1, name: 'john', flag: true } const options: PackedRequiredBoundedOptions = { packedRequiredProperties: [ 'bar', 'baz', 'extra', 'foo', 'qux' ], packedEncoding: { type: EncodingType.Integer, encoding: 'BOUNDED_MULTIPLE_8BITS_ENUM_FIXED', options: { minimum: 0, maximum: 2, multiplier: 1 } }, propertyEncodings: { name: getEncoding({ type: 'string' }, 1), flag: getEncoding({ type: 'boolean' }, 1) }, requiredProperties: [ 'name' ], booleanRequiredProperties: [ 'flag' ] } const bytesWritten: number = ENCODE_PACKED_BOUNDED_REQUIRED_OBJECT( buffer, 0, value, options, context) const result: ObjectResult = DECODE_PACKED_BOUNDED_REQUIRED_OBJECT( buffer, 0, options) test.is(bytesWritten, result.bytes) test.strictSame(result.value, value) test.end() })