From 65d16b954a5ec2fb4ef39ab973948f9f0391c89c Mon Sep 17 00:00:00 2001 From: Emma <-> Date: Thu, 9 Jul 2020 16:18:17 +0200 Subject: [PATCH] Added Union support, switched to ES6 modules everywhere --- README.md | 18 ++++--- converter/bare.js | 84 ++++++++---------------------- converter/dump.js | 37 ++++++++++++++ converter/io.js | 73 ++++++++++++++++++++++++++ converter/parser.js | 4 +- converter/templates.js | 29 ++++++----- converter/tokenizer.js | 88 ++++++++++++++++---------------- {library => example}/example.mjs | 53 +++++++++++++------ library/bare.mjs | 77 ++++++++++++++++++++-------- package.json | 6 +++ 10 files changed, 302 insertions(+), 167 deletions(-) create mode 100644 converter/dump.js create mode 100644 converter/io.js rename {library => example}/example.mjs (73%) create mode 100644 package.json diff --git a/README.md b/README.md index 085526a..5adfec3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ The idea is, that the schema converter runs on node.js, while the resulting Java both in the browser and in other javascript environments. ## What is still missing - * Union type, this one is difficult to translate to js, so I'm going to have to think about use cases and how to make them convenient to use * Conversion error handling and integrity verification. * Unit tests, these will come last. @@ -28,8 +27,8 @@ The output from the converter is a file which only works in conjunction with `li This file contains all the message conversion logic, whereas the conversion output contains the layout specifics of the types you specified in your schema file. -My default these two are to be used as [ES6 Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). -The benefit of this is, that you do not need to have any of the hidden classes in your scope, since they are only needed +By default, these two are to be used as [ES6 Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). +The benefit of this is, that you do not need to have any of the base type classes in your scope, since they are only needed in the type definitions of the second file. Most browsers support this spec, and so does node.js, if the files end in `.mjs`. Should your environment not support this feature, or you don't want to use modules, you can full well remove the import and export statements, @@ -37,7 +36,7 @@ and load the two files like regular scripts. (I'm not 100% on this, but you can #### Now to the actual how-to for using this in a web page: -If you want to use a bare type in your `main.js` script file, you need to load it as a module: ``. +If you want to use a bare type in your, say `main.js` script file, you need to load it as a module: ``. The basic gist is, that modules do not share a scope, but read the MDN article above for more details. Then in this file you can use an [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) @@ -47,14 +46,21 @@ Just make sure that the `library/bare.mjs` file is in the same folder as the out Or you can only load some types directly into your scope: `import {Address, Point} from './path/to/output.js';`. You can even rename them with `Type as OtherName` inside those brackets, should that matter in your use case. +## A Note on Unions +Sadly it isn't possible to just add a hidden property to every JS object, since there is a possibility of one being frozen (all the primitives are). +Therefore this lib needs a wrapper, in the form of the `UnionValue` class, whenever you want to pack someting into a union, you need to specify its +Bare type by putting it inside a UnionType. Since the unpack method also gives you a UnionType, you can just put one into your object from the get go +and always expect it to be there. + ## A note about Number and 64 bits Javascript is a wonderful language, and as such it doesn't use an integer type for numbers. Instead, every single number is stored as a double precision float. This limits the usable range of integers to 53 bits, which means the maximum unsigned value is just over 9 quadrillion `(10^15)`. -There is the [BitInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) type that allows arbitrary large integers. +There is the [BitInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) type, which allows arbitrarily large integers. But it has limitations; you can't use them in Math functions or beside regular Numbers. Keep this in mind when using the variable length, and the 64 bit integer types, since these return a BigInt by default. -The lib provides `safeNumber` which will convert a BigInt to Number if it fits into 53 bits, or throw an Error if it does not fit, for when you just need a little more headroom. +The lib provides `safeNumber` which will convert a BigInt to Number if it fits into 53 bits, or throw an Error if it does not fit, +for when you just need a little more headroom. diff --git a/converter/bare.js b/converter/bare.js index b5d10e5..395dbc9 100644 --- a/converter/bare.js +++ b/converter/bare.js @@ -1,81 +1,39 @@ +import io from './io.js'; +import tokenizer from './tokenizer.js'; +import parser from './parser.js'; +import templates from './templates.js'; + /** * This is the conversion script, to be run with node.js * - * This script takes two optional arguments: [input, [output]] + * This script takes two optional arguments: [input, [output, [libraryPath]]] * if either one is omitted it will use stdout and stdin respectively. * * I am using node.js 14.5.0 but as far as I know, this should run in earlier versions as well, * as long as it supports promises and arrow syntax */ -const fs = require('fs'); - -const tokenizer = require('./tokenizer'); -const parser = require('./parser'); -const templates = require('./templates'); - -function readFile(fileName) { - return new Promise((resolve, reject) => { - fs.readFile(fileName, {encoding: 'utf-8'}, function(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); -} - -function readStdin() { - return new Promise((resolve, reject) => { - fs.read(/* stdin */0, function(err, bytesRead, buffer) { - if (err) { - reject(err); - } else { - resolve(buffer.toString("utf-8")); - } - }); - }); -} - -function writeFile(fileName, data) { - return new Promise((resolve, reject) => { - fs.writeFile(fileName, data, function(err) { - if (err) { - reject(err); - } else { - resolve(); - } - }) - }); -} - -function writeStdout(data) { - return new Promise((resolve, reject) => { - fs.write(/* stdout */ 1, data, function(err, written, string) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -(async function() { - let [input, output] = process.argv.slice(2, 4); +async function processSchema() { + let [input, output, libraryPath] = process.argv.slice(2, 5); let schema; if (input) { - schema = await readFile(input); + schema = await io.readFile(input, 'utf-8'); } else { - schema = await readStdin(); + schema = await io.readStdin(); + } + if (!libraryPath) { + libraryPath = './bare.mjs'; } let tokenList = tokenizer.tokenizeSchema(schema); let objectTree = parser.parseSchema(tokenList); - let jsModule = templates.generateClasses(objectTree); + let jsModule = templates.generateClasses(objectTree, libraryPath); if (output) { - await writeFile(jsModule); + await io.writeFile(output, jsModule); } else { - await writeStdout(jsModule); + await io.writeStdout(jsModule); } -})(); \ No newline at end of file +} + +processSchema().catch((err) => { + console.error(err); +}); \ No newline at end of file diff --git a/converter/dump.js b/converter/dump.js new file mode 100644 index 0000000..9d6a0e3 --- /dev/null +++ b/converter/dump.js @@ -0,0 +1,37 @@ +import io from './io.js'; +import tokenizer from './tokenizer.js'; +import parser from './parser.js'; +import templates from './templates.js'; + +async function dumpMessage() { + let [schemaFile, type, binaryFile, libraryPath] = process.argv.slice(2, 2 + 4); + let schema = await io.readFile(schemaFile, 'utf-8'); + let binary = await io.readFile(binaryFile); + binary = Uint8Array.from(binary); + + if (!libraryPath) { + libraryPath = './bare.mjs'; + } + const Bare = await import(libraryPath); + + let tokenList = tokenizer.tokenizeSchema(schema); + let objectTree = parser.parseSchema(tokenList); + let jsModule = templates.generateClasses(objectTree, libraryPath); + + let modulePath = schemaFile.replace('.bare', '.temp.mjs'); + await io.writeFile(modulePath, jsModule); + + let types = (await import(modulePath)).default; + + console.log('Available types:', Object.keys(types).join(', ')); + + let [object, length] = types[type].unpack(binary); + + await io.writeStdout(Bare.stringifyJSON(object, 2)); + + await io.deleteFile(modulePath); +} + +dumpMessage().catch((err) => { + console.error(err); +}); \ No newline at end of file diff --git a/converter/io.js b/converter/io.js new file mode 100644 index 0000000..5b5dbdc --- /dev/null +++ b/converter/io.js @@ -0,0 +1,73 @@ +import fs from 'fs'; + +function readFile(fileName, encoding) { + return new Promise((resolve, reject) => { + let opt = null; + if (encoding) { + opt = {encoding: encoding}; + } + fs.readFile(fileName, opt, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +function readStdin() { + return new Promise((resolve, reject) => { + fs.read(/* stdin */0, function(err, bytesRead, buffer) { + if (err) { + reject(err); + } else { + resolve(buffer.toString("utf-8")); + } + }); + }); +} + +function writeFile(fileName, data) { + return new Promise((resolve, reject) => { + fs.writeFile(fileName, data, function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }) + }); +} + +function writeStdout(data) { + return new Promise((resolve, reject) => { + fs.write(/* stdout */ 1, data, function(err, written, string) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +function deleteFile(path) { + return new Promise(((resolve, reject) => { + fs.unlink(path, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + })); +} + +export default { + readFile: readFile, + readStdin: readStdin, + writeFile: writeFile, + writeStdout: writeStdout, + deleteFile: deleteFile, +}; \ No newline at end of file diff --git a/converter/parser.js b/converter/parser.js index e6fe126..2264cec 100644 --- a/converter/parser.js +++ b/converter/parser.js @@ -5,7 +5,7 @@ const ENUM_VALUE_PATTERN = RegExp(/[A-Z][A-Z0-9]*/); class TokenError extends Error { constructor(token, expected) { - super(`Unexpected token ${token[0]}, expected ${expected}`); + super(`Unexpected token ${token[0]} (line ${token[2] + 1}:${token[3] + 1}), expected ${expected}`); if (Error.captureStackTrace) { Error.captureStackTrace(this, TokenError); @@ -255,6 +255,6 @@ function parse_struct(tokenList) { } } -module.exports = { +export default { parseSchema: parseSchema, }; \ No newline at end of file diff --git a/converter/templates.js b/converter/templates.js index a86d6d4..b3d116f 100644 --- a/converter/templates.js +++ b/converter/templates.js @@ -1,13 +1,14 @@ -const import_statement = "import {mapEnum,mapUnion,safeNumber,BareUInt,BareInt," + +const import_statement = (libraryPath) => "import {mapEnum,mapUnion,BareUInt,BareInt," + "BareU8,BareU16,BareU32,BareU64,BareI8,BareI16,BareI32,BareI64,BareF32,BareF64," + "BareBool,BareEnum,BareString,BareDataFixed,BareData,BareVoid,BareOptional," + - "BareArrayFixed,BareArray,BareMap,BareUnion,BareStruct} from './bare.mjs';"; + "BareArrayFixed,BareArray,BareMap,BareUnion,BareStruct}\n" + + `\tfrom '${libraryPath}';`; -const named_class = (name, type, content) => `class ${name} extends ${type} \{ ${content} \}`; +const named_class = (name, type, content) => `class ${name} extends ${type} {${content}}`; -const inline_class = (type, content) => `class extends ${type} \{ ${content} \}`; +const inline_class = (type, content) => `class extends ${type} {${content}}`; -const enum_content = (pairs) => `static keys = mapEnum(this, \{ ${pairs.map(p => `${p[0]}: '${p[1]}'`).join(', ')} \});`; +const enum_content = (pairs) => `static keys = mapEnum(this, {${pairs.map(p => `${p[0]}:'${p[1]}'`).join(',')}});`; const fixed_content = (len) => `static length = ${len};`; const typed_content = (type) => `static type = ${type};`; @@ -16,17 +17,17 @@ const data_fixed_content = (len) => fixed_content(len); const optional_content = (type) => typed_content(type); -const array_fixed_content = (len, type) => `${fixed_content(len)} ${typed_content(type)}`; +const array_fixed_content = (len, type) => fixed_content(len) + typed_content(type); const array_content = (type) => typed_content(type); -const map_content = (keyType, valueType) => `static keyType = ${keyType}; static valueType = ${valueType};`; +const map_content = (keyType, valueType) => `static keyType = ${keyType};static valueType = ${valueType};`; -const union_content = (types) => ""; // TODO +const union_content = (types) => `static indices = mapUnion(this, {${types.map(t => `${t[1]}:${t[0]}`)}});`; -const struct_content = (entries) => `static entries = [ ${entries.map(e => `['${e[0]}', ${e[1]}]`).join(', ')} ];`; +const struct_content = (entries) => `static entries = [${entries.map(e => `['${e[0]}',${e[1]}]`).join(',')}];`; -const export_statement = (typeNames) => `export \{ ${typeNames.join(', ')} \};`; -const export_default_statement = (typeNames) => `export default \{ ${typeNames.map(t => `${t}: ${t}`).join(', ')} \};`; +const export_statement = (typeNames) => `export {${typeNames.join(',')}};`; +const export_default_statement = (typeNames) => `export default {${typeNames.map(t => `${t}:${t}`).join(',')}};`; function resolveContent(objectTail) { if (typeof objectTail === 'string' || objectTail instanceof String) { @@ -103,8 +104,8 @@ function resolveContent(objectTail) { return content; } -function generateClasses(objectTree) { - let output = import_statement; +function generateClasses(objectTree, libraryPath) { + let output = import_statement(libraryPath); let typeNames = []; for (let i = 0; i < objectTree.length; i++) { @@ -128,6 +129,6 @@ function generateClasses(objectTree) { return output; } -module.exports = { +export default { generateClasses: generateClasses, }; \ No newline at end of file diff --git a/converter/tokenizer.js b/converter/tokenizer.js index bceb63a..e0fe0e0 100644 --- a/converter/tokenizer.js +++ b/converter/tokenizer.js @@ -1,3 +1,4 @@ +const WHITESPACE_PATTERN = RegExp(/^\s+/); const NUMERIC_PATTERN = RegExp(/^[0-9]+/); const LETTER_PATTERN = RegExp(/^[a-zA-Z]+/); const WORD_PATTERN = RegExp(/^[a-zA-Z0-9_]+/); @@ -6,11 +7,10 @@ function tokenizeSchema(schema) { let tokenList = []; let lines = schema.split('\n'); for (let i = 0; i < lines.length; i++) { - // clean up whitespace - let line = lines[i].trim().replace(/\s+/g, ' '); + let line = lines[i]; for (let j = 0; j < line.length;) { let c = line.charAt(j); - if (c === ' ') { + if (WHITESPACE_PATTERN.test(c)) { // skip character j++; } else if (c === '#') { @@ -19,20 +19,20 @@ function tokenizeSchema(schema) { } else if (LETTER_PATTERN.test(c)) { let match = line.slice(j).match(WORD_PATTERN); if (!match) { - throw SyntaxError(`Could not match word token (@${j}):\n\t${line}`); + throw SyntaxError(`Could not match word token (line ${i + 1}:${j + 1}):\n\t${line}`); } - let token = stringToToken(match[0]); + let token = stringToToken(match[0], i, j); tokenList.push(token); j += match[0].length; } else if (NUMERIC_PATTERN.test(c)) { let match = line.slice(j).match(NUMERIC_PATTERN); if (!match) { - throw SyntaxError(`Could not match digit token (@${j}):\n\t${line}`); + throw SyntaxError(`Could not match digit token (line ${i + 1}:${j + 1}):\n\t${line}`); } - tokenList.push(['TINTEGER', match[0]]); + tokenList.push(['TINTEGER', match[0], i, j]); j += match[0].length; } else { - let token = charToToken(c); + let token = charToToken(c, i, j); tokenList.push(token); j++; } @@ -41,84 +41,84 @@ function tokenizeSchema(schema) { return tokenList; } -function stringToToken(string) { +function stringToToken(string, i, j) { switch (string) { case 'type': - return ['TTYPE', '']; + return ['TTYPE', '', i, j]; case 'enum': - return ['TENUM', '']; + return ['TENUM', '', i, j]; case 'uint': - return ['TUINT', '']; + return ['TUINT', '', i, j]; case 'u8': - return ['TU8', '']; + return ['TU8', '', i, j]; case 'u16': - return ['TU16', '']; + return ['TU16', '', i, j]; case 'u32': - return ['TU32', '']; + return ['TU32', '', i, j]; case 'u64': - return ['TU64', '']; + return ['TU64', '', i, j]; case 'int': - return ['TINT', '']; + return ['TINT', '', i, j]; case 'i8': - return ['TI8', '']; + return ['TI8', '', i, j]; case 'i16': - return ['TI16', '']; + return ['TI16', '', i, j]; case 'i32': - return ['TI32', '']; + return ['TI32', '', i, j]; case 'i64': - return ['TI64', '']; + return ['TI64', '', i, j]; case 'f32': - return ['TF32', '']; + return ['TF32', '', i, j]; case 'f64': - return ['TF64', '']; + return ['TF64', '', i, j]; case 'bool': - return ['TBOOL', '']; + return ['TBOOL', '', i, j]; case 'string': - return ['TSTRING', '']; + return ['TSTRING', '', i, j]; case 'data': - return ['TDATA', '']; + return ['TDATA', '', i, j]; case 'void': - return ['TVOID', '']; + return ['TVOID', '', i, j]; case 'optional': - return ['TOPTIONAL', '']; + return ['TOPTIONAL', '', i, j]; case 'map': - return ['TMAP', '']; + return ['TMAP', '', i, j]; default: - return ['TNAME', string]; + return ['TNAME', string, i, j]; } } -function charToToken(c) { +function charToToken(c, i, j) { switch (c) { case '<': - return ['TLANGLE', '']; + return ['TLANGLE', '', i, j]; case '>': - return ['TRANGLE', '']; + return ['TRANGLE', '', i, j]; case '{': - return ['TLBRACE', '']; + return ['TLBRACE', '', i, j]; case '}': - return ['TRBRACE', '']; + return ['TRBRACE', '', i, j]; case '[': - return ['TLBRACKET', '']; + return ['TLBRACKET', '', i, j]; case ']': - return ['TRBRACKET', '']; + return ['TRBRACKET', '', i, j]; case '(': - return ['TLPAREN', '']; + return ['TLPAREN', '', i, j]; case ')': - return ['TRPAREN', '']; + return ['TRPAREN', '', i, j]; case ',': - return ['TCOMMA', '']; + return ['TCOMMA', '', i, j]; case '|': - return ['TPIPE', '']; + return ['TPIPE', '', i, j]; case '=': - return ['TEQUAL', '']; + return ['TEQUAL', '', i, j]; case ':': - return ['TCOLON', '']; + return ['TCOLON', '', i, j]; default: throw SyntaxError(`Unknown character '${c}'`); } } -module.exports = { +export default { tokenizeSchema: tokenizeSchema, }; \ No newline at end of file diff --git a/library/example.mjs b/example/example.mjs similarity index 73% rename from library/example.mjs rename to example/example.mjs index c9694ac..f9ec7c4 100644 --- a/library/example.mjs +++ b/example/example.mjs @@ -1,4 +1,4 @@ -import Bare from './bare.mjs'; +import Bare from '../library/bare.mjs'; class Point extends Bare.ArrayFixed { static length = 3; @@ -74,17 +74,13 @@ let test3 = [ }, ]; -class Address extends Bare.Struct { - static entries = [ - ['address', class extends Bare.ArrayFixed { - static length = 4; - static type = Bare.String; - }], - ['city', Bare.String], - ['state', Bare.String], - ['country', Bare.String], - ]; -} +import {Address} from './schema.mjs'; + +/** + * hexdump -b to array: + * find: '([0-7]{3}) ?' + * replace: '0o$1, ' + */ let addrTest = Uint8Array.from([ 0o016, 0o101, 0o144, 0o144, 0o162, 0o145, 0o163, 0o163, 0o040, 0o154, 0o151, 0o156, 0o145, 0o040, 0o061, 0o000, @@ -93,12 +89,21 @@ let addrTest = Uint8Array.from([ 0o145, 0o162, 0o154, 0o141, 0o156, 0o144, 0o163, ]); -/** - * hexdump -b to array: - * find: '([0-7]{3}) ?' - * replace: '0o$1, ' - */ +class Test4 extends Bare.Union { + static indices = Bare.mapUnion(this, { + 0: Point, + 1: class extends Bare.Struct { + static entries = [ + ['x', Bare.F32], + ['y', Bare.F32], + ['z', Bare.F32], + ]; + }, + }); +} +let test4a = new Bare.UnionValue(Point, [3.6, Math.sqrt(2), 1/3]); +let test4b = new Bare.UnionValue(Test4[1], {x: 3.6, y: Math.sqrt(2), z: 1/3}); (() => { console.log(test1); @@ -124,4 +129,18 @@ let addrTest = Uint8Array.from([ console.log("-------------------"); let [addr, al] = Address.unpack(addrTest); console.log(addr); + + console.log("-------------------"); + console.log(test4a); + let test4a_bin = Test4.pack(test4a); + console.log(test4a_bin); + let [test4a_un, t4al] = Test4.unpack(test4a_bin); + console.log(test4a_un); + + console.log("-------------------"); + console.log(test4b); + let test4b_bin = Test4.pack(test4b); + console.log(test4b_bin); + let [test4b_un, t4bl] = Test4.unpack(test4b_bin); + console.log(test4b_un); })(); \ No newline at end of file diff --git a/library/bare.mjs b/library/bare.mjs index 194e835..e61f806 100644 --- a/library/bare.mjs +++ b/library/bare.mjs @@ -17,6 +17,25 @@ function safeNumber(bigInt) { } } +function stringifyJSON(object, space) { + /* allow printing of bigint values in JSON for debugging: */ + return JSON.stringify(object, (key, value) => { + if (typeof value === 'bigint') + return value.toString() + 'n'; + else if (value instanceof UnionValue) + return value.value; + else + return value; + }, space); +} + +const BIGINT_PATTERN = RegExp(/^\d+n$/); +function parseJSON(json) { + /* allow parsing of JSON with bigint values */ + return JSON.parse(json, + (key, value) => typeof value === 'string' && BIGINT_PATTERN.test(value) ? BigInt(value.slice(0, -1)) : value); +} + const MAX_U32 = 2n ** 32n - 1n; const MAX_U53 = 2n ** 53n - 1n; // same as Number.MAX_SAFE_INTEGER const MAX_U64 = 2n ** 64n - 1n; @@ -373,7 +392,7 @@ class BareOptional extends BareType { static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element - if (!raw instanceof DataView) { + if (!(raw instanceof DataView)) { raw = new DataView(raw.buffer, raw.byteOffset); } let status = raw.getUint8(0); @@ -426,7 +445,7 @@ class BareArray extends BareType { static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element - if (!raw instanceof DataView) { + if (!(raw instanceof DataView)) { raw = new DataView(raw.buffer, raw.byteOffset); } let obj = []; @@ -464,7 +483,7 @@ class BareMap extends BareType { static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element - if (!raw instanceof DataView) { + if (!(raw instanceof DataView)) { raw = new DataView(raw.buffer, raw.byteOffset); } let obj = {}; @@ -475,7 +494,7 @@ class BareMap extends BareType { numEntries = Number(numEntries); for (let i = 0; i < numEntries; i++) { let keyView = new DataView(raw.buffer, raw.byteOffset + length); - let [key, keyBytes] = this.type.unpack(keyView); + let [key, keyBytes] = this.keyType.unpack(keyView); length += keyBytes; let valueView = new DataView(raw.buffer, raw.byteOffset + length); let [value, valueBytes] = this.valueType.unpack(valueView); @@ -486,39 +505,51 @@ class BareMap extends BareType { } } -function mapUnion(obj) { - let entries = Object.entries(obj); - let reverse = {}; +class UnionValue extends Object { + constructor(type, value) { + super(); + this.type = type; + this.value = value; + } +} + +function mapUnion(unionClass, keys) { + let indices = new Map(); + let entries = Object.entries(keys); for (let i = 0; i < entries.length; i++) { - let [key, val] = entries[i]; - reverse[val] = key; + let [index, type] = entries[i]; + unionClass[index] = type; + indices.set(type, index); } - return reverse; + return indices; } -// TODO how do I store/represent the BARE type of an object for unions??? also; use BigInt class BareUnion extends BareType { // INVARIANT: has at least one type - static types; // = twoWayMap([[class, i], ...]) + static indices; // = mapUnion(this, {i: , ...}) static pack(obj) { - let unionIndex = this.types[obj.constructor]; - if (!unionIndex) { - console.err("The union " + this + " does not support encoding the type " + obj) + if (!(obj instanceof UnionValue)) { + throw Error("A union value needs to be associated with its type by creating a UnionValue object:\n" + stringifyJSON(obj)); + } + let objType = obj.type; + let unionIndex = this.indices.get(objType); + if (unionIndex === undefined) { + throw Error("The union " + this.name + " does not support encoding the type " + objType.name) } - - let bin = obj.pack(); let index = BareUInt.pack(unionIndex); - return joinUint8Arrays(index, bin); + let bytes = objType.pack(obj.value); + return joinUint8Arrays(index, bytes); } static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element - if (!raw instanceof DataView) { + if (!(raw instanceof DataView)) { raw = new DataView(raw.buffer, raw.byteOffset); } let [index, length] = BareUInt.unpack(raw); - let objType = this.types[index]; + let objType = this[index]; let [obj, bytes] = objType.unpack(new DataView(raw.buffer, raw.byteOffset + length)); + obj = new UnionValue(objType, obj); return [obj, bytes + length]; } } @@ -552,7 +583,8 @@ class BareStruct extends BareType { } export { - mapEnum, mapUnion, safeNumber, + mapEnum, mapUnion, UnionValue, + safeNumber, stringifyJSON, parseJSON, BareUInt, BareInt, BareU8, BareU16, BareU32, BareU64, BareI8, BareI16, BareI32, BareI64, @@ -572,7 +604,10 @@ export { export default { mapEnum: mapEnum, mapUnion: mapUnion, + UnionValue: UnionValue, safeNumber: safeNumber, + stringifyJSON: stringifyJSON, + parseJSON: parseJSON, UInt: BareUInt, Int: BareInt, U8: BareU8, diff --git a/package.json b/package.json new file mode 100644 index 0000000..93ede96 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "bare-js", + "version": "0.1.0", + "dependencies": {}, + "type": "module" +} -- 2.45.2