From f0a63fbb4ca6a7cd0e6c69e62cee93241e0eccc6 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Thu, 15 Nov 2018 23:18:16 +0000 Subject: [PATCH] Many features and fixes discovered while switching Syndicate/js to Record --- implementations/javascript/src/codec.js | 4 +- implementations/javascript/src/values.js | 142 ++++++++++++++++-- implementations/javascript/test/test-bytes.js | 86 +++++++++++ implementations/javascript/test/test-codec.js | 26 +++- 4 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 implementations/javascript/test/test-bytes.js diff --git a/implementations/javascript/src/codec.js b/implementations/javascript/src/codec.js index c80043d..4f08317 100644 --- a/implementations/javascript/src/codec.js +++ b/implementations/javascript/src/codec.js @@ -128,11 +128,11 @@ class Decoder { decoderecord(minor, vs) { if (minor === 3) { if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); - return Record(vs[0], vs.slice(1)); + return new Record(vs[0], vs.slice(1)); } else { const label = this.shortForms[minor]; if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor); - return Record(label, vs); + return new Record(label, vs); } } diff --git a/implementations/javascript/src/values.js b/implementations/javascript/src/values.js index 24986b2..ff2ca05 100644 --- a/implementations/javascript/src/values.js +++ b/implementations/javascript/src/values.js @@ -2,8 +2,10 @@ // Preserves Values. // Uses Immutable.js for many things; adds immutable values of its own for the rest. +const util = require('util'); + const Immutable = require('immutable'); -const { List, isList, Map, Set } = Immutable; +const { List, isList, Map, Set, is } = Immutable; const { PreserveOn, AsPreserve } = require('./symbols.js'); @@ -50,7 +52,7 @@ function fromJS(x) { if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) { return Bytes(x); } - return x; + return Immutable.fromJS(x); } } @@ -131,7 +133,13 @@ function Bytes(maybeByteIterable) { } } +function _installView(view) { + Object.defineProperty(this, '_view', { value: view, writable: false }); + Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true }); +} + Bytes.from = Bytes; +Bytes.of = function (...args) { return Bytes(Uint8Array.of(...args)); }; function unhexDigit(asciiCode) { if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48; @@ -151,6 +159,28 @@ Bytes.fromHex = function (s) { return result; }; +Bytes.fromIO = function (io) { + if (typeof io === 'string') return io; + if (io instanceof Bytes) return io; + if (io instanceof Uint8Array) return Bytes.from(io); + { + const e = new TypeError("Bytes.fromIO: unsupported value"); + e.irritant = io; + throw e; + } +}; + +Bytes.toIO = function (b) { + if (typeof b === 'string') return b; + if (b instanceof Bytes) return b._view; + if (b instanceof Uint8Array) return b; + { + const e = new TypeError("Bytes.toIO: unsupported value"); + e.irritant = b; + throw e; + } +}; + function underlying(b) { return b._view || b; } @@ -169,6 +199,10 @@ Bytes.concat = function (bss) { return result; }; +Bytes.prototype.get = function (index) { + return this._view[index]; +}; + Bytes.prototype.equals = function (other) { if (!(other instanceof Bytes)) return false; if (other.size !== this.size) return false; @@ -198,6 +232,26 @@ Bytes.prototype.toString = function () { return decoder.decode(this._view); }; +Bytes.prototype[util.inspect.custom] = function (depth, options) { + return '#"' + this.__asciify() + '"'; +}; + +Bytes.prototype.__asciify = function () { + const pieces = []; + const v = this._view; + for (let i = 0; i < v.length; i++) { + const b = v[i]; + if (b === 92 || b === 34) { + pieces.push('\\' + String.fromCharCode(b)); + } else if (b >= 32 && b <= 126) { + pieces.push(String.fromCharCode(b)); + } else { + pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15)); + } + } + return pieces.join(''); +}; + function hexDigit(n) { return '0123456789abcdef'[n]; } Bytes.prototype.toHex = function () { @@ -214,21 +268,39 @@ Bytes.prototype[PreserveOn] = function (encoder) { encoder.emitbytes(this._view); }; -function _installView(view) { - Object.defineProperty(this, '_view', { value: view, writable: false }); - Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true }); -} +// Uint8Array / TypedArray methods +(function () { + for (const k of `entries every find findIndex forEach includes indexOf join + keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/)) + { + Bytes.prototype[k] = function (...args) { return this._view[k](...args); }; + } + + for (const k of `filter map slice subarray`.split(/\s+/)) + { + Bytes.prototype[k] = function (...args) { return Bytes(this._view[k](...args)); }; + } + + for (const k of `reverse sort`.split(/\s+/)) + { + Bytes.prototype[k] = function (...args) { return Bytes(this._view.slice()[k](...args)); }; + } + + Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); }; +})(); function Record(label, fields) { - if (!(this instanceof Record)) return new Record(label, fields); + if (!(this instanceof Record)) { + throw new TypeError("Class constructor Record cannot be invoked without 'new'"); + } Object.defineProperty(this, 'label', { value: fromJS(label), writable: false, enumerable: true }); Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true }); } Record.prototype.equals = function (other) { return (other instanceof Record) && - Immutable.is(this.label, other.label) && - Immutable.is(this.fields, other.fields); + is(this.label, other.label) && + is(this.fields, other.fields); }; Record.prototype.hashCode = function () { @@ -244,11 +316,11 @@ Record.prototype.set = function (index, newValue) { }; Record.prototype[PreserveOn] = function (encoder) { - if (Immutable.is(encoder.shortForms[0], this.label)) { + if (is(encoder.shortForms[0], this.label)) { encoder.header(2, 0, this.fields.size); - } else if (Immutable.is(encoder.shortForms[1], this.label)) { + } else if (is(encoder.shortForms[1], this.label)) { encoder.header(2, 1, this.fields.size); - } else if (Immutable.is(encoder.shortForms[2], this.label)) { + } else if (is(encoder.shortForms[2], this.label)) { encoder.header(2, 2, this.fields.size); } else { encoder.header(2, 3, this.fields.size + 1); @@ -257,6 +329,21 @@ Record.prototype[PreserveOn] = function (encoder) { for (const field of this.fields) { encoder.push(field); } }; +Record.prototype.getConstructorInfo = function () { + return new RecordConstructorInfo(this.label, this.fields.size); +}; + +Record.prototype.toString = function () { + return this.label.toString().replace(/^Symbol\((.*)\)$/, '$1') + + '(' + this.fields.map((f) => { + try { + return "" + f; + } catch (e) { + return util.inspect(f); + } + }).join(', ') + ')'; +}; + Record.makeConstructor = function (labelSymbolText, fieldNames) { return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames); }; @@ -270,15 +357,38 @@ Record.makeBasicConstructor = function (label, fieldNames) { } return new Record(label, fields); }; - ctor.meta = label; - ctor.isClassOf = (v) => ((v instanceof Record) && Immutable.is(label, v.label)); + ctor.constructorInfo = new RecordConstructorInfo(label, arity); + ctor.isClassOf = (v) => ((v instanceof Record) && + is(label, v.label) && + v.fields.size === arity); return ctor; }; +function RecordConstructorInfo(label, arity) { + this.label = label; + this.arity = arity; +} + +RecordConstructorInfo.prototype.equals = function (other) { + return (other instanceof RecordConstructorInfo) && + (this.label === other.label) && + (this.arity === other.arity); +}; + +RecordConstructorInfo.prototype.hashCode = function () { + return Immutable.List([this.label, this.arity]).hashCode(); +}; + +RecordConstructorInfo.prototype.isClassOf = function (v) { + return (v instanceof Record) && + is(this.label, v.label) && + (this.arity === v.fields.size); +}; + Object.assign(module.exports, { fromJS, - List, Map, Set, + List, Map, Set, is, Float, Single, Double, Bytes, - Record, + Record, RecordConstructorInfo, }); diff --git a/implementations/javascript/test/test-bytes.js b/implementations/javascript/test/test-bytes.js new file mode 100644 index 0000000..085d8eb --- /dev/null +++ b/implementations/javascript/test/test-bytes.js @@ -0,0 +1,86 @@ +"use strict"; + +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-immutable')); + +const Immutable = require('immutable'); + +const { is, Bytes, fromJS } = require('../src/index.js'); + +describe('immutable byte arrays', () => { + describe('Uint8Array methods', () => { + const bs = Bytes.of(10, 20, 30, 40); + it('should yield entries', () => { + expect(fromJS(Array.from(bs.entries()))).to.equal(fromJS([[0,10],[1,20],[2,30],[3,40]])); + }); + it('should implement every', () => { + expect(bs.every((b) => !(b & 1))).to.be.true; + expect(bs.every((b) => b !== 50)).to.be.true; + expect(bs.every((b) => b !== 20)).to.be.false; + }); + it('should implement find', () => { + expect(bs.find((b) => b > 20)).to.equal(30); + expect(bs.find((b) => b > 50)).to.be.undefined; + }); + it('should implement findIndex', () => { + expect(bs.findIndex((b) => b > 20)).to.equal(2); + expect(bs.findIndex((b) => b > 50)).to.equal(-1); + }); + it('should implement forEach', () => { + const vs = []; + bs.forEach((b) => vs.push(b)); + expect(fromJS(vs)).to.equal(fromJS([10, 20, 30, 40])); + }); + it('should implement includes', () => { + expect(bs.includes(20)).to.be.true; + expect(bs.includes(50)).to.be.false; + }); + it('should implement indexOf', () => { + expect(bs.indexOf(20)).to.equal(1); + expect(bs.indexOf(50)).to.equal(-1); + }); + it('should implement join', () => expect(bs.join('-')).to.equal('10-20-30-40')); + it('should implement keys', () => { + expect(fromJS(Array.from(bs.keys()))).to.equal(fromJS([0,1,2,3])); + }); + it('should implement values', () => { + expect(fromJS(Array.from(bs.values()))).to.equal(fromJS([10,20,30,40])); + }); + it('should implement filter', () => { + expect(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40))).to.be.true; + }); + it('should implement slice', () => { + const vs = bs.slice(2); + expect(Object.is(vs._view.buffer, bs._view.buffer)).to.be.false; + expect(vs._view.buffer.byteLength).to.equal(2); + expect(vs.get(0)).to.equal(30); + expect(vs.get(1)).to.equal(40); + expect(vs.size).to.equal(2); + }); + it('should implement subarray', () => { + const vs = bs.subarray(2); + expect(Object.is(vs._view.buffer, bs._view.buffer)).to.be.true; + expect(vs._view.buffer.byteLength).to.equal(4); + expect(vs.get(0)).to.equal(30); + expect(vs.get(1)).to.equal(40); + expect(vs.size).to.equal(2); + }); + it('should implement reverse', () => { + const vs = bs.reverse(); + expect(Object.is(vs._view.buffer, bs._view.buffer)).to.be.false; + expect(bs.get(0)).to.equal(10); + expect(bs.get(3)).to.equal(40); + expect(vs.get(0)).to.equal(40); + expect(vs.get(3)).to.equal(10); + }); + it('should implement sort', () => { + const vs = bs.reverse().sort(); + expect(Object.is(vs._view.buffer, bs._view.buffer)).to.be.false; + expect(bs.get(0)).to.equal(10); + expect(bs.get(3)).to.equal(40); + expect(vs.get(0)).to.equal(10); + expect(vs.get(3)).to.equal(40); + }); + }); +}); diff --git a/implementations/javascript/test/test-codec.js b/implementations/javascript/test/test-codec.js index 6ca17e1..3b3c74e 100644 --- a/implementations/javascript/test/test-codec.js +++ b/implementations/javascript/test/test-codec.js @@ -7,7 +7,7 @@ chai.use(require('chai-immutable')); const Immutable = require('immutable'); const Preserves = require('../src/index.js'); -const { List, Set, Map, Decoder, Encoder, Bytes, Record, Single, Double } = Preserves; +const { is, List, Set, Map, Decoder, Encoder, Bytes, Record, Single, Double } = Preserves; const fs = require('fs'); const util = require('util'); @@ -22,6 +22,28 @@ const Discard = Record.makeConstructor('discard', []); const Capture = Record.makeConstructor('capture', ['pattern']); const Observe = Record.makeConstructor('observe', ['pattern']); +describe('record constructors', () => { + it('should have constructorInfo', () => { + expect(Discard.constructorInfo.label).to.equal(Symbol.for('discard')); + expect(Capture.constructorInfo.label).to.equal(Symbol.for('capture')); + expect(Observe.constructorInfo.label).to.equal(Symbol.for('observe')); + expect(Discard.constructorInfo.arity).to.equal(0); + expect(Capture.constructorInfo.arity).to.equal(1); + expect(Observe.constructorInfo.arity).to.equal(1); + }); +}); + +describe('records', () => { + it('should have correct getConstructorInfo', () => { + expect(Discard().getConstructorInfo().equals(Discard.constructorInfo)).to.be.true; + expect(Capture(Discard()).getConstructorInfo().equals(Capture.constructorInfo)).to.be.true; + expect(Observe(Capture(Discard())).getConstructorInfo().equals(Observe.constructorInfo)) + .to.be.true; + expect(is(Observe(Capture(Discard())).getConstructorInfo(), Observe.constructorInfo)) + .to.be.true; + }); +}); + describe('hex samples', () => { const samples = fs.readFileSync(__dirname + '/samples.txt').toString().split(/\n/) .filter((h) => h) // filters out empty lines @@ -103,7 +125,7 @@ describe('hex samples', () => { it('[' + sampleIndex + '] ' + s.toHex() + ' should decode OK', () => { const actual = new Decoder(s, { shortForms }).next(); const expected = samplesExpected[sampleIndex].expected; - expect(Immutable.is(actual, expected), + expect(is(actual, expected), '[' + sampleIndex + '] actual ' + util.inspect(actual) + ', expected ' + util.inspect(expected)) .to.be.true;