feat: map

This commit is contained in:
cha0s 2024-06-24 04:19:54 -05:00
parent 15ca775611
commit 9fe10408f7
2 changed files with 124 additions and 45 deletions

View File

@ -53,6 +53,16 @@ export default class Schema {
} }
return value; return value;
} }
case 'map': {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const value = {};
for (let i = 0; i < length; ++i) {
const key = this.deserialize(view, offset, {type: 'string'});
value[key] = this.deserialize(view, offset, specification.value);
}
return value;
}
case 'object': { case 'object': {
const value = {}; const value = {};
for (const key in specification.properties) { for (const key in specification.properties) {
@ -92,6 +102,9 @@ export default class Schema {
case 'array': { case 'array': {
return []; return [];
} }
case 'map': {
return {};
}
case 'object': { case 'object': {
const object = {}; const object = {};
for (const key in specification.properties) { for (const key in specification.properties) {
@ -109,10 +122,6 @@ export default class Schema {
return this.constructor.defaultValue(this.specification); return this.constructor.defaultValue(this.specification);
} }
has(key) {
return key in this.specification;
}
static normalize(specification) { static normalize(specification) {
let normalized = specification; let normalized = specification;
switch (specification.type) { switch (specification.type) {
@ -120,6 +129,10 @@ export default class Schema {
normalized.subtype = this.normalize(specification.subtype); normalized.subtype = this.normalize(specification.subtype);
break; break;
} }
case 'map': {
normalized.value = this.normalize(specification.value);
break;
}
case 'object': { case 'object': {
for (const key in specification.properties) { for (const key in specification.properties) {
normalized.properties[key] = this.normalize(specification.properties[key]) normalized.properties[key] = this.normalize(specification.properties[key])
@ -139,6 +152,7 @@ export default class Schema {
case 'string': { case 'string': {
break; break;
} }
/* v8 ignore next 2 */
default: default:
throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`); throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`);
} }
@ -154,8 +168,7 @@ export default class Schema {
switch (specification.type) { switch (specification.type) {
case 'array': { case 'array': {
view.setUint32(offset, source.length, true); view.setUint32(offset, source.length, true);
offset += 4; let arraySize = 4;
let arraySize = 0;
for (const element of source) { for (const element of source) {
arraySize += this.serialize( arraySize += this.serialize(
element, element,
@ -164,7 +177,27 @@ export default class Schema {
specification.subtype, specification.subtype,
); );
} }
return 4 + arraySize; return arraySize;
}
case 'map': {
const keys = Object.keys(source);
view.setUint32(offset, keys.length, true);
let mapSize = 4;
for (const key of keys) {
mapSize += this.serialize(
key,
view,
offset + mapSize,
{type: 'string'},
);
mapSize += this.serialize(
source[key],
view,
offset + mapSize,
specification.value,
);
}
return mapSize;
} }
case 'object': { case 'object': {
let objectSize = 0; let objectSize = 0;
@ -210,6 +243,14 @@ export default class Schema {
} }
break; break;
} }
case 'map': {
fullSize += 4;
for (const key in concrete) {
fullSize += this.sizeOf(key, {type: 'string'});
fullSize += this.sizeOf(concrete[key], specification.value);
}
break;
}
case 'object': { case 'object': {
for (const key in specification.properties) { for (const key in specification.properties) {
fullSize += this.sizeOf(concrete[key], specification.properties[key]); fullSize += this.sizeOf(concrete[key], specification.properties[key]);
@ -231,6 +272,7 @@ export default class Schema {
static size(specification) { static size(specification) {
switch (specification.type) { switch (specification.type) {
case 'array': return 0; case 'array': return 0;
case 'map': return 0; // TODO could be fixed-size w/ fixed-size key
case 'object': { case 'object': {
let size = 0; let size = 0;
for (const key in specification.properties) { for (const key in specification.properties) {
@ -258,8 +300,4 @@ export default class Schema {
} }
} }
size() {
return this.constructor.size(this.specification);
}
} }

View File

@ -3,6 +3,14 @@ import {expect, test} from 'vitest';
import Schema from './schema.js'; import Schema from './schema.js';
test('defaults values', () => { test('defaults values', () => {
const compare = (specification, value) => {
expect(Schema.defaultValue(specification))
.to.deep.equal(value);
expect(new Schema(specification).specification.defaultValue)
.to.deep.equal(value);
expect(new Schema(specification).defaultValue())
.to.deep.equal(value);
};
[ [
'uint8', 'uint8',
'int8', 'int8',
@ -15,37 +23,35 @@ test('defaults values', () => {
'float32', 'float32',
'float64', 'float64',
].forEach((type) => { ].forEach((type) => {
expect(Schema.defaultValue({type})) compare({type}, 0);
.to.equal(0);
expect(new Schema({type}).specification.defaultValue)
.to.equal(0);
}); });
expect(Schema.defaultValue({type: 'string'})) compare({type: 'string'}, '');
.to.equal(''); compare({type: 'array', subtype: {type: 'string'}}, []);
expect(new Schema({type: 'string'}).specification.defaultValue) compare(
.to.equal(''); {
expect(Schema.defaultValue({type: 'array', subtype: {type: 'string'}})) type: 'object',
.to.deep.equal([]); properties: {
expect(new Schema({type: 'array', subtype: {type: 'string'}}).specification.defaultValue) foo: {type: 'uint8'},
.to.deep.equal([]); bar: {type: 'string'},
expect(Schema.defaultValue({ baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}},
type: 'object', },
properties: {
foo: {type: 'uint8'},
bar: {type: 'string'},
baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}},
}, },
})) {foo: 0, bar: '', baz: {blah: []}},
.to.deep.equal({foo: 0, bar: '', baz: {blah: []}}); );
expect(new Schema({ compare(
type: 'object', {
properties: { type: 'map',
foo: {type: 'uint8'}, value: {
bar: {type: 'string'}, type: 'object',
baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}}, properties: {
foo: {type: 'uint8'},
bar: {type: 'string'},
baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}},
},
},
}, },
}).specification.defaultValue) {},
.to.deep.equal({foo: 0, bar: '', baz: {blah: []}}); );
}); });
test('validates a schema', () => { test('validates a schema', () => {
@ -66,6 +72,7 @@ test('validates a schema', () => {
new Schema({type}); new Schema({type});
new Schema({type: 'array', subtype: {type}}); new Schema({type: 'array', subtype: {type}});
new Schema({type: 'object', properties: {foo: {type}}}); new Schema({type: 'object', properties: {foo: {type}}});
new Schema({type: 'map', value: {type}});
}) })
.to.not.throw(); .to.not.throw();
}); });
@ -77,14 +84,16 @@ test('calculates the size of concrete instances', () => {
.sizeOf('hi') .sizeOf('hi')
) )
.to.equal(4 + (new TextEncoder().encode('hi')).length); .to.equal(4 + (new TextEncoder().encode('hi')).length);
expect( expect(
(new Schema( (new Schema(
{type: 'object', properties: { {
foo: {type: 'uint8'}, type: 'object',
bar: {type: 'uint32'}, properties: {
baz: {type: 'string'}, foo: {type: 'uint8'},
}} bar: {type: 'uint32'},
baz: {type: 'string'},
},
},
)) ))
.sizeOf({foo: 69, bar: 420, baz: 'aα'}) .sizeOf({foo: 69, bar: 420, baz: 'aα'})
) )
@ -102,6 +111,36 @@ test('calculates the size of concrete instances', () => {
+ 4 + (new TextEncoder().encode('hallo')).length + 4 + (new TextEncoder().encode('hallo')).length
+ 4 + (new TextEncoder().encode('hαllo')).length + 4 + (new TextEncoder().encode('hαllo')).length
); );
expect(
(new Schema(
{
type: 'map',
value: {
type: 'object',
properties: {
foo: {type: 'uint8'},
bar: {type: 'uint32'},
baz: {type: 'string'},
},
},
},
))
.sizeOf({
foo: {foo: 69, bar: 420, baz: 'aα'},
'aα': {foo: 69, bar: 420, baz: 'meow'},
})
)
.to.equal(
4
+ 4 + (new TextEncoder().encode('foo')).length
+ 1
+ 4
+ 4 + (new TextEncoder().encode('aα')).length
+ 4 + (new TextEncoder().encode('aα')).length
+ 1
+ 4
+ 4 + (new TextEncoder().encode('meow')).length
);
}); });
test('encodes and decodes', () => { test('encodes and decodes', () => {
@ -124,7 +163,9 @@ test('encodes and decodes', () => {
[{type: 'string'}, 'α'], [{type: 'string'}, 'α'],
[{type: 'array', subtype: {type: 'uint8'}}, [1, 2, 3, 4]], [{type: 'array', subtype: {type: 'uint8'}}, [1, 2, 3, 4]],
[{type: 'array', subtype: {type: 'string'}}, ['one', 'two', 'three', 'four']], [{type: 'array', subtype: {type: 'string'}}, ['one', 'two', 'three', 'four']],
[{type: 'map', value: {type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}}, {one: {foo: 64, bar: 'baz'}, two: {foo: 128, bar: 'baw'}}],
[{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}], [{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}],
[{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}],
]; ];
entries.forEach(([specification, concrete]) => { entries.forEach(([specification, concrete]) => {
const schema = new Schema(specification); const schema = new Schema(specification);