From c0c6ed2429be2a4afcc7314669aa32fe573ad89d Mon Sep 17 00:00:00 2001 From: cha0s Date: Tue, 26 Nov 2024 18:33:45 -0600 Subject: [PATCH] feat: type aliases --- README.md | 52 ++++++++++++++++++++++------------- src/codecs.js | 22 +++++++++++++++ src/codecs.test.js | 23 ++++++++++++++++ src/codecs/array.js | 9 ++---- src/codecs/object.js | 58 +++++++++++++++++++++------------------ src/codecs/object.test.js | 22 +++++++++++++-- src/index.js | 4 +-- src/schema.js | 7 ++--- 8 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 src/codecs.test.js diff --git a/README.md b/README.md index afba9b6..fc87076 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # crunches :muscle: -The smallest **and** fastest JavaScript web standards-compliant value serialization library in the wild. **3.06 kB** gzipped; **0 dependencies**. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets. +The smallest **and** fastest JavaScript web standards-compliant value serialization library in the wild. **3.24 kB** gzipped; **0 dependencies**. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets. ## Example @@ -205,24 +205,40 @@ Inside your codec, you must increment `target.byteOffset` as you decode bytes. Just set a key on the `Codecs` object and go. Too easy! +### Type aliases + +You may add type aliases: + +```js +import {Aliases} from 'crunches'; + +Aliases.foobar = 'bool'; + +const schema = new Schema({ + type: 'foobar', +}); + +console.log(schema.size()); // 1, because it's a bool. +``` + ## Primitive types -| Type Name | Bytes | Range of Values | -|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| -| bool | 1 (worst case, see [boolean coalescence](#boolean-coalescence)) | Truthy values are coerced to `true`; falsy values to `false` | -| int8 | 1 | -128 to 127 | -| uint8 | 1 | 0 to 255 | -| int16 | 2 | -32,768 to 32,767 | -| uint16 | 2 | 0 to 65,535 | -| int32 | 4 | -2,147,483,648 to 2,147,483,647 | -| uint32 | 4 | 0 to 4,294,967,295 | -| float32 | 4 | 3.4E +/- 38 (7 digits) | -| float64 | 8 | 1.7E +/- 308 (15 digits) | -| string | [Prefix](#varuint-prefixes) followed by the [encoded](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto) string bytes | Any string | -| buffer | [Prefix](#varuint-prefixes) followed by the bytes of the buffer | Any `ArrayBuffer`

Decodes to a `DataView`.

See: [buffers and arrays](#buffers-and-arrays). | -| varuint |
sizeminmax
10127
212816,383
316,3842,097,151
42,097,152268,435,455
5268,435,45634,359,738,367
634,359,738,3684,398,046,511,103
74,398,046,511,104562,949,953,421,311
| 0 to 562,949,953,421,311 | -| varint |
sizeminmax
1-6463
2-8,1928,191
3-1,048,5761,048,575
4-134,217,728134,217,727
5-17,179,869,18417,179,869,183
6-2,199,023,255,5522,199,023,255,551
7-281,474,976,710,656281,474,976,710,655
| -281,474,976,710,656 to 281,474,976,710,655 | -| date | Same as `string` above after calling [`toIsoString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) | Value is coerced to `Date` e.g. `new Date(value).toIsoString()` | +| Type Name | Bytes | Range of Values | +|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| bool (alias: boolean) | 1 (worst case, see [boolean coalescence](#boolean-coalescence)) | Truthy values are coerced to `true`; falsy values to `false` | +| int8 | 1 | -128 to 127 | +| uint8 | 1 | 0 to 255 | +| int16 | 2 | -32,768 to 32,767 | +| uint16 | 2 | 0 to 65,535 | +| int32 | 4 | -2,147,483,648 to 2,147,483,647 | +| uint32 | 4 | 0 to 4,294,967,295 | +| float32 | 4 | 3.4E +/- 38 (7 digits) | +| float64 | 8 | 1.7E +/- 308 (15 digits) | +| string | [Prefix](#varuint-prefixes) followed by the [encoded](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto) string bytes | Any string | +| buffer | [Prefix](#varuint-prefixes) followed by the bytes of the buffer | Any `ArrayBuffer`

Decodes to a `DataView`.

See: [buffers and arrays](#buffers-and-arrays). | +| varuint |
sizeminmax
10127
212816,383
316,3842,097,151
42,097,152268,435,455
5268,435,45634,359,738,367
634,359,738,3684,398,046,511,103
74,398,046,511,104562,949,953,421,311
| 0 to 562,949,953,421,311 | +| varint |
sizeminmax
1-6463
2-8,1928,191
3-1,048,5761,048,575
4-134,217,728134,217,727
5-17,179,869,18417,179,869,183
6-2,199,023,255,5522,199,023,255,551
7-281,474,976,710,656281,474,976,710,655
| -281,474,976,710,656 to 281,474,976,710,655 | +| date | Same as `string` above after calling [`toIsoString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) | Value is coerced to `Date` e.g. `new Date(value).toIsoString()` | ## Aggregate types @@ -376,8 +392,6 @@ Instead of copying the data from the buffer, a [`TypedArray`](https://developer. # TODO -- Coalescence for boolean arrays? -- Type aliases? - BigInts? - Endianness? diff --git a/src/codecs.js b/src/codecs.js index 56265b6..68de7cf 100644 --- a/src/codecs.js +++ b/src/codecs.js @@ -1 +1,23 @@ +export const Aliases = { + boolean: 'bool', +}; + export const Codecs = {}; + +export function resolveCodec(blueprint) { + let {type} = blueprint; + if (undefined === type) { + throw new TypeError("No codec specified. Did you forget to include a 'type' key in your schema blueprint?"); + } + const searched = new Set([type]); + let Codec = Codecs[type]; + while (!Codec) { + type = Aliases[type]; + if (!type || searched.has(type)) { + throw new TypeError(`Codec not found: '${blueprint.type}'`); + } + searched.add(type) + Codec = Codecs[type]; + } + return new Codecs[type](blueprint); +} diff --git a/src/codecs.test.js b/src/codecs.test.js new file mode 100644 index 0000000..65415fe --- /dev/null +++ b/src/codecs.test.js @@ -0,0 +1,23 @@ +import {expect, test} from 'vitest'; + +import BoolCodec from './codecs/bool.js'; +import {Aliases, Codecs, resolveCodec} from './codecs.js'; + +Codecs.bool = BoolCodec; + +test('resolve', async () => { + expect(resolveCodec({type: 'bool'})).toBeInstanceOf(BoolCodec); +}); + +test('aliases', async () => { + expect(resolveCodec({type: 'boolean'})).toBeInstanceOf(BoolCodec); + Aliases.booboo = 'boolean'; + expect(resolveCodec({type: 'booboo'})).toBeInstanceOf(BoolCodec); +}); + +test('alias cycle', async () => { + expect(resolveCodec({type: 'boolean'})).toBeInstanceOf(BoolCodec); + Aliases.foofoo = 'bar'; + Aliases.bar = 'foofoo'; + expect(() => resolveCodec({type: 'foofoo'})).toThrowError(); +}); diff --git a/src/codecs/array.js b/src/codecs/array.js index 36145c4..5a2fa08 100644 --- a/src/codecs/array.js +++ b/src/codecs/array.js @@ -1,4 +1,4 @@ -import {Codecs} from '../codecs.js'; +import {resolveCodec} from '../codecs.js'; export function typeToElementClass(type) { switch (type) { @@ -27,12 +27,7 @@ class ArrayCodec { $$elementCodec; constructor(blueprint) { - if (!(blueprint.element.type in Codecs)) { - throw new TypeError(`No such codec '${blueprint.element.type}'`); - } - // todo: throw on optional or honor and encode sparse arrays - // todo: boolean coalescence - this.$$elementCodec = new Codecs[blueprint.element.type](blueprint.element); + this.$$elementCodec = resolveCodec(blueprint.element); const {length = 0} = blueprint; const {type} = blueprint.element; const ElementClass = typeToElementClass(type); diff --git a/src/codecs/object.js b/src/codecs/object.js index 7028746..4f6bc36 100644 --- a/src/codecs/object.js +++ b/src/codecs/object.js @@ -1,4 +1,5 @@ -import {Codecs} from '../codecs.js'; +import {resolveCodec} from '../codecs.js'; +import BoolCodec from './bool.js'; class ObjectCodec { @@ -14,15 +15,12 @@ class ObjectCodec { let i = 0; for (const key in blueprint.properties) { const property = blueprint.properties[key]; - if (!(property.type in Codecs)) { - throw new TypeError(`No such codec '${property.type}'`); - } - const codec = new Codecs[property.type](property); + const codec = resolveCodec(property); if (property.optional) { this.$$optionals += 1; decoderCode += ` if (!(optionalFlags[currentOptional >> 3] & (1 << (currentOptional & 7)))) { - ${'bool' === property.type ? '$$booleans -= 1' : ''} + ${(codec instanceof BoolCodec) ? '$$booleans -= 1' : ''} } else { `; @@ -33,7 +31,7 @@ class ObjectCodec { if (isPresent) { `; } - if ('bool' === property.type) { + if (codec instanceof BoolCodec) { this.$$booleans += 1; decoderCode += ` booleanBackpatches.push({bit: currentBoolean & 7, index: currentBoolean >> 3, key: '${key}'}); @@ -45,8 +43,8 @@ class ObjectCodec { `; } else { - decoderCode += `value['${key}'] = this.$$codecs[${i}].codec.decode(view, target);`; - encoderCode += `written += this.$$codecs[${i}].codec.encode(value['${key}'], view, byteOffset + written);`; + decoderCode += `value['${key}'] = this.$$codecs[${i}].decode(view, target);`; + encoderCode += `written += this.$$codecs[${i}].encode(value['${key}'], view, byteOffset + written);`; } if (property.optional) { decoderCode += ` @@ -55,7 +53,7 @@ class ObjectCodec { `; encoderCode += '}'; } - this.$$codecs.push({codec, key, property}); + this.$$codecs.push(codec); i += 1; } if (this.$$booleans > 0) { @@ -119,6 +117,29 @@ class ObjectCodec { decoderCode += 'return value'; this.$$decode = new Function('view, target', decoderCode); this.$$encode = new Function('value, view, byteOffset', encoderCode); + this.$$size = (value) => { + let {$$booleans} = this; + let size = 0; + size += Math.ceil(this.$$optionals / 8); + let i = 0; + for (const key in blueprint.properties) { + const codec = this.$$codecs[i]; + const property = blueprint.properties[key]; + if (property.optional && 'undefined' === typeof value[key]) { + if (codec instanceof BoolCodec) { + $$booleans -= 1; + } + i += 1; + continue; + } + if (!(codec instanceof BoolCodec)) { + size += codec.size(value[key]); + } + i += 1; + } + size += Math.ceil($$booleans / 8); + return size; + }; } decode(view, target) { @@ -130,22 +151,7 @@ class ObjectCodec { } size(value) { - let {$$booleans} = this; - let size = 0; - size += Math.ceil(this.$$optionals / 8); - for (const {codec, key, property} of this.$$codecs) { - if (property.optional && 'undefined' === typeof value[key]) { - if ('bool' === property.type) { - $$booleans -= 1; - } - continue; - } - if ('bool' !== property.type) { - size += codec.size(value[key]); - } - } - size += Math.ceil($$booleans / 8); - return size; + return this.$$size(value); } } diff --git a/src/codecs/object.test.js b/src/codecs/object.test.js index 389a53c..bf450b1 100644 --- a/src/codecs/object.test.js +++ b/src/codecs/object.test.js @@ -2,7 +2,7 @@ import {expect, test} from 'vitest'; import {Codecs} from '../codecs.js'; import Codec from './object.js'; -import BoolCodec from './uint8.js'; +import BoolCodec from './bool.js'; import Uint8Codec from './uint8.js'; Codecs.bool = BoolCodec; @@ -17,7 +17,7 @@ test('object', async () => { }, }); const view = new DataView(new ArrayBuffer(codec.size({1: 32, 2: 32}))); - codec.encode({1: 32, 2: 32}, view, 0); + expect(codec.encode({1: 32, 2: 32}, view, 0)).to.equal(2); expect(codec.decode(view, {byteOffset: 0})).to.deep.equal({1: 32, 2: 32}); }); @@ -39,6 +39,24 @@ test('object boolean coalescence', async () => { expect(codec.size(value)).to.equal(2); }); +test('object aliased boolean coalescence', async () => { + let codec; + const blueprint = {properties: {}}; + const value = {}; + for (let i = 0; i < 8; ++i) { + blueprint.properties[i] = {type: 'boolean'}; + value[i] = i; + } + codec = new Codec(blueprint); + expect(codec.size(value)).to.equal(1); + for (let i = 8; i < 16; ++i) { + blueprint.properties[i] = {type: 'boolean'}; + value[i] = i; + } + codec = new Codec(blueprint); + expect(codec.size(value)).to.equal(2); +}); + test('object optional coalescence', async () => { let codec; const blueprint = {properties: {}}; diff --git a/src/index.js b/src/index.js index 7091687..c9cafd0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import {Codecs} from './codecs.js'; +import {Aliases, Codecs} from './codecs.js'; import Schema from './schema.js'; const codecs = import.meta.glob( @@ -10,4 +10,4 @@ for (const path in codecs) { Codecs[key] = codecs[path]; } -export {Codecs, Schema}; +export {Aliases, Codecs, Schema}; diff --git a/src/schema.js b/src/schema.js index 3c72b19..57214c2 100644 --- a/src/schema.js +++ b/src/schema.js @@ -1,4 +1,4 @@ -import {Codecs} from './codecs.js'; +import {resolveCodec} from './codecs.js'; // Just an ergonomic wrapper around the root codec. class Schema { @@ -6,10 +6,7 @@ class Schema { $$codec; constructor(blueprint) { - if (!(blueprint.type in Codecs)) { - throw new TypeError(`No such codec '${blueprint.type}'`); - } - this.$$codec = new Codecs[blueprint.type](blueprint); + this.$$codec = resolveCodec(blueprint); } decode(view, target = {byteOffset: 0}) {