Many features and fixes discovered while switching Syndicate/js to Record

This commit is contained in:
Tony Garnock-Jones 2018-11-15 23:18:16 +00:00
parent bd08ede47a
commit f0a63fbb4c
4 changed files with 238 additions and 20 deletions

View File

@ -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);
}
}

View File

@ -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,
});

View File

@ -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);
});
});
});

View File

@ -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;