const encoder = new TextEncoder(); export default class Schema { $$size = 0; specification; static viewGetMethods = { uint8: 'getUint8', int8: 'getInt8', uint16: 'getUint16', int16: 'getInt16', uint32: 'getUint32', int32: 'getInt32', float32: 'getFloat32', float64: 'getFloat64', int64: 'getBigInt64', uint64: 'getBigUint64', }; static viewSetMethods = { uint8: 'setUint8', int8: 'setInt8', uint16: 'setUint16', int16: 'setInt16', uint32: 'setUint32', int32: 'setInt32', float32: 'setFloat32', float64: 'setFloat64', int64: 'setBigInt64', uint64: 'setBigUint64', }; constructor(specification) { this.specification = this.constructor.normalize(specification); } static deserialize(view, offset, specification) { const viewGetMethod = this.viewGetMethods[specification.type]; if (viewGetMethod) { const value = view[viewGetMethod](offset.value, true); offset.value += this.size(specification); return value; } switch (specification.type) { case 'array': { const length = view.getUint32(offset.value, true); offset.value += 4; const value = []; for (let i = 0; i < length; ++i) { value.push(this.deserialize(view, offset, specification.subtype)); } return value; } case 'object': { const value = {}; for (const key in specification.properties) { value[key] = this.deserialize(view, offset, specification.properties[key]); } return value; } case 'string': { const length = view.getUint32(offset.value, true); offset.value += 4; const {buffer, byteOffset} = view; const decoder = new TextDecoder(); const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length)); offset.value += length; return value; } } } deserialize(view, offset = 0) { const wrapped = {value: offset}; return this.constructor.deserialize(view, wrapped, this.specification); } static defaultValue(specification) { if (specification.defaultValue) { return specification.defaultValue; } switch (specification.type) { case 'uint8': case 'int8': case 'uint16': case 'int16': case 'uint32': case 'int32': case 'uint64': case 'int64': case 'float32': case 'float64': { return 0; } case 'array': { return []; } case 'object': { const object = {}; for (const key in specification.properties) { object[key] = this.defaultValue(specification.properties[key]); } return object; } case 'string': { return ''; } } } defaultValue() { return this.constructor.defaultValue(this.specification); } has(key) { return key in this.specification; } static normalize(specification) { let normalized = specification; switch (specification.type) { case 'array': { normalized.subtype = this.normalize(specification.subtype); break; } case 'object': { for (const key in specification.properties) { normalized.properties[key] = this.normalize(specification.properties[key]) } break; } case 'uint8': case 'int8': case 'uint16': case 'int16': case 'uint32': case 'int32': case 'uint64': case 'int64': case 'float32': case 'float64': case 'string': { break; } default: throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`); } return {...normalized, defaultValue: this.defaultValue(normalized)}; } static readSize(view, offset, specification) { const size = this.size(specification); if (size > 0) { return size; } switch (specification.type) { case 'array': { const length = view.getUint32(offset.value, true); offset.value += 4; let arraySize = 0; for (let i = 0; i < length; ++i) { arraySize += this.readSize(view, offset, specification.subtype); } return 4 + arraySize; } case 'object': { let objectSize = 0; for (const key in specification.properties) { objectSize += this.readSize(view, offset, specification.properties[key]); } return objectSize; } case 'string': { const length = view.getUint32(offset.value, true); offset.value += 4 + length; return 4 + length; } } } readSize(view, offset) { const wrapped = {value: offset}; return this.constructor.readSize(view, wrapped, this.specification); } static serialize(source, view, offset, specification) { const viewSetMethod = this.viewSetMethods[specification.type]; if (viewSetMethod) { view[viewSetMethod](offset, source, true); return this.size(specification); } switch (specification.type) { case 'array': { view.setUint32(offset, source.length, true); offset += 4; let arraySize = 0; for (const element of source) { arraySize += this.serialize( element, view, offset + arraySize, specification.subtype, ); } return 4 + arraySize; } case 'object': { let objectSize = 0; for (const key in specification.properties) { objectSize += this.serialize( source[key], view, offset + objectSize, specification.properties[key], ); } return objectSize; } case 'string': { const encoder = new TextEncoder(); const bytes = encoder.encode(source); view.setUint32(offset, bytes.length, true); offset += 4; for (let i = 0; i < bytes.length; ++i) { view.setUint8(offset++, bytes[i]); } return 4 + bytes.length; } } } serialize(source, view, offset = 0) { this.constructor.serialize(source, view, offset, this.specification); } static sizeOf(concrete, specification) { const size = this.size(specification); if (size > 0) { return size; } let fullSize = 0; const {type} = specification; switch (type) { case 'array': { fullSize += 4; for (const element of concrete) { fullSize += this.sizeOf(element, specification.subtype); } break; } case 'object': { for (const key in specification.properties) { fullSize += this.sizeOf(concrete[key], specification.properties[key]); } break; } case 'string': fullSize += 4; fullSize += (encoder.encode(concrete)).length; break; } return fullSize; } sizeOf(concrete) { return this.constructor.sizeOf(concrete, this.specification); } static size(specification) { switch (specification.type) { case 'array': return 0; case 'object': { let size = 0; for (const key in specification.properties) { const propertySize = this.size(specification.properties[key]); if (0 === propertySize) { return 0; } size += propertySize; } return size; } case 'uint8': case 'int8': { return 1; } case 'uint16': case 'int16': { return 2; } case 'uint32': case 'int32': case 'float32': { return 4; } case 'uint64': case 'int64': case 'float64': { return 8; } default: return 0; } } size() { return this.constructor.size(this.specification); } }