Update JavaScript implementation and port to TypeScript
This commit is contained in:
parent
2c5ed693f5
commit
44f142d86b
|
@ -1,4 +1,3 @@
|
|||
.nyc_output/
|
||||
coverage/
|
||||
lib/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import './src/values'; // required here because it monkey-patches
|
||||
// various globals like Array, Symbol etc., and
|
||||
// we need the patching to be done here so the
|
||||
// patching is consistently visible in the
|
||||
// per-test sub-VMs.
|
||||
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "preserves",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"description": "Experimental data serialization format",
|
||||
"homepage": "https://gitlab.com/preserves/preserves",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -8,17 +8,22 @@
|
|||
"access": "public"
|
||||
},
|
||||
"repository": "gitlab:preserves/preserves",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"cover": "nyc --reporter=html mocha"
|
||||
},
|
||||
"main": "src/index.js",
|
||||
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
|
||||
"devDependencies": {
|
||||
"mocha": "^8.2.0",
|
||||
"nyc": "^14.1.1"
|
||||
"@types/jest": "^26.0.19",
|
||||
"jest": "^26.6.3",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-node-dev": "^1.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"immutable": "^4.0.0-rc.12"
|
||||
}
|
||||
"scripts": {
|
||||
"prepare": "npx tsc",
|
||||
"watch": "npx tsc -w",
|
||||
"clean": "rm -rf lib",
|
||||
"veryclean": "npm run clean && rm -rf node_modules",
|
||||
"test": "npx jest",
|
||||
"testwatch": "npx jest -w"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
"use strict";
|
||||
// Preserves Annotations.
|
||||
|
||||
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
||||
require('../package.json').version,
|
||||
'annotations.js',
|
||||
module)) return;
|
||||
|
||||
const { Record, List, Map, Set, is, hash } = require('./values.js');
|
||||
const { PreserveOn, AsPreserve } = require('./symbols.js');
|
||||
|
||||
function Annotated(item) {
|
||||
this.annotations = [];
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
Annotated.prototype[AsPreserve] = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotated.prototype[PreserveOn] = function (encoder) {
|
||||
for (const a of this.annotations) {
|
||||
encoder.header(0, 0, 5);
|
||||
encoder.push(a);
|
||||
}
|
||||
encoder.push(this.item);
|
||||
};
|
||||
|
||||
Annotated.prototype.strip = function (depth) {
|
||||
return stripAnnotations(this, depth);
|
||||
};
|
||||
|
||||
Annotated.prototype.peel = function () {
|
||||
return stripAnnotations(this, 1);
|
||||
};
|
||||
|
||||
Annotated.prototype.equals = function (other) {
|
||||
return isAnnotated(other) && is(this.item, other.item);
|
||||
};
|
||||
|
||||
Annotated.prototype.hashCode = function () {
|
||||
return hash(this.item);
|
||||
};
|
||||
|
||||
function isAnnotated(v) {
|
||||
return (v instanceof Annotated);
|
||||
}
|
||||
|
||||
function stripAnnotations(v, depth) {
|
||||
function step(v, depth) {
|
||||
if (depth === 0) return v;
|
||||
if (!isAnnotated(v)) return v;
|
||||
|
||||
const nextDepth = depth - 1;
|
||||
function walk(v) { return step(v, nextDepth); }
|
||||
|
||||
if (v.item instanceof Record) {
|
||||
return new Record(step(v.item.label, depth), v.item.fields.map(walk));
|
||||
} else if (List.isList(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (Set.isSet(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (Map.isMap(v.item)) {
|
||||
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
|
||||
} else if (isAnnotated(v.item)) {
|
||||
const e = new Error("Improper annotation structure");
|
||||
e.irritant = v;
|
||||
throw e;
|
||||
} else {
|
||||
return v.item;
|
||||
}
|
||||
}
|
||||
return step(v, (depth === void 0) ? Infinity : depth);
|
||||
}
|
||||
|
||||
function annotate(v, ...anns) {
|
||||
if (!isAnnotated(v)) {
|
||||
v = new Annotated(v);
|
||||
}
|
||||
anns.forEach((a) => v.annotations.push(a));
|
||||
return v;
|
||||
}
|
||||
|
||||
Object.assign(module.exports, {
|
||||
Annotated,
|
||||
isAnnotated,
|
||||
stripAnnotations,
|
||||
annotate,
|
||||
});
|
|
@ -1,394 +0,0 @@
|
|||
"use strict";
|
||||
// Preserves Binary codec.
|
||||
|
||||
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
||||
require('../package.json').version,
|
||||
'codec.js',
|
||||
module)) return;
|
||||
|
||||
const Values = require('./values.js');
|
||||
const Annotations = require('./annotations.js');
|
||||
const { fromJS, List, Map, Set, Bytes, Record, Single, Double } = Values;
|
||||
|
||||
const { PreserveOn } = require('./symbols.js');
|
||||
|
||||
class DecodeError extends Error {}
|
||||
class EncodeError extends Error {
|
||||
constructor(message, irritant) {
|
||||
super(message);
|
||||
this.irritant = irritant;
|
||||
}
|
||||
}
|
||||
class ShortPacket extends DecodeError {}
|
||||
|
||||
class Decoder {
|
||||
constructor(packet, options) {
|
||||
options = options || {};
|
||||
this.packet = packet
|
||||
? (packet._view || packet) // strip off Bytes wrapper, if any
|
||||
: new Uint8Array(0);
|
||||
this.index = 0;
|
||||
this.includeAnnotations = options.includeAnnotations || false;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
nextbyte() {
|
||||
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than-or-equal-to, not greater-than.
|
||||
return this.packet[this.index++];
|
||||
}
|
||||
|
||||
nextbytes(n) {
|
||||
const start = this.index;
|
||||
this.index += n;
|
||||
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than, not greater-than-or-equal-to.
|
||||
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
|
||||
}
|
||||
|
||||
wirelength(arg) {
|
||||
if (arg < 15) return arg;
|
||||
return this.varint();
|
||||
}
|
||||
|
||||
varint() {
|
||||
// TODO: Bignums :-/
|
||||
const v = this.nextbyte();
|
||||
if (v < 128) return v;
|
||||
return (this.varint() << 7) + (v - 128);
|
||||
}
|
||||
|
||||
nextvalues(n) {
|
||||
const result = [];
|
||||
for (let i = 0; i < n; i++) result.push(this.next());
|
||||
return result;
|
||||
}
|
||||
|
||||
nextop() {
|
||||
const b = this.nextbyte();
|
||||
const major = b >> 6;
|
||||
const minor = (b >> 4) & 3;
|
||||
const arg = b & 15;
|
||||
return [major, minor, arg];
|
||||
}
|
||||
|
||||
peekend() {
|
||||
const result = this.nextbyte() === 4;
|
||||
if (!result) this.index--;
|
||||
return result;
|
||||
}
|
||||
|
||||
binarystream(minor) {
|
||||
const result = [];
|
||||
while (!this.peekend()) {
|
||||
const chunk = Annotations.stripAnnotations(this.next());
|
||||
if (ArrayBuffer.isView(chunk)) {
|
||||
if (chunk.byteLength == 0) {
|
||||
throw new DecodeError("Empty binary chunks are forbidden");
|
||||
}
|
||||
result.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
|
||||
} else if (chunk instanceof Bytes) {
|
||||
if (chunk._view.length == 0) {
|
||||
throw new DecodeError("Empty binary chunks are forbidden");
|
||||
}
|
||||
result.push(chunk._view);
|
||||
} else {
|
||||
const e = new DecodeError("Unexpected non-binary chunk");
|
||||
e.irritant = chunk;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this.decodebinary(minor, Bytes.concat(result));
|
||||
}
|
||||
|
||||
valuestream(minor) {
|
||||
const result = [];
|
||||
while (!this.peekend()) result.push(this.next());
|
||||
return this.decodecompound(minor, result);
|
||||
}
|
||||
|
||||
decodeint(bs) {
|
||||
// TODO: Bignums :-/
|
||||
if (bs.length === 0) return 0;
|
||||
let acc = bs[0];
|
||||
if (acc & 0x80) acc -= 256;
|
||||
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | bs[i];
|
||||
return acc;
|
||||
}
|
||||
|
||||
decodebinary(minor, bs) {
|
||||
switch (minor) {
|
||||
case 0: return this.decodeint(bs._view);
|
||||
case 1: return bs.fromUtf8();
|
||||
case 2: return bs;
|
||||
case 3: return Symbol.for(bs.fromUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
decodecompound(minor, vs) {
|
||||
switch (minor) {
|
||||
case 0: {
|
||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
||||
return new Record(vs[0], vs.slice(1));
|
||||
}
|
||||
case 1: return List(vs);
|
||||
case 2: return Set(vs);
|
||||
case 3: return this.mapFromArray(vs);
|
||||
}
|
||||
}
|
||||
|
||||
mapFromArray(vs) {
|
||||
return Map().withMutations((m) => {
|
||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
m.set(vs[i], vs[i+1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wrap(v) {
|
||||
return this.includeAnnotations ? new Annotations.Annotated(v) : v;
|
||||
}
|
||||
|
||||
unshiftAnnotation(a, v) {
|
||||
if (this.includeAnnotations) {
|
||||
v.annotations.unshift(a);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
next() {
|
||||
while (true) { // we loop because we may need to consume an arbitrary number of no-ops
|
||||
const [major, minor, arg] = this.nextop();
|
||||
switch (major) {
|
||||
case 0:
|
||||
switch (minor) {
|
||||
case 0:
|
||||
switch (arg) {
|
||||
case 0: return this.wrap(false);
|
||||
case 1: return this.wrap(true);
|
||||
case 2: return this.wrap(Single(this.nextbytes(4).getFloat32(0, false)));
|
||||
case 3: return this.wrap(Double(this.nextbytes(8).getFloat64(0, false)));
|
||||
case 4: throw new DecodeError("Unexpected end-of-stream marker");
|
||||
case 5: {
|
||||
const a = this.next();
|
||||
const v = this.next();
|
||||
return this.unshiftAnnotation(a, v);
|
||||
}
|
||||
default: throw new DecodeError("Illegal format A lead byte");
|
||||
}
|
||||
case 1: throw new DecodeError("Illegal format A lead byte");
|
||||
case 2: {
|
||||
const t = arg >> 2;
|
||||
const n = arg & 3;
|
||||
switch (t) {
|
||||
case 1: return this.wrap(this.binarystream(n));
|
||||
case 2: return this.wrap(this.valuestream(n));
|
||||
default: throw new DecodeError("Invalid format C start byte");
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
return this.wrap((arg > 12) ? arg - 16 : arg);
|
||||
}
|
||||
case 1:
|
||||
return this.wrap(this.decodebinary(minor, Bytes.from(this.nextbytes(this.wirelength(arg)))));
|
||||
case 2:
|
||||
return this.wrap(this.decodecompound(minor, this.nextvalues(this.wirelength(arg))));
|
||||
case 3:
|
||||
if (minor === 3 && arg === 15) {
|
||||
// no-op.
|
||||
continue;
|
||||
} else {
|
||||
throw new DecodeError("Invalid lead byte (major 3)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try_next() {
|
||||
const start = this.index;
|
||||
try {
|
||||
return this.next();
|
||||
} catch (e) {
|
||||
if (e instanceof ShortPacket) {
|
||||
this.index = start;
|
||||
return void 0;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decode(bs, options) {
|
||||
return new Decoder(bs, options).next();
|
||||
}
|
||||
|
||||
function decodeWithAnnotations(bs, options) {
|
||||
options = options || {};
|
||||
options.includeAnnotations = true;
|
||||
return decode(bs, options);
|
||||
}
|
||||
|
||||
class Encoder {
|
||||
constructor(options) {
|
||||
options = options || {}
|
||||
this.chunks = [];
|
||||
this.view = new DataView(new ArrayBuffer(256));
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
contents() {
|
||||
this.rotatebuffer(4096);
|
||||
return Bytes.concat(this.chunks);
|
||||
}
|
||||
|
||||
rotatebuffer(size) {
|
||||
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
|
||||
this.view = new DataView(new ArrayBuffer(size));
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
makeroom(amount) {
|
||||
if (this.index + amount > this.view.byteLength) {
|
||||
this.rotatebuffer(amount + 4096);
|
||||
}
|
||||
}
|
||||
|
||||
emitbyte(b) {
|
||||
this.makeroom(1);
|
||||
this.view.setUint8(this.index++, b);
|
||||
}
|
||||
|
||||
emitbytes(bs) {
|
||||
this.makeroom(bs.length);
|
||||
(new Uint8Array(this.view.buffer)).set(bs, this.index);
|
||||
this.index += bs.length;
|
||||
}
|
||||
|
||||
varint(v) {
|
||||
while (v >= 128) {
|
||||
this.emitbyte((v % 128) + 128);
|
||||
v = Math.floor(v / 128);
|
||||
}
|
||||
this.emitbyte(v);
|
||||
}
|
||||
|
||||
leadbyte(major, minor, arg) {
|
||||
this.emitbyte(((major & 3) << 6) | ((minor & 3) << 4) | (arg & 15));
|
||||
}
|
||||
|
||||
header(major, minor, wirelength) {
|
||||
if (wirelength < 15) {
|
||||
this.leadbyte(major, minor, wirelength);
|
||||
} else {
|
||||
this.leadbyte(major, minor, 15);
|
||||
this.varint(wirelength);
|
||||
}
|
||||
}
|
||||
|
||||
encodeint(v) {
|
||||
// TODO: Bignums :-/
|
||||
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
|
||||
const signed_bitcount = plain_bitcount + 1;
|
||||
const bytecount = (signed_bitcount + 7) >> 3;
|
||||
this.header(1, 0, bytecount);
|
||||
const enc = (n, x) => {
|
||||
if (n > 0) {
|
||||
enc(n - 1, x >> 8);
|
||||
this.emitbyte(x & 255);
|
||||
}
|
||||
};
|
||||
enc(bytecount, v);
|
||||
}
|
||||
|
||||
encodecollection(minor, items) {
|
||||
this.header(2, minor, items.size);
|
||||
for (const item of items) { this.push(item); }
|
||||
}
|
||||
|
||||
encodestream(t, n, items) {
|
||||
const tn = ((t & 3) << 2) | (n & 3);
|
||||
this.header(0, 2, tn);
|
||||
for (const item of items) { this.push(item); }
|
||||
this.header(0, 0, 4);
|
||||
}
|
||||
|
||||
encodenoop() {
|
||||
this.leadbyte(3, 3, 15);
|
||||
}
|
||||
|
||||
push(v) {
|
||||
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
|
||||
v[PreserveOn](this);
|
||||
}
|
||||
else if (typeof v === 'boolean') {
|
||||
this.leadbyte(0, 0, v ? 1 : 0);
|
||||
}
|
||||
else if (typeof v === 'number') {
|
||||
if (v >= -3 && v <= 12) {
|
||||
this.leadbyte(0, 3, v >= 0 ? v : v + 16);
|
||||
} else {
|
||||
this.encodeint(v);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'string') {
|
||||
const bs = Bytes(v)._view;
|
||||
this.header(1, 1, bs.length);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
else if (typeof v === 'symbol') {
|
||||
const key = Symbol.keyFor(v);
|
||||
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||
const bs = Bytes(key)._view;
|
||||
this.header(1, 3, bs.length);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
else if (ArrayBuffer.isView(v)) {
|
||||
if (v instanceof Uint8Array) {
|
||||
this.header(1, 2, v.length);
|
||||
this.emitbytes(v);
|
||||
} else {
|
||||
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
||||
this.header(1, 2, bs.length);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
}
|
||||
else if (List.isList(v)) {
|
||||
this.encodecollection(1, v);
|
||||
}
|
||||
else if (Set.isSet(v)) {
|
||||
this.encodecollection(2, v);
|
||||
}
|
||||
else if (Map.isMap(v)) {
|
||||
this.encodecollection(3, List().withMutations((l) => {
|
||||
v.forEach((val, key) => { l.push(key).push(val); });
|
||||
}));
|
||||
}
|
||||
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
|
||||
this.encodestream(2, 1, v);
|
||||
}
|
||||
else {
|
||||
throw new EncodeError("Cannot encode", v);
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
}
|
||||
|
||||
function encode(v, options) {
|
||||
return new Encoder(options).push(v).contents();
|
||||
}
|
||||
|
||||
Object.assign(module.exports, {
|
||||
DecodeError,
|
||||
EncodeError,
|
||||
ShortPacket,
|
||||
Decoder,
|
||||
decode,
|
||||
decodeWithAnnotations,
|
||||
Encoder,
|
||||
encode,
|
||||
});
|
|
@ -0,0 +1,370 @@
|
|||
// Preserves Binary codec.
|
||||
|
||||
import {
|
||||
underlying,
|
||||
Annotated,
|
||||
Dictionary, Set, Bytes, Record, Single, Double,
|
||||
isSet, isDictionary,
|
||||
BytesLike,
|
||||
Value,
|
||||
} from './values';
|
||||
import { Tag } from './constants';
|
||||
|
||||
import { PreserveOn } from './symbols';
|
||||
|
||||
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
|
||||
|
||||
export function isCodecError(e: any, t?: ErrorType): e is PreservesCodecError {
|
||||
return typeof e === 'object' && e !== null &&
|
||||
'_codecErrorType' in e &&
|
||||
(!t || e._codecErrorType() === t);
|
||||
}
|
||||
|
||||
export const isDecodeError = (e: any): e is DecodeError => isCodecError(e, 'DecodeError');
|
||||
export const isEncodeError = (e: any): e is EncodeError => isCodecError(e, 'EncodeError');
|
||||
export const isShortPacket = (e: any): e is ShortPacket => isCodecError(e, 'ShortPacket');
|
||||
|
||||
export abstract class PreservesCodecError {
|
||||
abstract _codecErrorType(): ErrorType;
|
||||
}
|
||||
|
||||
export class DecodeError extends Error {
|
||||
_codecErrorType(): ErrorType { return 'DecodeError' }
|
||||
}
|
||||
|
||||
export class EncodeError extends Error {
|
||||
_codecErrorType(): ErrorType { return 'EncodeError' }
|
||||
|
||||
readonly irritant: any;
|
||||
|
||||
constructor(message: string, irritant: any) {
|
||||
super(message);
|
||||
this.irritant = irritant;
|
||||
}
|
||||
}
|
||||
|
||||
export class ShortPacket extends DecodeError {
|
||||
_codecErrorType(): ErrorType { return 'ShortPacket' }
|
||||
}
|
||||
|
||||
export interface DecoderOptions {
|
||||
includeAnnotations?: boolean;
|
||||
}
|
||||
|
||||
export class Decoder {
|
||||
packet: Uint8Array;
|
||||
index: number;
|
||||
options: DecoderOptions;
|
||||
|
||||
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) {
|
||||
this.packet = underlying(packet);
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? false;
|
||||
}
|
||||
|
||||
write(data: BytesLike) {
|
||||
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
nextbyte(): number {
|
||||
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than-or-equal-to, not greater-than.
|
||||
return this.packet[this.index++];
|
||||
}
|
||||
|
||||
nextbytes(n: number): DataView {
|
||||
const start = this.index;
|
||||
this.index += n;
|
||||
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than, not greater-than-or-equal-to.
|
||||
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
|
||||
}
|
||||
|
||||
varint(): number {
|
||||
// TODO: Bignums :-/
|
||||
const v = this.nextbyte();
|
||||
if (v < 128) return v;
|
||||
return (this.varint() << 7) + (v - 128);
|
||||
}
|
||||
|
||||
peekend(): boolean {
|
||||
const matched = this.nextbyte() === Tag.End;
|
||||
if (!matched) this.index--;
|
||||
return matched;
|
||||
}
|
||||
|
||||
nextvalues(): Value[] {
|
||||
const result = [];
|
||||
while (!this.peekend()) result.push(this.next());
|
||||
return result;
|
||||
}
|
||||
|
||||
nextint(n: number): number {
|
||||
// TODO: Bignums :-/
|
||||
if (n === 0) return 0;
|
||||
let acc = this.nextbyte();
|
||||
if (acc & 0x80) acc -= 256;
|
||||
for (let i = 1; i < n; i++) acc = (acc << 8) | this.nextbyte();
|
||||
return acc;
|
||||
}
|
||||
|
||||
wrap(v: Value): Value {
|
||||
return this.includeAnnotations ? new Annotated(v) : v;
|
||||
}
|
||||
|
||||
static dictionaryFromArray(vs: Value[]): Dictionary {
|
||||
const d = new Dictionary();
|
||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
d.set(vs[i], vs[i+1]);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
unshiftAnnotation(a: Value, v: Annotated) {
|
||||
if (this.includeAnnotations) {
|
||||
v.annotations.unshift(a);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
next(): Value {
|
||||
const tag = this.nextbyte();
|
||||
switch (tag) {
|
||||
case Tag.False: return this.wrap(false);
|
||||
case Tag.True: return this.wrap(true);
|
||||
case Tag.Float: return this.wrap(new Single(this.nextbytes(4).getFloat32(0, false)));
|
||||
case Tag.Double: return this.wrap(new Double(this.nextbytes(8).getFloat64(0, false)));
|
||||
case Tag.End: throw new DecodeError("Unexpected Compound end marker");
|
||||
case Tag.Annotation: {
|
||||
const a = this.next();
|
||||
const v = this.next() as Annotated;
|
||||
return this.unshiftAnnotation(a, v);
|
||||
}
|
||||
case Tag.SignedInteger: return this.wrap(this.nextint(this.varint()));
|
||||
case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8());
|
||||
case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint())));
|
||||
case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8()));
|
||||
case Tag.Record: {
|
||||
const vs = this.nextvalues();
|
||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
||||
return this.wrap(new Record(vs[0], vs.slice(1)));
|
||||
}
|
||||
case Tag.Sequence: return this.wrap(this.nextvalues());
|
||||
case Tag.Set: return this.wrap(new Set(this.nextvalues()));
|
||||
case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues()));
|
||||
default: {
|
||||
if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) {
|
||||
const v = tag - Tag.SmallInteger_lo;
|
||||
return this.wrap(v > 12 ? v - 16 : v);
|
||||
}
|
||||
if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) {
|
||||
const n = tag - Tag.MediumInteger_lo;
|
||||
return this.wrap(this.nextint(n + 1));
|
||||
}
|
||||
throw new DecodeError("Unsupported Preserves tag: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try_next() {
|
||||
const start = this.index;
|
||||
try {
|
||||
return this.next();
|
||||
} catch (e) {
|
||||
if (e instanceof ShortPacket) {
|
||||
this.index = start;
|
||||
return void 0;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decode(bs: BytesLike, options?: DecoderOptions) {
|
||||
return new Decoder(bs, options).next();
|
||||
}
|
||||
|
||||
export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated {
|
||||
return decode(bs, { ... options, includeAnnotations: true }) as Annotated;
|
||||
}
|
||||
|
||||
export interface EncoderOptions {
|
||||
canonical?: boolean;
|
||||
includeAnnotations?: boolean;
|
||||
}
|
||||
|
||||
export class Encoder {
|
||||
chunks: Array<Uint8Array>;
|
||||
view: DataView;
|
||||
index: number;
|
||||
options: EncoderOptions;
|
||||
|
||||
constructor(options: EncoderOptions = {}) {
|
||||
this.chunks = [];
|
||||
this.view = new DataView(new ArrayBuffer(256));
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get canonical(): boolean {
|
||||
return this.options.canonical ?? true;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? !this.canonical;
|
||||
}
|
||||
|
||||
contents(): Bytes {
|
||||
this.rotatebuffer(4096);
|
||||
return Bytes.concat(this.chunks);
|
||||
}
|
||||
|
||||
rotatebuffer(size: number) {
|
||||
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
|
||||
this.view = new DataView(new ArrayBuffer(size));
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
makeroom(amount: number) {
|
||||
if (this.index + amount > this.view.byteLength) {
|
||||
this.rotatebuffer(amount + 4096);
|
||||
}
|
||||
}
|
||||
|
||||
emitbyte(b: number) {
|
||||
this.makeroom(1);
|
||||
this.view.setUint8(this.index++, b);
|
||||
}
|
||||
|
||||
emitbytes(bs: Uint8Array) {
|
||||
this.makeroom(bs.length);
|
||||
(new Uint8Array(this.view.buffer)).set(bs, this.index);
|
||||
this.index += bs.length;
|
||||
}
|
||||
|
||||
varint(v: number) {
|
||||
while (v >= 128) {
|
||||
this.emitbyte((v % 128) + 128);
|
||||
v = Math.floor(v / 128);
|
||||
}
|
||||
this.emitbyte(v);
|
||||
}
|
||||
|
||||
encodeint(v: number) {
|
||||
// TODO: Bignums :-/
|
||||
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
|
||||
const signed_bitcount = plain_bitcount + 1;
|
||||
const bytecount = (signed_bitcount + 7) >> 3;
|
||||
if (bytecount <= 16) {
|
||||
this.emitbyte(Tag.MediumInteger_lo + bytecount - 1);
|
||||
} else {
|
||||
this.emitbyte(Tag.SignedInteger);
|
||||
this.varint(bytecount);
|
||||
}
|
||||
const enc = (n, x) => {
|
||||
if (n > 0) {
|
||||
enc(n - 1, x >> 8);
|
||||
this.emitbyte(x & 255);
|
||||
}
|
||||
};
|
||||
enc(bytecount, v);
|
||||
}
|
||||
|
||||
encodebytes(tag: Tag, bs: Uint8Array) {
|
||||
this.emitbyte(tag);
|
||||
this.varint(bs.length);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
|
||||
encodevalues(tag: Tag, items: Iterable<Value>) {
|
||||
this.emitbyte(tag);
|
||||
for (let i of items) { this.push(i); }
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
encoderawvalues(tag: Tag, items: BytesLike[]) {
|
||||
this.emitbyte(tag);
|
||||
items.forEach((i) => this.emitbytes(underlying(i)));
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
push(v: Value) {
|
||||
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
|
||||
v[PreserveOn](this);
|
||||
}
|
||||
else if (typeof v === 'boolean') {
|
||||
this.emitbyte(v ? Tag.True : Tag.False);
|
||||
}
|
||||
else if (typeof v === 'number') {
|
||||
if (v >= -3 && v <= 12) {
|
||||
this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf));
|
||||
} else {
|
||||
this.encodeint(v);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'string') {
|
||||
this.encodebytes(Tag.String, new Bytes(v)._view);
|
||||
}
|
||||
else if (typeof v === 'symbol') {
|
||||
const key = Symbol.keyFor(v);
|
||||
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||
this.encodebytes(Tag.Symbol, new Bytes(key)._view);
|
||||
}
|
||||
else if (ArrayBuffer.isView(v)) {
|
||||
if (v instanceof Uint8Array) {
|
||||
this.encodebytes(Tag.ByteString, v);
|
||||
} else {
|
||||
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
||||
this.encodebytes(Tag.ByteString, bs);
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(v)) {
|
||||
this.encodevalues(Tag.Sequence, v);
|
||||
}
|
||||
else if (isSet(v)) {
|
||||
if (this.canonical) {
|
||||
const pieces = v._map((_v, k) => encode(k, { canonical: true }));
|
||||
pieces.sort(Bytes.compare);
|
||||
this.encoderawvalues(Tag.Set, pieces);
|
||||
} else {
|
||||
this.encodevalues(Tag.Set, v);
|
||||
}
|
||||
}
|
||||
else if (isDictionary(v)) {
|
||||
if (this.canonical) {
|
||||
const pieces = v._map((v, k) => Bytes.concat([encode(k, { canonical: true }),
|
||||
encode(v, { canonical: true })]));
|
||||
pieces.sort(Bytes.compare);
|
||||
this.encoderawvalues(Tag.Dictionary, pieces);
|
||||
} else {
|
||||
this.emitbyte(Tag.Dictionary);
|
||||
v._forEach((v, k) => {
|
||||
this.push(k);
|
||||
this.push(v);
|
||||
});
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
|
||||
this.encodevalues(Tag.Sequence, v as Iterable<Value>);
|
||||
}
|
||||
else {
|
||||
throw new EncodeError("Cannot encode", v);
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
}
|
||||
|
||||
export function encode(v: Value, options?: EncoderOptions): Bytes {
|
||||
return new Encoder(options).push(v).contents();
|
||||
}
|
||||
|
||||
export function encodeWithAnnotations(v: Value, options: EncoderOptions = {}): Bytes {
|
||||
return encode(v, { ... options, includeAnnotations: true });
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
export enum Tag {
|
||||
False = 0x80,
|
||||
True,
|
||||
Float,
|
||||
Double,
|
||||
End,
|
||||
Annotation,
|
||||
|
||||
SmallInteger_lo = 0x90,
|
||||
MediumInteger_lo = 0xa0,
|
||||
|
||||
SignedInteger = 0xb0,
|
||||
String,
|
||||
ByteString,
|
||||
Symbol,
|
||||
Record,
|
||||
Sequence,
|
||||
Set,
|
||||
Dictionary,
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
||||
require('../package.json').version,
|
||||
'index.js',
|
||||
module)) return;
|
||||
|
||||
Object.assign(module.exports, require('./symbols.js'));
|
||||
Object.assign(module.exports, require('./codec.js'));
|
||||
Object.assign(module.exports, require('./values.js'));
|
||||
Object.assign(module.exports, require('./annotations.js'));
|
|
@ -0,0 +1,5 @@
|
|||
export * from './symbols';
|
||||
export * from './codec';
|
||||
export * from './values';
|
||||
export * from './text';
|
||||
export * as Constants from './constants';
|
|
@ -0,0 +1,12 @@
|
|||
// Patching to support node.js extensions.
|
||||
|
||||
import * as util from 'util';
|
||||
import { Record, Bytes, Annotated } from './values';
|
||||
|
||||
[Bytes, Annotated, Record].forEach((C) => {
|
||||
C.prototype[util.inspect.custom] = function (_depth: any, _options: any) {
|
||||
return this.asPreservesText();
|
||||
};
|
||||
});
|
||||
|
||||
Record.fallbackToString = util.inspect;
|
|
@ -1,26 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
function initialize_singleton(namespace_key_str, package_version, module_key, module_object) {
|
||||
const namespace_key = Symbol.for(namespace_key_str);
|
||||
if (!(namespace_key in global)) {
|
||||
global[namespace_key] = {
|
||||
version: package_version,
|
||||
modules: {}
|
||||
};
|
||||
}
|
||||
let cache = global[namespace_key];
|
||||
if (cache.version !== package_version) {
|
||||
console.warn('Potentially incompatible versions of ' + namespace_key_str + ' loaded:',
|
||||
cache.version,
|
||||
package_version);
|
||||
}
|
||||
if (module_key in cache.modules) {
|
||||
module_object.exports = cache.modules[module_key];
|
||||
return true;
|
||||
} else {
|
||||
cache.modules[module_key] = module_object.exports;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = initialize_singleton;
|
|
@ -1,15 +0,0 @@
|
|||
"use strict";
|
||||
// Symbols for various Preserves protocols.
|
||||
|
||||
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
||||
require('../package.json').version,
|
||||
'symbols.js',
|
||||
module)) return;
|
||||
|
||||
const PreserveOn = Symbol.for('PreserveOn');
|
||||
const AsPreserve = Symbol.for('AsPreserve');
|
||||
|
||||
Object.assign(module.exports, {
|
||||
PreserveOn,
|
||||
AsPreserve,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
// Symbols for various Preserves protocols.
|
||||
|
||||
export const PreserveOn = Symbol.for('PreserveOn');
|
||||
export const AsPreserve = Symbol.for('AsPreserve');
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Value } from './values';
|
||||
|
||||
export function stringify(x: Value): string {
|
||||
if (typeof x === 'object' && x !== null && 'asPreservesText' in x) {
|
||||
return x.asPreservesText();
|
||||
} else {
|
||||
return JSON.stringify(x);
|
||||
}
|
||||
}
|
||||
|
||||
export function preserves(pieces: TemplateStringsArray, ...values: Value[]): string {
|
||||
const result = [pieces[0]];
|
||||
values.forEach((v, i) => {
|
||||
result.push(stringify(v));
|
||||
result.push(pieces[i + 1]);
|
||||
});
|
||||
return result.join('');
|
||||
}
|
|
@ -1,422 +0,0 @@
|
|||
"use strict";
|
||||
// Preserves Values.
|
||||
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
|
||||
|
||||
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
||||
require('../package.json').version,
|
||||
'values.js',
|
||||
module)) return;
|
||||
|
||||
const util = require('util');
|
||||
|
||||
const Immutable = require('immutable');
|
||||
const { List, Map, Set, is, hash } = Immutable;
|
||||
|
||||
const { PreserveOn, AsPreserve } = require('./symbols.js');
|
||||
|
||||
function _decode(bs) {
|
||||
return Buffer.from(bs.buffer, bs.byteOffset, bs.byteLength).toString();
|
||||
}
|
||||
|
||||
const encoder = (typeof TextEncoder === 'undefined') ? { encode: Buffer.from } : new TextEncoder();
|
||||
const decoder = (typeof TextDecoder === 'undefined') ? { decode: _decode } : new TextDecoder();
|
||||
|
||||
function fromJS(x) {
|
||||
switch (typeof x) {
|
||||
case 'number':
|
||||
if (!Number.isInteger(x)) {
|
||||
// We require that clients be explicit about integer vs. non-integer types.
|
||||
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
|
||||
}
|
||||
// FALL THROUGH
|
||||
case 'string':
|
||||
case 'symbol':
|
||||
case 'boolean':
|
||||
return x;
|
||||
case 'undefined':
|
||||
throw new TypeError("Cannot represent JavaScript undefined value as Preserves");
|
||||
case 'function':
|
||||
// We are relaxed about these, for now, even though Preserves can't serialize them.
|
||||
return x;
|
||||
case 'object':
|
||||
if (x === null) {
|
||||
// We are relaxed about these, for now, even though null is
|
||||
// strictly speaking not a Preserves Value.
|
||||
return x;
|
||||
}
|
||||
if (typeof x[AsPreserve] === 'function') {
|
||||
return x[AsPreserve]();
|
||||
}
|
||||
if (Array.isArray(x)) {
|
||||
return List().withMutations((l) => {
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
l.push(fromJS(x[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
||||
return Bytes(x);
|
||||
}
|
||||
return Immutable.fromJS(x);
|
||||
}
|
||||
}
|
||||
|
||||
function _Float(value) {
|
||||
Object.defineProperty(this, 'value', {
|
||||
value: value,
|
||||
writable: false,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
function Float(value) {
|
||||
if (!(this instanceof Float)) return new Float(value);
|
||||
_Float.call(this, value);
|
||||
}
|
||||
|
||||
Float.prototype.toString = function () {
|
||||
return '' + this.value;
|
||||
};
|
||||
|
||||
Float.prototype.hashCode = function () {
|
||||
return this.value | 0; // TODO: something better?
|
||||
};
|
||||
|
||||
Float.unwrap = function (v) {
|
||||
if (typeof v === 'number') return v;
|
||||
if (v instanceof Float) return v.value;
|
||||
{
|
||||
const e = new TypeError("Float.unwrap failed");
|
||||
e.irritant = v;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
function Single(value) {
|
||||
if (!(this instanceof Single)) return new Single(value);
|
||||
_Float.call(this, value);
|
||||
}
|
||||
Single.prototype = Float(NaN);
|
||||
|
||||
Single.prototype.equals = function (other) {
|
||||
return (other instanceof Single) && (other.value === this.value);
|
||||
};
|
||||
|
||||
Single.prototype[PreserveOn] = function (encoder) {
|
||||
encoder.leadbyte(0, 0, 2);
|
||||
encoder.makeroom(4);
|
||||
encoder.view.setFloat32(encoder.index, this.value, false);
|
||||
encoder.index += 4;
|
||||
};
|
||||
|
||||
function Double(value) {
|
||||
if (!(this instanceof Double)) return new Double(value);
|
||||
_Float.call(this, value);
|
||||
}
|
||||
Double.prototype = Float(NaN);
|
||||
|
||||
Double.prototype.equals = function (other) {
|
||||
return (other instanceof Double) && (other.value === this.value);
|
||||
};
|
||||
|
||||
Double.prototype[PreserveOn] = function (encoder) {
|
||||
encoder.leadbyte(0, 0, 3);
|
||||
encoder.makeroom(8);
|
||||
encoder.view.setFloat64(encoder.index, this.value, false);
|
||||
encoder.index += 8;
|
||||
};
|
||||
|
||||
function Bytes(maybeByteIterable) {
|
||||
if (!(this instanceof Bytes)) return new Bytes(maybeByteIterable);
|
||||
if (maybeByteIterable === void 0) {
|
||||
_installView.call(this, new Uint8Array());
|
||||
} else if (ArrayBuffer.isView(maybeByteIterable)) {
|
||||
_installView.call(this, new Uint8Array(maybeByteIterable.buffer,
|
||||
maybeByteIterable.byteOffset,
|
||||
maybeByteIterable.byteLength));
|
||||
} else if (maybeByteIterable instanceof ArrayBuffer) {
|
||||
_installView.call(this, new Uint8Array(maybeByteIterable.slice()));
|
||||
} else if (typeof maybeByteIterable === 'string') {
|
||||
_installView.call(this, encoder.encode(maybeByteIterable));
|
||||
} else if (typeof maybeByteIterable === 'number') {
|
||||
_installView.call(this, new Uint8Array(maybeByteIterable));
|
||||
} else if (typeof maybeByteIterable.length === 'number') {
|
||||
_installView.call(this, Uint8Array.from(maybeByteIterable));
|
||||
} else if (maybeByteIterable instanceof Bytes) {
|
||||
_installView.call(this, maybeByteIterable._view);
|
||||
} else if (typeof maybeByteIterable.size === 'number') {
|
||||
_installView.call(this, new Uint8Array(maybeByteIterable.size));
|
||||
for (let i = 0; i < this.size; i++) { this._view[i] = maybeByteIterable.get(i); }
|
||||
} else {
|
||||
const e = new TypeError("Attempt to initialize Bytes from unsupported value");
|
||||
e.irritant = maybeByteIterable;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
|
||||
if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
|
||||
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
|
||||
}
|
||||
|
||||
Bytes.fromHex = function (s) {
|
||||
if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
|
||||
const len = s.length >> 1;
|
||||
const result = Bytes(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
result._view[i] =
|
||||
(unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Bytes.concat = function (bss) {
|
||||
let len = 0;
|
||||
for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }
|
||||
|
||||
const result = Bytes(len);
|
||||
let index = 0;
|
||||
for (let i = 0; i < bss.length; i++) {
|
||||
const bs = underlying(bss[i]);
|
||||
result._view.set(bs, index);
|
||||
index += bs.length;
|
||||
}
|
||||
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;
|
||||
const va = this._view;
|
||||
const vb = other._view;
|
||||
for (let i = 0; i < va.length; i++) {
|
||||
if (va[i] !== vb[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Bytes.prototype.hashCode = function () {
|
||||
// Immutable.js uses this function for strings.
|
||||
const v = this._view;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
hash = ((31 * hash) + v[i]) | 0;
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
Bytes.decodeUtf8 = function (bs) {
|
||||
return decoder.decode(underlying(bs));
|
||||
};
|
||||
|
||||
Bytes.prototype.fromUtf8 = function () {
|
||||
return decoder.decode(this._view);
|
||||
};
|
||||
|
||||
Bytes.prototype.toString = function () {
|
||||
return '#"' + this.__asciify() + '"';
|
||||
};
|
||||
|
||||
if (util.inspect) {
|
||||
Bytes.prototype[util.inspect.custom] = function (depth, options) {
|
||||
return this.toString();
|
||||
};
|
||||
}
|
||||
|
||||
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 () {
|
||||
var nibbles = [];
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
nibbles.push(hexDigit(this._view[i] >> 4));
|
||||
nibbles.push(hexDigit(this._view[i] & 15));
|
||||
}
|
||||
return nibbles.join('');
|
||||
};
|
||||
|
||||
Bytes.prototype[PreserveOn] = function (encoder) {
|
||||
encoder.header(1, 2, this.size);
|
||||
encoder.emitbytes(this._view);
|
||||
};
|
||||
|
||||
// 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)) {
|
||||
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) &&
|
||||
is(this.label, other.label) &&
|
||||
is(this.fields, other.fields);
|
||||
};
|
||||
|
||||
Record.prototype.hashCode = function () {
|
||||
return Immutable.List([this.label, this.fields]).hashCode();
|
||||
};
|
||||
|
||||
Record.prototype.get = function (index, defaultValue) {
|
||||
return this.fields.get(index, defaultValue);
|
||||
};
|
||||
|
||||
Record.prototype.set = function (index, newValue) {
|
||||
return new Record(this.label, this.fields.set(index, newValue));
|
||||
};
|
||||
|
||||
Record.prototype[PreserveOn] = function (encoder) {
|
||||
encoder.header(2, 0, this.fields.size + 1);
|
||||
encoder.push(this.label);
|
||||
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) => '<unprintable_preserves_field_value>'))(f);
|
||||
}
|
||||
}).join(', ') + ')';
|
||||
};
|
||||
|
||||
Record.makeConstructor = function (labelSymbolText, fieldNames) {
|
||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||
};
|
||||
|
||||
Record.makeBasicConstructor = function (label, fieldNames) {
|
||||
const arity = fieldNames.length;
|
||||
const ctor = (...fields) => {
|
||||
if (fields.length !== arity) {
|
||||
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
|
||||
" expecting " + arity + " fields with " + fields.length + " fields");
|
||||
}
|
||||
return new Record(label, fields);
|
||||
};
|
||||
ctor.constructorInfo = new RecordConstructorInfo(label, arity);
|
||||
ctor.isClassOf = (v) => ((v instanceof Record) &&
|
||||
is(label, v.label) &&
|
||||
v.fields.size === arity);
|
||||
fieldNames.forEach((name, i) => {
|
||||
ctor['_'+name] = function (r) {
|
||||
if (!ctor.isClassOf(r)) {
|
||||
throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+
|
||||
" from non-"+label.toString()+": "+(r && r.toString()));
|
||||
}
|
||||
return r.get(i);
|
||||
};
|
||||
});
|
||||
return ctor;
|
||||
};
|
||||
|
||||
function RecordConstructorInfo(label, arity) {
|
||||
this.label = label;
|
||||
this.arity = arity;
|
||||
}
|
||||
|
||||
RecordConstructorInfo.prototype.equals = function (other) {
|
||||
return (other instanceof RecordConstructorInfo) &&
|
||||
is(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, is, hash,
|
||||
Float, Single, Double,
|
||||
Bytes,
|
||||
Record, RecordConstructorInfo,
|
||||
});
|
|
@ -0,0 +1,793 @@
|
|||
// Preserves Values.
|
||||
|
||||
import { PreserveOn, AsPreserve } from './symbols';
|
||||
import { Tag } from './constants';
|
||||
import { Encoder, encode } from './codec';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
export type Value = Atom | Compound | Annotated;
|
||||
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
|
||||
export type Compound = Record | Array<Value> | Set | Dictionary;
|
||||
|
||||
export function isRecord(x: any): x is Record {
|
||||
return Array.isArray(x) && 'label' in x;
|
||||
}
|
||||
|
||||
export function fromJS(x: any): Value {
|
||||
switch (typeof x) {
|
||||
case 'number':
|
||||
if (!Number.isInteger(x)) {
|
||||
// We require that clients be explicit about integer vs. non-integer types.
|
||||
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
|
||||
}
|
||||
// FALL THROUGH
|
||||
case 'string':
|
||||
case 'symbol':
|
||||
case 'boolean':
|
||||
return x;
|
||||
|
||||
case 'undefined':
|
||||
case 'function':
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if (x === null) {
|
||||
break;
|
||||
}
|
||||
if (typeof x[AsPreserve] === 'function') {
|
||||
return x[AsPreserve]();
|
||||
}
|
||||
if (isRecord(x)) {
|
||||
return x;
|
||||
}
|
||||
if (Array.isArray(x)) {
|
||||
return x.map(fromJS);
|
||||
}
|
||||
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
||||
return Bytes.from(x);
|
||||
}
|
||||
return Dictionary.fromJS(x);
|
||||
}
|
||||
|
||||
throw new TypeError("Cannot represent JavaScript value as Preserves: " + x);
|
||||
}
|
||||
|
||||
export type FloatType = 'Single' | 'Double';
|
||||
|
||||
export abstract class Float {
|
||||
readonly value: number;
|
||||
|
||||
constructor(value: number | Float) {
|
||||
this.value = typeof value === 'number' ? value : value.value;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
return Object.is(other.constructor, this.constructor) && (other.value === this.value);
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
return (this.value | 0); // TODO: something better?
|
||||
}
|
||||
|
||||
abstract asPreservesText(): string;
|
||||
}
|
||||
|
||||
export class Single extends Float {
|
||||
[AsPreserve](): Value {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
encoder.emitbyte(Tag.Float);
|
||||
encoder.makeroom(4);
|
||||
encoder.view.setFloat32(encoder.index, this.value, false);
|
||||
encoder.index += 4;
|
||||
}
|
||||
|
||||
_floatType(): FloatType {
|
||||
return 'Single';
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '' + this.value + 'f';
|
||||
}
|
||||
}
|
||||
|
||||
export class Double extends Float {
|
||||
[AsPreserve](): Value {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
encoder.emitbyte(Tag.Double);
|
||||
encoder.makeroom(8);
|
||||
encoder.view.setFloat64(encoder.index, this.value, false);
|
||||
encoder.index += 8;
|
||||
}
|
||||
|
||||
_floatType(): FloatType {
|
||||
return 'Double';
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '' + this.value;
|
||||
}
|
||||
}
|
||||
|
||||
function isFloat(x: any, t: FloatType): x is Float {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
'value' in x && typeof x.value === 'number' &&
|
||||
'_floatType' in x && x._floatType() === t;
|
||||
}
|
||||
|
||||
export const isSingle = (x: any): x is Single => isFloat(x, 'Single');
|
||||
export const isDouble = (x: any): x is Double => isFloat(x, 'Double');
|
||||
|
||||
export type BytesLike = Bytes | Uint8Array;
|
||||
|
||||
export class Bytes {
|
||||
readonly _view: Uint8Array;
|
||||
|
||||
constructor(maybeByteIterable: any = new Uint8Array()) {
|
||||
if (isBytes(maybeByteIterable)) {
|
||||
this._view = maybeByteIterable._view;
|
||||
} else if (ArrayBuffer.isView(maybeByteIterable)) {
|
||||
this._view = new Uint8Array(maybeByteIterable.buffer,
|
||||
maybeByteIterable.byteOffset,
|
||||
maybeByteIterable.byteLength);
|
||||
} else if (maybeByteIterable instanceof ArrayBuffer) {
|
||||
this._view = new Uint8Array(maybeByteIterable.slice(0));
|
||||
} else if (typeof maybeByteIterable === 'string') {
|
||||
this._view = textEncoder.encode(maybeByteIterable);
|
||||
} else if (typeof maybeByteIterable === 'number') {
|
||||
this._view = new Uint8Array(maybeByteIterable);
|
||||
} else if (typeof maybeByteIterable.length === 'number') {
|
||||
this._view = Uint8Array.from(maybeByteIterable);
|
||||
} else {
|
||||
throw new TypeError("Attempt to initialize Bytes from unsupported value: " +
|
||||
maybeByteIterable);
|
||||
}
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this._view.length;
|
||||
}
|
||||
|
||||
static from(x: any): Bytes {
|
||||
return new Bytes(x);
|
||||
}
|
||||
|
||||
static of(...bytes: number[]): Bytes {
|
||||
return new Bytes(Uint8Array.of(...bytes));
|
||||
}
|
||||
|
||||
static fromHex(s: string): Bytes {
|
||||
if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
|
||||
const len = s.length >> 1;
|
||||
const result = new Bytes(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
result._view[i] =
|
||||
(unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static fromIO(io: string | BytesLike): string | Bytes {
|
||||
if (typeof io === 'string') return io;
|
||||
if (isBytes(io)) return io;
|
||||
if (io instanceof Uint8Array) return new Bytes(io);
|
||||
}
|
||||
|
||||
static toIO(b : string | BytesLike): string | Uint8Array {
|
||||
if (typeof b === 'string') return b;
|
||||
if (isBytes(b)) return b._view;
|
||||
if (b instanceof Uint8Array) return b;
|
||||
}
|
||||
|
||||
static concat = function (bss: BytesLike[]): Bytes {
|
||||
let len = 0;
|
||||
for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }
|
||||
|
||||
const result = new Bytes(len);
|
||||
let index = 0;
|
||||
for (let i = 0; i < bss.length; i++) {
|
||||
const bs = underlying(bss[i]);
|
||||
result._view.set(bs, index);
|
||||
index += bs.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get(index: number): number {
|
||||
return this._view[index];
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (!isBytes(other)) return false;
|
||||
if (other.length !== this.length) return false;
|
||||
const va = this._view;
|
||||
const vb = other._view;
|
||||
for (let i = 0; i < va.length; i++) {
|
||||
if (va[i] !== vb[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
// Immutable.js uses this function for strings.
|
||||
const v = this._view;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
hash = ((31 * hash) + v[i]) | 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static compare(a: Bytes, b: Bytes): number {
|
||||
if (a < b) return -1;
|
||||
if (b < a) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static decodeUtf8(bs: Bytes | Uint8Array): string {
|
||||
return textDecoder.decode(underlying(bs));
|
||||
}
|
||||
|
||||
fromUtf8(): string {
|
||||
return textDecoder.decode(this._view);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
[AsPreserve](): Value {
|
||||
return this;
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '#"' + this.__asciify() + '"';
|
||||
}
|
||||
|
||||
__asciify(): string {
|
||||
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('');
|
||||
}
|
||||
|
||||
toHex(): string {
|
||||
var nibbles = [];
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
nibbles.push(hexDigit(this._view[i] >> 4));
|
||||
nibbles.push(hexDigit(this._view[i] & 15));
|
||||
}
|
||||
return nibbles.join('');
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
encoder.emitbyte(Tag.ByteString);
|
||||
encoder.varint(this.length);
|
||||
encoder.emitbytes(this._view);
|
||||
}
|
||||
}
|
||||
|
||||
export function hexDigit(n: number): string {
|
||||
return '0123456789abcdef'[n];
|
||||
}
|
||||
|
||||
export function unhexDigit(asciiCode: number) {
|
||||
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
|
||||
if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
|
||||
if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
|
||||
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
|
||||
}
|
||||
|
||||
export function isBytes(x: any): x is Bytes {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
'_view' in x &&
|
||||
x._view instanceof Uint8Array;
|
||||
}
|
||||
|
||||
export function underlying(b: Bytes | Uint8Array): Uint8Array {
|
||||
return (b instanceof Uint8Array) ? b : b._view;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Boolean { asPreservesText(): string; }
|
||||
interface Number { asPreservesText(): string; }
|
||||
interface String { asPreservesText(): string; }
|
||||
interface Symbol { asPreservesText(): string; }
|
||||
interface Array<T> { asPreservesText(): string; }
|
||||
}
|
||||
|
||||
Boolean.prototype.asPreservesText = function (): string {
|
||||
return this ? '#t' : '#f';
|
||||
};
|
||||
|
||||
Number.prototype.asPreservesText = function (): string {
|
||||
return '' + this;
|
||||
};
|
||||
|
||||
String.prototype.asPreservesText = function (): string {
|
||||
return JSON.stringify(this);
|
||||
};
|
||||
|
||||
Symbol.prototype.asPreservesText = function (): string {
|
||||
// TODO: escaping
|
||||
return this.description;
|
||||
};
|
||||
|
||||
Array.prototype.asPreservesText = function (): string {
|
||||
return '[' + this.map((i: Value) => i.asPreservesText()).join(', ') + ']';
|
||||
};
|
||||
|
||||
// Uint8Array / TypedArray methods
|
||||
|
||||
export interface Bytes {
|
||||
entries(): IterableIterator<[number, number]>;
|
||||
every(predicate: (value: number, index: number, array: Uint8Array) => unknown,
|
||||
thisArg?: any): boolean;
|
||||
find(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
|
||||
thisArg?: any): number;
|
||||
findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
|
||||
thisArg?: any): number;
|
||||
forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void,
|
||||
thisArg?: any): void;
|
||||
includes(searchElement: number, fromIndex?: number): boolean;
|
||||
indexOf(searchElement: number, fromIndex?: number): number;
|
||||
join(separator?: string): string;
|
||||
keys(): IterableIterator<number>;
|
||||
lastIndexOf(searchElement: number, fromIndex?: number): number;
|
||||
reduce(callbackfn: (previousValue: number,
|
||||
currentValue: number,
|
||||
currentIndex: number,
|
||||
array: Uint8Array) => number,
|
||||
initialValue?: number): number;
|
||||
reduceRight(callbackfn: (previousValue: number,
|
||||
currentValue: number,
|
||||
currentIndex: number,
|
||||
array: Uint8Array) => number,
|
||||
initialValue?: number): number;
|
||||
some(predicate: (value: number, index: number, array: Uint8Array) => unknown,
|
||||
thisArg?: any): boolean;
|
||||
toLocaleString(): string;
|
||||
values(): IterableIterator<number>;
|
||||
|
||||
filter(predicate: (value: number, index: number, array: Uint8Array) => any,
|
||||
thisArg?: any): Bytes;
|
||||
map(callbackfn: (value: number, index: number, array: Uint8Array) => number,
|
||||
thisArg?: any): Bytes;
|
||||
slice(start?: number, end?: number): Bytes;
|
||||
subarray(begin?: number, end?: number): Bytes;
|
||||
|
||||
reverse(): Bytes;
|
||||
sort(compareFn?: (a: number, b: number) => number): Bytes;
|
||||
|
||||
[Symbol.iterator](): IterableIterator<number>;
|
||||
}
|
||||
|
||||
(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: any[]) { return this._view[k](...args); };
|
||||
}
|
||||
|
||||
for (const k of `filter map slice subarray`.split(/\s+/))
|
||||
{
|
||||
Bytes.prototype[k] = function (...args: any[]) { return new Bytes(this._view[k](...args)); };
|
||||
}
|
||||
|
||||
for (const k of `reverse sort`.split(/\s+/))
|
||||
{
|
||||
Bytes.prototype[k] = function (...args: any[]) { return new Bytes(this._view.slice()[k](...args)); };
|
||||
}
|
||||
|
||||
Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); };
|
||||
})();
|
||||
|
||||
export class Record extends Array<Value> {
|
||||
readonly label: Value;
|
||||
|
||||
constructor(label: Value, fieldsJS: any[]) {
|
||||
if (arguments.length === 1) {
|
||||
// Using things like someRecord.map() involves the runtime
|
||||
// apparently instantiating instances of this.constructor
|
||||
// as if it were just plain old Array, so we have to be
|
||||
// somewhat calling-convention-compatible. This is
|
||||
// something that isn't part of the user-facing API.
|
||||
super(label);
|
||||
return;
|
||||
}
|
||||
|
||||
super(fieldsJS.length);
|
||||
fieldsJS.forEach((f, i) => this[i] = fromJS(f));
|
||||
this.label = label;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
get(index: number, defaultValue?: Value): Value {
|
||||
return (index < this.length) ? this[index] : defaultValue;
|
||||
}
|
||||
|
||||
set(index: number, newValue: Value): Record {
|
||||
return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f));
|
||||
}
|
||||
|
||||
getConstructorInfo(): RecordConstructorInfo {
|
||||
return { label: this.label, arity: this.length };
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
return isRecord(other) &&
|
||||
is(this.label, other.label) &&
|
||||
this.every((f, i) => is(f, other.get(i)));
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
let h = hash(this.label);
|
||||
this.forEach((f) => h = ((31 * h) + hash(f)) | 0);
|
||||
return h;
|
||||
}
|
||||
|
||||
static fallbackToString: (f: Value) => string = (_f) => '<unprintable_preserves_field_value>';
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
if (!('label' in this)) {
|
||||
// A quasi-Array from someRecord.map() or similar. See constructor.
|
||||
return super.toString();
|
||||
}
|
||||
return this.label.asPreservesText() +
|
||||
'(' + this.map((f) => {
|
||||
try {
|
||||
return f.asPreservesText();
|
||||
} catch (e) {
|
||||
return Record.fallbackToString(f);
|
||||
}
|
||||
}).join(', ') + ')';
|
||||
}
|
||||
|
||||
static makeConstructor(labelSymbolText: string, fieldNames: string[]) {
|
||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||
}
|
||||
|
||||
static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor {
|
||||
const label = fromJS(label0);
|
||||
const arity = fieldNames.length;
|
||||
const ctor = (...fields: any[]) => {
|
||||
if (fields.length !== arity) {
|
||||
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
|
||||
" expecting " + arity + " fields with " + fields.length + " fields");
|
||||
}
|
||||
return new Record(label, fields);
|
||||
};
|
||||
ctor.constructorInfo = { label, arity };
|
||||
ctor.isClassOf =
|
||||
(v: any): v is Record => (isRecord(v) && is(label, v.label) && v.length === arity);
|
||||
ctor._ = {};
|
||||
fieldNames.forEach((name, i) => {
|
||||
ctor._[name] = function (r: any): Value {
|
||||
if (!ctor.isClassOf(r)) {
|
||||
throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+
|
||||
" from non-"+label.toString()+": "+(r && r.toString()));
|
||||
}
|
||||
return r.get(i);
|
||||
};
|
||||
});
|
||||
return ctor;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
encoder.emitbyte(Tag.Record);
|
||||
encoder.push(this.label);
|
||||
this.forEach((f) => encoder.push(f));
|
||||
encoder.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecordConstructor {
|
||||
(...fields: any[]): Record;
|
||||
constructorInfo: RecordConstructorInfo;
|
||||
isClassOf(v: any): v is Record;
|
||||
_: { [getter: string]: (r: any) => Value };
|
||||
}
|
||||
|
||||
export interface RecordConstructorInfo {
|
||||
label: Value;
|
||||
arity: number;
|
||||
}
|
||||
|
||||
export function is(a: Value, b: Value): boolean {
|
||||
if (Object.is(a, b)) return true;
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (typeof a === 'object') {
|
||||
if (a === null || b === null) return false;
|
||||
if ('equals' in a) return a.equals(b);
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hash(a: Value): number {
|
||||
throw new Error("shouldBeImplemented"); // TODO
|
||||
}
|
||||
|
||||
export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
|
||||
return (isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
|
||||
}
|
||||
|
||||
export type DictionaryType = 'Dictionary' | 'Set';
|
||||
|
||||
export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
'_items' in x &&
|
||||
'_dictionaryType' in x &&
|
||||
x._dictionaryType() === t;
|
||||
}
|
||||
|
||||
export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary');
|
||||
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
|
||||
|
||||
export type DictionaryEntry = [Value, Value];
|
||||
|
||||
export abstract class _Dictionary {
|
||||
_items: { [key: string]: DictionaryEntry } = {};
|
||||
|
||||
_key(key: Value): string {
|
||||
const bs = encode(key, { canonical: true })._view;
|
||||
const s = String.fromCharCode.apply(null, bs);
|
||||
return s;
|
||||
}
|
||||
|
||||
_lookup(key: Value): DictionaryEntry | null {
|
||||
const k = this._key(key);
|
||||
return k in this._items ? this._items[k] : null;
|
||||
}
|
||||
|
||||
_set(key: Value, value: Value) {
|
||||
this._items[this._key(key)] = [key, value];
|
||||
}
|
||||
|
||||
_get(key: Value, defaultValue?: Value): Value {
|
||||
const k = this._key(key);
|
||||
return k in this._items ? this._items[k][1] : defaultValue;
|
||||
}
|
||||
|
||||
_delete(key: Value) {
|
||||
delete this._items[this._key(key)];
|
||||
}
|
||||
|
||||
_forEach(f: (value: Value, key: Value) => void) {
|
||||
for (let ks in this._items) {
|
||||
const [k, v] = this._items[ks];
|
||||
f(v, k);
|
||||
}
|
||||
}
|
||||
|
||||
_map<T>(f: (value: Value, key: Value) => T): Array<T> {
|
||||
const result = [];
|
||||
for (let ks in this._items) {
|
||||
const [k, v] = this._items[ks];
|
||||
result.push(f(v, k));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (!Object.is(other.constructor, this.constructor)) return false;
|
||||
const es1 = Object.entries(this._items);
|
||||
if (es1.length !== Object.entries(other._items).length) return false;
|
||||
for (let [ks1, e1] of es1) {
|
||||
const e2 = other._items[ks1];
|
||||
if (!is(e1[1], e2[1])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
abstract asPreservesText(): string;
|
||||
abstract _dictionaryType(): DictionaryType;
|
||||
}
|
||||
|
||||
export class Dictionary extends _Dictionary {
|
||||
static fromJS(x: object): Dictionary {
|
||||
if (isDictionary(x)) return x;
|
||||
const d = new Dictionary();
|
||||
for (let key in x) {
|
||||
const value = x[key];
|
||||
d._set(key, fromJS(value));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_dictionaryType(): DictionaryType {
|
||||
return 'Dictionary';
|
||||
}
|
||||
|
||||
set(key: Value, value: Value) {
|
||||
this._set(key, value);
|
||||
}
|
||||
|
||||
get(key: Value, defaultValue?: Value): Value {
|
||||
return this._get(key, defaultValue);
|
||||
}
|
||||
|
||||
delete(key: Value) {
|
||||
this._delete(key);
|
||||
}
|
||||
|
||||
mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary {
|
||||
const result = new Dictionary();
|
||||
for (let ks in this._items) {
|
||||
const oldEntry = this._items[ks];
|
||||
const newEntry = f(oldEntry);
|
||||
result._set(newEntry[0], newEntry[1])
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
forEach(f: (value: Value, key: Value) => void) {
|
||||
this._forEach(f);
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '{' +
|
||||
this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
export class Set extends _Dictionary implements Iterable<Value> {
|
||||
constructor(items: Iterable<any> = []) {
|
||||
super();
|
||||
for (let item of items) this.add(fromJS(item));
|
||||
}
|
||||
|
||||
_dictionaryType(): DictionaryType {
|
||||
return 'Set';
|
||||
}
|
||||
|
||||
add(v: Value) {
|
||||
this._set(v, true);
|
||||
}
|
||||
|
||||
delete(v: Value) {
|
||||
this._delete(v);
|
||||
}
|
||||
|
||||
includes(key: Value) {
|
||||
return this._lookup(key) !== null;
|
||||
}
|
||||
|
||||
forEach(f: (value: Value) => void) {
|
||||
this._forEach((_v, k) => f(k));
|
||||
}
|
||||
|
||||
map(f: (value: Value) => Value): Set {
|
||||
const result = new Set();
|
||||
for (let ks in this._items) {
|
||||
const k = this._items[ks][0];
|
||||
result._set(f(k), true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<Value> {
|
||||
return this._map((_v, k) => k)[Symbol.iterator]();
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '#{' +
|
||||
this._map((_v, k) => k.asPreservesText()).join(', ') +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
export function isAnnotated(x: any): x is Annotated {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
'annotations' in x &&
|
||||
'item' in x;
|
||||
}
|
||||
|
||||
export class Annotated {
|
||||
readonly annotations: Array<Value>;
|
||||
readonly item: Value;
|
||||
|
||||
constructor(item: Value) {
|
||||
this.annotations = [];
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
[AsPreserve](): Value {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
if (encoder.includeAnnotations) {
|
||||
for (const a of this.annotations) {
|
||||
encoder.emitbyte(Tag.Annotation);
|
||||
encoder.push(a);
|
||||
}
|
||||
}
|
||||
encoder.push(this.item);
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
return isAnnotated(other) && is(this.item, other.item);
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
return hash(this.item);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
const anns = this.annotations.map((a) => '@' + a.asPreservesText()).join(' ');
|
||||
return (anns ? anns + ' ' : anns) + this.item.asPreservesText();
|
||||
}
|
||||
}
|
||||
|
||||
export function peel(v: Value): Value {
|
||||
return strip(v, 1);
|
||||
}
|
||||
|
||||
export function strip(v: Value, depth: number = Infinity) {
|
||||
function step(v: Value, depth: number): Value {
|
||||
if (depth === 0) return v;
|
||||
if (!isAnnotated(v)) return v;
|
||||
|
||||
const nextDepth = depth - 1;
|
||||
function walk(v: Value) { return step(v, nextDepth); }
|
||||
|
||||
if (isRecord(v.item)) {
|
||||
return new Record(step(v.item.label, depth), v.item.map(walk));
|
||||
} else if (Array.isArray(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (isSet(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (isDictionary(v.item)) {
|
||||
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
|
||||
} else if (isAnnotated(v.item)) {
|
||||
throw new Error("Improper annotation structure");
|
||||
} else {
|
||||
return v.item;
|
||||
}
|
||||
}
|
||||
return step(v, depth);
|
||||
}
|
||||
|
||||
export function annotate(v0: Value, ...anns: Value[]) {
|
||||
const v = isAnnotated(v0) ? v0 : new Annotated(v0);
|
||||
anns.forEach((a) => v.annotations.push(a));
|
||||
return v;
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { Bytes, fromJS } from '../src/values';
|
||||
import './test-utils';
|
||||
|
||||
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())))
|
||||
.is(fromJS([[0,10],[1,20],[2,30],[3,40]]));
|
||||
});
|
||||
it('should implement every', () => {
|
||||
expect(bs.every((b) => !(b & 1))).toBe(true);
|
||||
expect(bs.every((b) => b !== 50)).toBe(true);
|
||||
expect(!(bs.every((b) => b !== 20))).toBe(true);
|
||||
});
|
||||
// it('should implement find', () => {
|
||||
// assert.strictEqual(bs.find((b) => b > 20), 30);
|
||||
// assert.strictEqual(bs.find((b) => b > 50), void 0);
|
||||
// });
|
||||
// it('should implement findIndex', () => {
|
||||
// assert.strictEqual(bs.findIndex((b) => b > 20), 2);
|
||||
// assert.strictEqual(bs.findIndex((b) => b > 50), -1);
|
||||
// });
|
||||
// it('should implement forEach', () => {
|
||||
// const vs = [];
|
||||
// bs.forEach((b) => vs.push(b));
|
||||
// assert(is(fromJS(vs), fromJS([10, 20, 30, 40])));
|
||||
// });
|
||||
// it('should implement includes', () => {
|
||||
// assert(bs.includes(20));
|
||||
// assert(!bs.includes(50));
|
||||
// });
|
||||
// it('should implement indexOf', () => {
|
||||
// assert.strictEqual(bs.indexOf(20), 1);
|
||||
// assert.strictEqual(bs.indexOf(50), -1);
|
||||
// });
|
||||
// it('should implement join', () => assert.strictEqual(bs.join('-'), '10-20-30-40'));
|
||||
// it('should implement keys', () => {
|
||||
// assert(is(fromJS(Array.from(bs.keys())), fromJS([0,1,2,3])));
|
||||
// });
|
||||
// it('should implement values', () => {
|
||||
// assert(is(fromJS(Array.from(bs.values())), fromJS([10,20,30,40])));
|
||||
// });
|
||||
// it('should implement filter', () => {
|
||||
// assert(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40)));
|
||||
// });
|
||||
// it('should implement slice', () => {
|
||||
// const vs = bs.slice(2);
|
||||
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
// assert.strictEqual(vs._view.buffer.byteLength, 2);
|
||||
// assert.strictEqual(vs.get(0), 30);
|
||||
// assert.strictEqual(vs.get(1), 40);
|
||||
// assert.strictEqual(vs.size, 2);
|
||||
// });
|
||||
// it('should implement subarray', () => {
|
||||
// const vs = bs.subarray(2);
|
||||
// assert(Object.is(vs._view.buffer, bs._view.buffer));
|
||||
// assert.strictEqual(vs._view.buffer.byteLength, 4);
|
||||
// assert.strictEqual(vs.get(0), 30);
|
||||
// assert.strictEqual(vs.get(1), 40);
|
||||
// assert.strictEqual(vs.size, 2);
|
||||
// });
|
||||
// it('should implement reverse', () => {
|
||||
// const vs = bs.reverse();
|
||||
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
// assert.strictEqual(bs.get(0), 10);
|
||||
// assert.strictEqual(bs.get(3), 40);
|
||||
// assert.strictEqual(vs.get(0), 40);
|
||||
// assert.strictEqual(vs.get(3), 10);
|
||||
// });
|
||||
// it('should implement sort', () => {
|
||||
// const vs = bs.reverse().sort();
|
||||
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
// assert.strictEqual(bs.get(0), 10);
|
||||
// assert.strictEqual(bs.get(3), 40);
|
||||
// assert.strictEqual(vs.get(0), 10);
|
||||
// assert.strictEqual(vs.get(3), 40);
|
||||
// });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
import {
|
||||
Value,
|
||||
Dictionary,
|
||||
decode, decodeWithAnnotations, encodeWithAnnotations,
|
||||
isDecodeError, isShortPacket,
|
||||
Bytes, Record,
|
||||
annotate,
|
||||
strip, peel,
|
||||
preserves,
|
||||
} from '../src/index';
|
||||
import './test-utils';
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
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).toEqual(Symbol.for('discard'));
|
||||
expect(Capture.constructorInfo.label).toEqual(Symbol.for('capture'));
|
||||
expect(Observe.constructorInfo.label).toEqual(Symbol.for('observe'));
|
||||
expect(Discard.constructorInfo.arity).toEqual(0);
|
||||
expect(Capture.constructorInfo.arity).toEqual(1);
|
||||
expect(Observe.constructorInfo.arity).toEqual(1);
|
||||
});
|
||||
})
|
||||
|
||||
describe('RecordConstructorInfo', () => {
|
||||
const C1 = Record.makeBasicConstructor([1], ['x', 'y']);
|
||||
const C2 = Record.makeBasicConstructor([1], ['z', 'w']);
|
||||
it('instance comparison should ignore pointer and fieldname differences', () => {
|
||||
expect(C1(9,9)).is(C2(9,9));
|
||||
expect(C1(9,9)).not.is(C2(9,8));
|
||||
});
|
||||
it('comparison based on pointer equality should not work', () => {
|
||||
expect(C1.constructorInfo).not.toBe(C2.constructorInfo);
|
||||
});
|
||||
it('comparison based on .equals should work', () => {
|
||||
expect(C1.constructorInfo).toEqual(C2.constructorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('records', () => {
|
||||
it('should have correct getConstructorInfo', () => {
|
||||
expect(Discard().getConstructorInfo()).toEqual(Discard.constructorInfo);
|
||||
expect(Capture(Discard()).getConstructorInfo()).toEqual(Capture.constructorInfo);
|
||||
expect(Observe(Capture(Discard())).getConstructorInfo()).toEqual(Observe.constructorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsing from subarray', () => {
|
||||
it('should maintain alignment of nextbytes', () => {
|
||||
const u = Uint8Array.of(1, 1, 1, 1, 0xb1, 0x03, 0x33, 0x33, 0x33);
|
||||
const bs = Bytes.from(u.subarray(4));
|
||||
expect(decode(bs)).is("333");
|
||||
});
|
||||
});
|
||||
|
||||
describe('common test suite', () => {
|
||||
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
|
||||
const samples = decodeWithAnnotations(samples_bin);
|
||||
|
||||
const TestCases = Record.makeConstructor('TestCases', ['cases']);
|
||||
|
||||
function DS(bs: Bytes) {
|
||||
return decode(bs);
|
||||
}
|
||||
function D(bs: Bytes) {
|
||||
return decodeWithAnnotations(bs);
|
||||
}
|
||||
function E(v: Value) {
|
||||
return encodeWithAnnotations(v);
|
||||
}
|
||||
|
||||
const expectedValues = {
|
||||
annotation1: { forward: annotate(9, "abc"),
|
||||
back: 9 },
|
||||
annotation2: { forward: annotate([[], annotate([], "x")], "abc", "def"),
|
||||
back: [[], []] },
|
||||
annotation3: { forward: annotate(5,
|
||||
annotate(2, 1),
|
||||
annotate(4, 3)),
|
||||
back: 5 },
|
||||
annotation5: { forward: annotate(new Record(Symbol.for('R'),
|
||||
[annotate(Symbol.for('f'),
|
||||
Symbol.for('af'))]),
|
||||
Symbol.for('ar')),
|
||||
back: new Record(Symbol.for('R'), [Symbol.for('f')]) },
|
||||
annotation6: { forward: new Record(annotate(Symbol.for('R'),
|
||||
Symbol.for('ar')),
|
||||
[annotate(Symbol.for('f'),
|
||||
Symbol.for('af'))]),
|
||||
back: new Record(Symbol.for('R'), [Symbol.for('f')]) },
|
||||
annotation7: { forward: annotate([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
|
||||
back: [] },
|
||||
list1: { forward: [1, 2, 3, 4],
|
||||
back: [1, 2, 3, 4] },
|
||||
record2: { value: Observe(new Record(Symbol.for("speak"), [
|
||||
Discard(),
|
||||
Capture(Discard())
|
||||
])) },
|
||||
};
|
||||
|
||||
type Variety = 'normal' | 'nondeterministic' | 'decode';
|
||||
|
||||
function runTestCase(variety: Variety, tName: string, binaryForm: Bytes, annotatedTextForm: Value) {
|
||||
describe(tName, () => {
|
||||
const textForm = strip(annotatedTextForm);
|
||||
const {forward, back} = (function () {
|
||||
const entry = expectedValues[tName] || {value: textForm};
|
||||
if ('value' in entry) {
|
||||
return {forward: entry.value, back: entry.value};
|
||||
} else if ('forward' in entry && 'back' in entry) {
|
||||
return entry;
|
||||
} else {
|
||||
throw new Error('Invalid expectedValues entry for ' + tName);
|
||||
}
|
||||
})();
|
||||
it('should match the expected value', () => expect(textForm).is(back));
|
||||
it('should round-trip', () => expect(DS(E(textForm))).is(back));
|
||||
it('should go forward', () => expect(DS(E(forward))).is(back));
|
||||
it('should go back', () => expect(DS(binaryForm)).is(back));
|
||||
it('should go back with annotations',
|
||||
() => expect(D(E(annotatedTextForm))).is(annotatedTextForm));
|
||||
if (variety !== 'decode' && variety !== 'nondeterministic') {
|
||||
it('should encode correctly', () => expect(E(forward)).is(binaryForm));
|
||||
it('should encode correctly with annotations',
|
||||
() => expect(E(annotatedTextForm)).is(binaryForm));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary;
|
||||
tests.forEach((t0: Value, tName0: Value) => {
|
||||
const tName = Symbol.keyFor(strip(tName0) as symbol);
|
||||
const t = peel(t0) as Record;
|
||||
switch (t.label) {
|
||||
case Symbol.for('Test'):
|
||||
runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);
|
||||
break;
|
||||
case Symbol.for('NondeterministicTest'):
|
||||
runTestCase('nondeterministic', tName, strip(t[0]) as Bytes, t[1]);
|
||||
break;
|
||||
case Symbol.for('DecodeTest'):
|
||||
runTestCase('decode', tName, strip(t[0]) as Bytes, t[1]);
|
||||
break;
|
||||
case Symbol.for('DecodeError'):
|
||||
describe(tName, () => {
|
||||
it('should fail with DecodeError', () => {
|
||||
expect(() => D(strip(t[0]) as Bytes))
|
||||
.toThrowFilter(e => isDecodeError(e) && !isShortPacket(e));
|
||||
});
|
||||
});
|
||||
break;
|
||||
case Symbol.for('DecodeEOF'): // fall through
|
||||
case Symbol.for('DecodeShort'):
|
||||
describe(tName, () => {
|
||||
it('should fail with ShortPacket', () => {
|
||||
expect(() => D(strip(t[0]) as Bytes))
|
||||
.toThrowFilter(e => isShortPacket(e));
|
||||
});
|
||||
});
|
||||
break;
|
||||
case Symbol.for('ParseError'):
|
||||
case Symbol.for('ParseEOF'):
|
||||
case Symbol.for('ParseShort'):
|
||||
/* Skipped for now, until we have an implementation of text syntax */
|
||||
break;
|
||||
default:{
|
||||
const e = new Error(preserves`Unsupported test kind ${t}`);
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const assert = require('assert');
|
||||
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', () => {
|
||||
assert(is(fromJS(Array.from(bs.entries())),
|
||||
fromJS([[0,10],[1,20],[2,30],[3,40]])));
|
||||
});
|
||||
it('should implement every', () => {
|
||||
assert(bs.every((b) => !(b & 1)));
|
||||
assert(bs.every((b) => b !== 50));
|
||||
assert(!(bs.every((b) => b !== 20)));
|
||||
});
|
||||
it('should implement find', () => {
|
||||
assert.strictEqual(bs.find((b) => b > 20), 30);
|
||||
assert.strictEqual(bs.find((b) => b > 50), void 0);
|
||||
});
|
||||
it('should implement findIndex', () => {
|
||||
assert.strictEqual(bs.findIndex((b) => b > 20), 2);
|
||||
assert.strictEqual(bs.findIndex((b) => b > 50), -1);
|
||||
});
|
||||
it('should implement forEach', () => {
|
||||
const vs = [];
|
||||
bs.forEach((b) => vs.push(b));
|
||||
assert(is(fromJS(vs), fromJS([10, 20, 30, 40])));
|
||||
});
|
||||
it('should implement includes', () => {
|
||||
assert(bs.includes(20));
|
||||
assert(!bs.includes(50));
|
||||
});
|
||||
it('should implement indexOf', () => {
|
||||
assert.strictEqual(bs.indexOf(20), 1);
|
||||
assert.strictEqual(bs.indexOf(50), -1);
|
||||
});
|
||||
it('should implement join', () => assert.strictEqual(bs.join('-'), '10-20-30-40'));
|
||||
it('should implement keys', () => {
|
||||
assert(is(fromJS(Array.from(bs.keys())), fromJS([0,1,2,3])));
|
||||
});
|
||||
it('should implement values', () => {
|
||||
assert(is(fromJS(Array.from(bs.values())), fromJS([10,20,30,40])));
|
||||
});
|
||||
it('should implement filter', () => {
|
||||
assert(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40)));
|
||||
});
|
||||
it('should implement slice', () => {
|
||||
const vs = bs.slice(2);
|
||||
assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
assert.strictEqual(vs._view.buffer.byteLength, 2);
|
||||
assert.strictEqual(vs.get(0), 30);
|
||||
assert.strictEqual(vs.get(1), 40);
|
||||
assert.strictEqual(vs.size, 2);
|
||||
});
|
||||
it('should implement subarray', () => {
|
||||
const vs = bs.subarray(2);
|
||||
assert(Object.is(vs._view.buffer, bs._view.buffer));
|
||||
assert.strictEqual(vs._view.buffer.byteLength, 4);
|
||||
assert.strictEqual(vs.get(0), 30);
|
||||
assert.strictEqual(vs.get(1), 40);
|
||||
assert.strictEqual(vs.size, 2);
|
||||
});
|
||||
it('should implement reverse', () => {
|
||||
const vs = bs.reverse();
|
||||
assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
assert.strictEqual(bs.get(0), 10);
|
||||
assert.strictEqual(bs.get(3), 40);
|
||||
assert.strictEqual(vs.get(0), 40);
|
||||
assert.strictEqual(vs.get(3), 10);
|
||||
});
|
||||
it('should implement sort', () => {
|
||||
const vs = bs.reverse().sort();
|
||||
assert(!Object.is(vs._view.buffer, bs._view.buffer));
|
||||
assert.strictEqual(bs.get(0), 10);
|
||||
assert.strictEqual(bs.get(3), 40);
|
||||
assert.strictEqual(vs.get(0), 10);
|
||||
assert.strictEqual(vs.get(3), 40);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,240 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const assert = require('assert');
|
||||
const Immutable = require('immutable');
|
||||
|
||||
const Preserves = require('../src/index.js');
|
||||
const {
|
||||
is, List, Set, Map,
|
||||
Decoder, Encoder, decode, decodeWithAnnotations, encode,
|
||||
DecodeError, EncodeError, ShortPacket,
|
||||
Bytes, Record, Single, Double,
|
||||
annotate,
|
||||
stripAnnotations,
|
||||
PreserveOn,
|
||||
} = Preserves;
|
||||
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
|
||||
const Discard = Record.makeConstructor('discard', []);
|
||||
const Capture = Record.makeConstructor('capture', ['pattern']);
|
||||
const Observe = Record.makeConstructor('observe', ['pattern']);
|
||||
|
||||
describe('record constructors', () => {
|
||||
it('should have constructorInfo', () => {
|
||||
assert.strictEqual(Discard.constructorInfo.label, Symbol.for('discard'));
|
||||
assert.strictEqual(Capture.constructorInfo.label, Symbol.for('capture'));
|
||||
assert.strictEqual(Observe.constructorInfo.label, Symbol.for('observe'));
|
||||
assert.strictEqual(Discard.constructorInfo.arity, 0);
|
||||
assert.strictEqual(Capture.constructorInfo.arity, 1);
|
||||
assert.strictEqual(Observe.constructorInfo.arity, 1);
|
||||
});
|
||||
})
|
||||
|
||||
describe('RecordConstructorInfo', () => {
|
||||
const C1 = Record.makeBasicConstructor(Immutable.List([1]), ['x', 'y']);
|
||||
const C2 = Record.makeBasicConstructor(Immutable.List([1]), ['z', 'w']);
|
||||
it('instance comparison should ignore pointer and fieldname differences', () => {
|
||||
assert(is(C1(9,9), C2(9,9)));
|
||||
assert(!is(C1(9,9), C2(9,8)));
|
||||
});
|
||||
it('comparison based on pointer equality should not work', () => {
|
||||
assert.notStrictEqual(C1.constructorInfo, C2.constructorInfo);
|
||||
});
|
||||
it('comparison based on .equals should work', () => {
|
||||
assert(is(C1.constructorInfo, C2.constructorInfo));
|
||||
});
|
||||
});
|
||||
|
||||
describe('records', () => {
|
||||
it('should have correct getConstructorInfo', () => {
|
||||
assert(Discard().getConstructorInfo().equals(Discard.constructorInfo));
|
||||
assert(Capture(Discard()).getConstructorInfo().equals(Capture.constructorInfo));
|
||||
assert(Observe(Capture(Discard())).getConstructorInfo().equals(Observe.constructorInfo));
|
||||
assert(is(Observe(Capture(Discard())).getConstructorInfo(), Observe.constructorInfo));
|
||||
});
|
||||
});
|
||||
|
||||
class SimpleStream {
|
||||
constructor(t, n, items) {
|
||||
this.t = t;
|
||||
this.n = n;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
[PreserveOn](e) {
|
||||
e.encodestream(this.t, this.n, this.items);
|
||||
}
|
||||
}
|
||||
|
||||
class StringStream extends SimpleStream { constructor(items) { super(1, 1, items); }}
|
||||
class BytesStream extends SimpleStream { constructor(items) { super(1, 2, items); }}
|
||||
class SymbolStream extends SimpleStream { constructor(items) { super(1, 3, items); }}
|
||||
class RecordStream extends SimpleStream { constructor(items) { super(2, 0, items); }}
|
||||
// Not needed -- an ordinary array will do!
|
||||
// class SequenceStream extends SimpleStream { constructor(items) { super(2, 1, items); }}
|
||||
class SetStream extends SimpleStream { constructor(items) { super(2, 2, items); }}
|
||||
class DictionaryStream extends SimpleStream { constructor(items) { super(2, 3, items); }}
|
||||
|
||||
describe('common test suite', () => {
|
||||
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
|
||||
const samples = decodeWithAnnotations(samples_bin);
|
||||
|
||||
const TestCases = Record.makeConstructor('TestCases', ['cases']);
|
||||
|
||||
function DS(bs) {
|
||||
return decode(bs);
|
||||
}
|
||||
function D(bs) {
|
||||
return decodeWithAnnotations(bs);
|
||||
}
|
||||
function E(v) {
|
||||
return encode(v);
|
||||
}
|
||||
|
||||
const expectedValues = {
|
||||
annotation1: { forward: annotate(9, "abc"),
|
||||
back: 9 },
|
||||
annotation2: { forward: annotate(List([List(), annotate(List(), "x")]), "abc", "def"),
|
||||
back: List([List(), List()]) },
|
||||
annotation3: { forward: annotate(5,
|
||||
annotate(2, 1),
|
||||
annotate(4, 3)),
|
||||
back: 5 },
|
||||
annotation5: { forward: annotate(new Record(Symbol.for('R'),
|
||||
[annotate(Symbol.for('f'),
|
||||
Symbol.for('af'))]),
|
||||
Symbol.for('ar')),
|
||||
back: new Record(Symbol.for('R'), [Symbol.for('f')]) },
|
||||
annotation6: { forward: new Record(annotate(Symbol.for('R'),
|
||||
Symbol.for('ar')),
|
||||
[annotate(Symbol.for('f'),
|
||||
Symbol.for('af'))]),
|
||||
back: new Record(Symbol.for('R'), [Symbol.for('f')]) },
|
||||
annotation7: { forward: annotate(List(), Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
|
||||
back: List() },
|
||||
bytes1: { forward: new BytesStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
|
||||
back: Bytes('hello') },
|
||||
list1: { forward: [1, 2, 3, 4],
|
||||
back: List([1, 2, 3, 4]) },
|
||||
list2: {
|
||||
forward: [ new StringStream([Bytes('abc')]),
|
||||
new StringStream([Bytes('def')]), ],
|
||||
back: List(["abc", "def"])
|
||||
},
|
||||
list3: {
|
||||
forward: [List(["a", 1]), List(["b", 2]), List(["c", 3])],
|
||||
back: List([List(["a", 1]), List(["b", 2]), List(["c", 3])])
|
||||
},
|
||||
record2: { value: Observe(new Record(Symbol.for("speak"), [
|
||||
Discard(),
|
||||
Capture(Discard())
|
||||
])) },
|
||||
string0a: { forward: new StringStream([]), back: '' },
|
||||
string1: { forward: new StringStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
|
||||
back: 'hello' },
|
||||
string2: { forward: new StringStream([Bytes('he'), Bytes('llo')]),
|
||||
back: 'hello' },
|
||||
symbol1: { forward: new SymbolStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
|
||||
back: Symbol.for('hello') },
|
||||
};
|
||||
|
||||
function runTestCase(variety, tName, binaryForm, annotatedTextForm) {
|
||||
describe(tName, () => {
|
||||
const textForm = annotatedTextForm.strip();
|
||||
const {forward, back} = (function () {
|
||||
const entry = expectedValues[tName] || {value: textForm};
|
||||
if ('value' in entry) {
|
||||
return {forward: entry.value, back: entry.value};
|
||||
} else if ('forward' in entry && 'back' in entry) {
|
||||
return entry;
|
||||
} else {
|
||||
throw new Error('Invalid expectedValues entry for ' + tName);
|
||||
}
|
||||
})();
|
||||
it('should match the expected value', () => assert(is(textForm, back)));
|
||||
it('should round-trip', () => assert(is(DS(E(textForm)), back)));
|
||||
it('should go forward', () => assert(is(DS(E(forward)), back)));
|
||||
it('should go back', () => assert(is(DS(binaryForm), back)));
|
||||
it('should go back with annotations',
|
||||
() => assert(is(D(E(annotatedTextForm)), annotatedTextForm)));
|
||||
if (variety !== 'decode' && variety !== 'nondeterministic') {
|
||||
it('should encode correctly',
|
||||
() => assert(is(E(forward), binaryForm),
|
||||
E(forward) + ' ' + binaryForm));
|
||||
}
|
||||
if (variety !== 'decode' && variety !== 'nondeterministic' && variety !== 'streaming') {
|
||||
it('should encode correctly with annotations',
|
||||
() => assert(is(E(annotatedTextForm), binaryForm),
|
||||
E(annotatedTextForm) + ' ' + binaryForm));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tests = TestCases._cases(samples.peel()).peel();
|
||||
tests.forEach((t0, tName0) => {
|
||||
const tName = Symbol.keyFor(tName0.strip());
|
||||
const t = t0.peel();
|
||||
switch (t.label) {
|
||||
case Symbol.for('Test'):
|
||||
runTestCase('normal', tName, t.get(0).strip(), t.get(1));
|
||||
break;
|
||||
case Symbol.for('StreamingTest'):
|
||||
runTestCase('streaming', tName, t.get(0).strip(), t.get(1));
|
||||
break;
|
||||
case Symbol.for('NondeterministicTest'):
|
||||
runTestCase('nondeterministic', tName, t.get(0).strip(), t.get(1));
|
||||
break;
|
||||
case Symbol.for('DecodeTest'):
|
||||
runTestCase('decode', tName, t.get(0).strip(), t.get(1));
|
||||
break;
|
||||
case Symbol.for('DecodeError'):
|
||||
describe(tName, () => {
|
||||
it('should fail with DecodeError', () => {
|
||||
try {
|
||||
D(t.get(0).strip());
|
||||
assert.fail("but it didn't");
|
||||
} catch (e) {
|
||||
assert(e instanceof DecodeError);
|
||||
assert(!(e instanceof ShortPacket));
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
case Symbol.for('DecodeEOF'): // fall through
|
||||
case Symbol.for('DecodeShort'):
|
||||
describe(tName, () => {
|
||||
it('should fail with ShortPacket', () => {
|
||||
try {
|
||||
D(t.get(0).strip());
|
||||
assert.fail("but it didn't");
|
||||
} catch (e) {
|
||||
assert(e instanceof ShortPacket);
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
case Symbol.for('ParseError'):
|
||||
case Symbol.for('ParseEOF'):
|
||||
case Symbol.for('ParseShort'):
|
||||
/* Skipped for now, until we have an implementation of text syntax */
|
||||
break;
|
||||
default:{
|
||||
const e = new Error('Unsupported test kind');
|
||||
e.irritant = t;
|
||||
e.testKind = t.label;
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsing from subarray', () => {
|
||||
it('should maintain alignment of nextbytes', () => {
|
||||
const u = Uint8Array.of(57, 57, 57, 57, 83, 51, 51, 51);
|
||||
const bs = Bytes.from(u.subarray(4));
|
||||
assert.strictEqual(new Decoder(bs).next(), "333");
|
||||
});
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
"use strict";
|
||||
// We really, really, REALLY want not to load two separate
|
||||
// implementations of values.js into the same node.js instance, so
|
||||
// there's a bunch of singleton hackery in values.js. These tests
|
||||
// check that separate loads don't cause separate instances.
|
||||
|
||||
const assert = require('assert');
|
||||
const Immutable = require('immutable');
|
||||
|
||||
describe('reloading values.js', () => {
|
||||
const V1 = require('../src/values.js');
|
||||
delete require.cache[require.resolve('../src/values.js')];
|
||||
const V2 = require('../src/values.js');
|
||||
|
||||
const C1 = V1.Record.makeConstructor('c', ['a', 'b']);
|
||||
const C2 = V2.Record.makeConstructor('c', ['a', 'b']);
|
||||
|
||||
it('should reuse RecordConstructorInfo (1)', () => {
|
||||
assert(C1.constructorInfo instanceof V1.RecordConstructorInfo);
|
||||
});
|
||||
it('should reuse RecordConstructorInfo (2)', () => {
|
||||
assert(C1.constructorInfo instanceof V2.RecordConstructorInfo);
|
||||
});
|
||||
it('should identify RecordConstructorInfo', () => {
|
||||
assert(Object.is(V1.RecordConstructorInfo, V2.RecordConstructorInfo));
|
||||
});
|
||||
it('should produce identical module instances', () => { assert.strictEqual(V1, V2); });
|
||||
|
||||
it('should produce distinct constructor instances', () => { assert.notStrictEqual(C1, C2); });
|
||||
it('should produce distinct constructor info', () => {
|
||||
assert(!Object.is(C1.constructorInfo, C2.constructorInfo));
|
||||
});
|
||||
it('should produce compatible constructor info', () => {
|
||||
assert(Immutable.is(C1.constructorInfo, C2.constructorInfo));
|
||||
});
|
||||
it('should produce compatible record instances', () => {
|
||||
assert(Immutable.is(C1(1,2), C2(1,2)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloading index.js', () => {
|
||||
it('produces identical module exports objects', () => {
|
||||
const I1 = require('../src/index.js');
|
||||
delete require.cache[require.resolve('../src/index.js')];
|
||||
const I2 = require('../src/index.js');
|
||||
assert(Object.is(I1, I2));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { Value, is, preserves } from '../src/index';
|
||||
import '../src/node_support';
|
||||
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
is(expected: Value): R;
|
||||
toThrowFilter(f: (e: Error) => boolean): R;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
is(actual, expected) {
|
||||
return is(actual, expected)
|
||||
? { message: () => preserves`expected ${actual} not to be Preserves.is to ${expected}`,
|
||||
pass: true }
|
||||
: { message: () => preserves`expected ${actual} to be Preserves.is to ${expected}`,
|
||||
pass: false };
|
||||
},
|
||||
|
||||
toThrowFilter(thunk, f) {
|
||||
try {
|
||||
thunk();
|
||||
return { message: () => preserves`expected an exception`, pass: false };
|
||||
} catch (e) {
|
||||
if (f(e)) {
|
||||
return { message: () => preserves`expected an exception not matching the filter`,
|
||||
pass: true };
|
||||
} else {
|
||||
return { message: () => preserves`expected an exception matching the filter: ${e.constructor.name}`,
|
||||
pass: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const assert = require('assert');
|
||||
const Immutable = require('immutable');
|
||||
|
||||
const { is, Single, Double, fromJS } = require('../src/index.js');
|
||||
|
||||
describe('Single', () => {
|
||||
it('should print reasonably', () => {
|
||||
assert.strictEqual(Single(123.45).toString(), "123.45");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Double', () => {
|
||||
it('should print reasonably', () => {
|
||||
assert.strictEqual(Double(123.45).toString(), "123.45");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { Single, Double } from '../src/index';
|
||||
import './test-utils';
|
||||
|
||||
describe('Single', () => {
|
||||
it('should print reasonably', () => {
|
||||
expect(new Single(123.45).toString()).toEqual("123.45f");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Double', () => {
|
||||
it('should print reasonably', () => {
|
||||
expect(new Double(123.45).toString()).toEqual("123.45");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"declaration": true,
|
||||
"baseUrl": "./src",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./lib",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
Loading…
Reference in New Issue