preserves/implementations/javascript/src/values.ts

115 lines
3.5 KiB
TypeScript

// Preserves Values.
import { AsPreserve } from './symbols';
import { Bytes } from './bytes';
import { DoubleFloat, SingleFloat } from './float';
import { Record } from './record';
import { Annotated } from './annotated';
import { Set, Dictionary } from './dictionary';
import { Tuple } from './tuple';
export * from './bytes';
export * from './float';
export * from './record';
export * from './annotated';
export * from './dictionary';
export * from './tuple';
export type DefaultPointer = object
export type Value<T extends object = DefaultPointer> = Atom | Compound<T> | T | Annotated<T>;
export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol;
export type Compound<T extends object = DefaultPointer> = Record<any, Tuple<any>, T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
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':
case 'bigint':
break;
case 'object':
if (x === null) {
break;
}
if (typeof x[AsPreserve] === 'function') {
return x[AsPreserve]();
}
if (Record.isRecord<T>(x)) {
return x;
}
if (Array.isArray(x)) {
return (x as Array<Value<T>>).map<Value<T>>(fromJS);
}
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
return Bytes.from(x);
}
// Just... assume it's a T.
return (x as T);
default:
break;
}
throw new TypeError("Cannot represent JavaScript value as Preserves: " + x);
}
export function is(a: any, b: any): boolean {
if (Annotated.isAnnotated(a)) a = a.item;
if (Annotated.isAnnotated(b)) b = b.item;
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 && typeof a.equals === 'function') return a.equals(b, is);
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;
}
declare global {
interface Object { asPreservesText(): string; }
}
Object.defineProperty(Object.prototype, 'asPreservesText', {
enumerable: false,
writable: true,
value: function(): string { return '#!' + JSON.stringify(this); }
});
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<any>) => i.asPreservesText()).join(', ') + ']';
};