feat: map
This commit is contained in:
parent
15ca775611
commit
9fe10408f7
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user