Skip to content

Commit

Permalink
feat: type aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
cha0s committed Nov 27, 2024
1 parent b596112 commit c0c6ed2
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 61 deletions.
52 changes: 33 additions & 19 deletions README.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions src/codecs.js
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 23 additions & 0 deletions src/codecs.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
9 changes: 2 additions & 7 deletions src/codecs/array.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Codecs} from '../codecs.js';
import {resolveCodec} from '../codecs.js';

export function typeToElementClass(type) {
switch (type) {
Expand Down Expand Up @@ -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);
Expand Down
58 changes: 32 additions & 26 deletions src/codecs/object.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Codecs} from '../codecs.js';
import {resolveCodec} from '../codecs.js';
import BoolCodec from './bool.js';

class ObjectCodec {

Expand All @@ -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 {
`;
Expand All @@ -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}'});
Expand All @@ -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 += `
Expand All @@ -55,7 +53,7 @@ class ObjectCodec {
`;
encoderCode += '}';
}
this.$$codecs.push({codec, key, property});
this.$$codecs.push(codec);
i += 1;
}
if (this.$$booleans > 0) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}

}
Expand Down
22 changes: 20 additions & 2 deletions src/codecs/object.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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});
});

Expand All @@ -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: {}};
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -10,4 +10,4 @@ for (const path in codecs) {
Codecs[key] = codecs[path];
}

export {Codecs, Schema};
export {Aliases, Codecs, Schema};
7 changes: 2 additions & 5 deletions src/schema.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import {Codecs} from './codecs.js';
import {resolveCodec} from './codecs.js';

// Just an ergonomic wrapper around the root codec.
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}) {
Expand Down

0 comments on commit c0c6ed2

Please sign in to comment.