preserves/implementations/javascript/src/values.js

285 lines
8.6 KiB
JavaScript
Raw Normal View History

"use strict";
// Preserves Values.
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
const Immutable = require('immutable');
const { List, isList, Map, Set } = 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 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.hashCode = function () {
return this.value | 0; // TODO: something better?
};
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 (typeof maybeByteIterable.size === 'number') {
_installView.call(this, new ArrayBuffer(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;
}
}
Bytes.from = Bytes;
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;
};
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.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.toString = function () {
return decoder.decode(this._view);
};
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);
};
function _installView(view) {
Object.defineProperty(this, '_view', { value: view, writable: false });
Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true });
}
function Record(label, fields) {
if (!(this instanceof Record)) return new Record(label, fields);
Object.defineProperty(this, 'label', { value: fromJS(label), writable: false, enumerable: true });
Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true });
}
Record.prototype.equals = function (other) {
return (other instanceof Record) &&
Immutable.is(this.label, other.label) &&
Immutable.is(this.fields, other.fields);
};
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) {
if (Immutable.is(encoder.shortForms[0], this.label)) {
encoder.header(2, 0, this.fields.size);
} else if (Immutable.is(encoder.shortForms[1], this.label)) {
encoder.header(2, 1, this.fields.size);
} else if (Immutable.is(encoder.shortForms[2], this.label)) {
encoder.header(2, 2, this.fields.size);
} else {
encoder.header(2, 3, this.fields.size + 1);
encoder.push(this.label);
}
for (const field of this.fields) { encoder.push(field); }
};
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.meta = label;
ctor.isClassOf = (v) => ((v instanceof Record) && Immutable.is(label, v.label));
return ctor;
};
Object.assign(module.exports, {
fromJS,
List, Map, Set,
Float, Single, Double,
Bytes,
Record,
});