forked from syndicate-lang/preserves
Many features and fixes discovered while switching Syndicate/js to Record
This commit is contained in:
parent
bd08ede47a
commit
f0a63fbb4c
|
@ -128,11 +128,11 @@ class Decoder {
|
||||||
decoderecord(minor, vs) {
|
decoderecord(minor, vs) {
|
||||||
if (minor === 3) {
|
if (minor === 3) {
|
||||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
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 {
|
} else {
|
||||||
const label = this.shortForms[minor];
|
const label = this.shortForms[minor];
|
||||||
if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor);
|
if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor);
|
||||||
return Record(label, vs);
|
return new Record(label, vs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
// Preserves Values.
|
// Preserves Values.
|
||||||
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
|
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
|
||||||
|
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
const Immutable = require('immutable');
|
const Immutable = require('immutable');
|
||||||
const { List, isList, Map, Set } = Immutable;
|
const { List, isList, Map, Set, is } = Immutable;
|
||||||
|
|
||||||
const { PreserveOn, AsPreserve } = require('./symbols.js');
|
const { PreserveOn, AsPreserve } = require('./symbols.js');
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ function fromJS(x) {
|
||||||
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
||||||
return Bytes(x);
|
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.from = Bytes;
|
||||||
|
Bytes.of = function (...args) { return Bytes(Uint8Array.of(...args)); };
|
||||||
|
|
||||||
function unhexDigit(asciiCode) {
|
function unhexDigit(asciiCode) {
|
||||||
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
|
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
|
||||||
|
@ -151,6 +159,28 @@ Bytes.fromHex = function (s) {
|
||||||
return result;
|
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) {
|
function underlying(b) {
|
||||||
return b._view || b;
|
return b._view || b;
|
||||||
}
|
}
|
||||||
|
@ -169,6 +199,10 @@ Bytes.concat = function (bss) {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Bytes.prototype.get = function (index) {
|
||||||
|
return this._view[index];
|
||||||
|
};
|
||||||
|
|
||||||
Bytes.prototype.equals = function (other) {
|
Bytes.prototype.equals = function (other) {
|
||||||
if (!(other instanceof Bytes)) return false;
|
if (!(other instanceof Bytes)) return false;
|
||||||
if (other.size !== this.size) return false;
|
if (other.size !== this.size) return false;
|
||||||
|
@ -198,6 +232,26 @@ Bytes.prototype.toString = function () {
|
||||||
return decoder.decode(this._view);
|
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]; }
|
function hexDigit(n) { return '0123456789abcdef'[n]; }
|
||||||
|
|
||||||
Bytes.prototype.toHex = function () {
|
Bytes.prototype.toHex = function () {
|
||||||
|
@ -214,21 +268,39 @@ Bytes.prototype[PreserveOn] = function (encoder) {
|
||||||
encoder.emitbytes(this._view);
|
encoder.emitbytes(this._view);
|
||||||
};
|
};
|
||||||
|
|
||||||
function _installView(view) {
|
// Uint8Array / TypedArray methods
|
||||||
Object.defineProperty(this, '_view', { value: view, writable: false });
|
(function () {
|
||||||
Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true });
|
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) {
|
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, 'label', { value: fromJS(label), writable: false, enumerable: true });
|
||||||
Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true });
|
Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
Record.prototype.equals = function (other) {
|
Record.prototype.equals = function (other) {
|
||||||
return (other instanceof Record) &&
|
return (other instanceof Record) &&
|
||||||
Immutable.is(this.label, other.label) &&
|
is(this.label, other.label) &&
|
||||||
Immutable.is(this.fields, other.fields);
|
is(this.fields, other.fields);
|
||||||
};
|
};
|
||||||
|
|
||||||
Record.prototype.hashCode = function () {
|
Record.prototype.hashCode = function () {
|
||||||
|
@ -244,11 +316,11 @@ Record.prototype.set = function (index, newValue) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Record.prototype[PreserveOn] = function (encoder) {
|
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);
|
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);
|
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);
|
encoder.header(2, 2, this.fields.size);
|
||||||
} else {
|
} else {
|
||||||
encoder.header(2, 3, this.fields.size + 1);
|
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); }
|
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) {
|
Record.makeConstructor = function (labelSymbolText, fieldNames) {
|
||||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||||
};
|
};
|
||||||
|
@ -270,15 +357,38 @@ Record.makeBasicConstructor = function (label, fieldNames) {
|
||||||
}
|
}
|
||||||
return new Record(label, fields);
|
return new Record(label, fields);
|
||||||
};
|
};
|
||||||
ctor.meta = label;
|
ctor.constructorInfo = new RecordConstructorInfo(label, arity);
|
||||||
ctor.isClassOf = (v) => ((v instanceof Record) && Immutable.is(label, v.label));
|
ctor.isClassOf = (v) => ((v instanceof Record) &&
|
||||||
|
is(label, v.label) &&
|
||||||
|
v.fields.size === arity);
|
||||||
return ctor;
|
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, {
|
Object.assign(module.exports, {
|
||||||
fromJS,
|
fromJS,
|
||||||
List, Map, Set,
|
List, Map, Set, is,
|
||||||
Float, Single, Double,
|
Float, Single, Double,
|
||||||
Bytes,
|
Bytes,
|
||||||
Record,
|
Record, RecordConstructorInfo,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,7 +7,7 @@ chai.use(require('chai-immutable'));
|
||||||
const Immutable = require('immutable');
|
const Immutable = require('immutable');
|
||||||
|
|
||||||
const Preserves = require('../src/index.js');
|
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 fs = require('fs');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
@ -22,6 +22,28 @@ const Discard = Record.makeConstructor('discard', []);
|
||||||
const Capture = Record.makeConstructor('capture', ['pattern']);
|
const Capture = Record.makeConstructor('capture', ['pattern']);
|
||||||
const Observe = Record.makeConstructor('observe', ['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', () => {
|
describe('hex samples', () => {
|
||||||
const samples = fs.readFileSync(__dirname + '/samples.txt').toString().split(/\n/)
|
const samples = fs.readFileSync(__dirname + '/samples.txt').toString().split(/\n/)
|
||||||
.filter((h) => h) // filters out empty lines
|
.filter((h) => h) // filters out empty lines
|
||||||
|
@ -103,7 +125,7 @@ describe('hex samples', () => {
|
||||||
it('[' + sampleIndex + '] ' + s.toHex() + ' should decode OK', () => {
|
it('[' + sampleIndex + '] ' + s.toHex() + ' should decode OK', () => {
|
||||||
const actual = new Decoder(s, { shortForms }).next();
|
const actual = new Decoder(s, { shortForms }).next();
|
||||||
const expected = samplesExpected[sampleIndex].expected;
|
const expected = samplesExpected[sampleIndex].expected;
|
||||||
expect(Immutable.is(actual, expected),
|
expect(is(actual, expected),
|
||||||
'[' + sampleIndex + '] actual ' + util.inspect(actual) +
|
'[' + sampleIndex + '] actual ' + util.inspect(actual) +
|
||||||
', expected ' + util.inspect(expected))
|
', expected ' + util.inspect(expected))
|
||||||
.to.be.true;
|
.to.be.true;
|
||||||
|
|
Loading…
Reference in New Issue