diff --git a/app/ecs/schema.js b/app/ecs/schema.js index dafb4fb..a379f65 100644 --- a/app/ecs/schema.js +++ b/app/ecs/schema.js @@ -53,6 +53,16 @@ export default class Schema { } 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': { const value = {}; for (const key in specification.properties) { @@ -92,6 +102,9 @@ export default class Schema { case 'array': { return []; } + case 'map': { + return {}; + } case 'object': { const object = {}; for (const key in specification.properties) { @@ -109,10 +122,6 @@ export default class Schema { return this.constructor.defaultValue(this.specification); } - has(key) { - return key in this.specification; - } - static normalize(specification) { let normalized = specification; switch (specification.type) { @@ -120,6 +129,10 @@ export default class Schema { normalized.subtype = this.normalize(specification.subtype); break; } + case 'map': { + normalized.value = this.normalize(specification.value); + break; + } case 'object': { for (const key in specification.properties) { normalized.properties[key] = this.normalize(specification.properties[key]) @@ -139,6 +152,7 @@ export default class Schema { case 'string': { break; } + /* v8 ignore next 2 */ default: throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`); } @@ -154,8 +168,7 @@ export default class Schema { switch (specification.type) { case 'array': { view.setUint32(offset, source.length, true); - offset += 4; - let arraySize = 0; + let arraySize = 4; for (const element of source) { arraySize += this.serialize( element, @@ -164,7 +177,27 @@ export default class Schema { 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': { let objectSize = 0; @@ -210,6 +243,14 @@ export default class Schema { } 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': { for (const key in specification.properties) { fullSize += this.sizeOf(concrete[key], specification.properties[key]); @@ -231,6 +272,7 @@ export default class Schema { static size(specification) { switch (specification.type) { case 'array': return 0; + case 'map': return 0; // TODO could be fixed-size w/ fixed-size key case 'object': { let size = 0; for (const key in specification.properties) { @@ -258,8 +300,4 @@ export default class Schema { } } - size() { - return this.constructor.size(this.specification); - } - } diff --git a/app/ecs/schema.test.js b/app/ecs/schema.test.js index 3ee5eb4..50a1d5d 100644 --- a/app/ecs/schema.test.js +++ b/app/ecs/schema.test.js @@ -3,6 +3,14 @@ import {expect, test} from 'vitest'; import Schema from './schema.js'; 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', 'int8', @@ -15,37 +23,35 @@ test('defaults values', () => { 'float32', 'float64', ].forEach((type) => { - expect(Schema.defaultValue({type})) - .to.equal(0); - expect(new Schema({type}).specification.defaultValue) - .to.equal(0); + compare({type}, 0); }); - expect(Schema.defaultValue({type: 'string'})) - .to.equal(''); - expect(new Schema({type: 'string'}).specification.defaultValue) - .to.equal(''); - expect(Schema.defaultValue({type: 'array', subtype: {type: 'string'}})) - .to.deep.equal([]); - expect(new Schema({type: 'array', subtype: {type: 'string'}}).specification.defaultValue) - .to.deep.equal([]); - expect(Schema.defaultValue({ - type: 'object', - properties: { - foo: {type: 'uint8'}, - bar: {type: 'string'}, - baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}}, + compare({type: 'string'}, ''); + compare({type: 'array', subtype: {type: 'string'}}, []); + compare( + { + type: 'object', + properties: { + foo: {type: 'uint8'}, + bar: {type: 'string'}, + baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}}, + }, }, - })) - .to.deep.equal({foo: 0, bar: '', baz: {blah: []}}); - expect(new Schema({ - type: 'object', - properties: { - foo: {type: 'uint8'}, - bar: {type: 'string'}, - baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}}, + {foo: 0, bar: '', baz: {blah: []}}, + ); + compare( + { + type: 'map', + value: { + type: 'object', + 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', () => { @@ -66,6 +72,7 @@ test('validates a schema', () => { new Schema({type}); new Schema({type: 'array', subtype: {type}}); new Schema({type: 'object', properties: {foo: {type}}}); + new Schema({type: 'map', value: {type}}); }) .to.not.throw(); }); @@ -77,14 +84,16 @@ test('calculates the size of concrete instances', () => { .sizeOf('hi') ) .to.equal(4 + (new TextEncoder().encode('hi')).length); - expect( (new Schema( - {type: 'object', properties: { - foo: {type: 'uint8'}, - bar: {type: 'uint32'}, - baz: {type: 'string'}, - }} + { + type: 'object', + properties: { + foo: {type: 'uint8'}, + bar: {type: 'uint32'}, + baz: {type: 'string'}, + }, + }, )) .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('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', () => { @@ -124,7 +163,9 @@ test('encodes and decodes', () => { [{type: 'string'}, 'α'], [{type: 'array', subtype: {type: 'uint8'}}, [1, 2, 3, 4]], [{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'}}}, {foo: 64}], ]; entries.forEach(([specification, concrete]) => { const schema = new Schema(specification);