Fix *almost* all cyclic dependencies in js impl

This commit is contained in:
Tony Garnock-Jones 2021-03-02 22:43:10 +01:00
parent c8c027f762
commit 6d2120989b
12 changed files with 177 additions and 150 deletions

View File

@ -1,11 +1,8 @@
import { Encoder } from "./codec";
import { Tag } from "./constants";
import { AsPreserve, PreserveOn } from "./symbols";
import { DefaultPointer, is, Value } from "./values";
import { Record, Tuple } from './record';
import { Dictionary, Set } from './dictionary';
export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated');
import { DefaultPointer, Value } from "./values";
import { is, isAnnotated, IsPreservesAnnotated } from './is';
export class Annotated<T extends object = DefaultPointer> {
readonly annotations: Array<Value<T>>;
@ -52,43 +49,10 @@ export class Annotated<T extends object = DefaultPointer> {
}
static isAnnotated<T extends object = DefaultPointer>(x: any): x is Annotated<T> {
return !!x?.[IsPreservesAnnotated];
return isAnnotated(x);
}
}
export function unannotate<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
return Annotated.isAnnotated<T>(v) ? v.item : v;
}
export function peel<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
return strip(v, 1);
}
export function strip<T extends object = DefaultPointer>(v: Value<T>, depth: number = Infinity): Value<T> {
function step(v: Value<T>, depth: number): Value<T> {
if (depth === 0) return v;
if (!Annotated.isAnnotated<T>(v)) return v;
const nextDepth = depth - 1;
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v.item)) {
return Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return (v.item as Value<T>[]).map(walk);
} else if (Set.isSet<T>(v.item)) {
return v.item.map(walk);
} else if (Dictionary.isDictionary<Value<T>, T>(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (Annotated.isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else {
return v.item;
}
}
return step(v, depth);
}
export function annotate<T extends object = DefaultPointer>(v0: Value<T>, ...anns: Value<T>[]): Annotated<T> {
const v = Annotated.isAnnotated<T>(v0) ? v0 : new Annotated(v0);
anns.forEach((a) => v.annotations.push(a));

View File

@ -1,16 +1,13 @@
// Preserves Binary codec.
import {
underlying,
Annotated,
Dictionary, Set, Bytes, Record, SingleFloat, DoubleFloat,
BytesLike,
Value,
Tuple,
} from './values';
import { Value } from './values';
import { Tag } from './constants';
import { PreserveOn } from './symbols';
import { Bytes, BytesLike, underlying } from './bytes';
import { Annotated } from './annotated';
import { Set, Dictionary } from './dictionary';
import { DoubleFloat, SingleFloat } from './float';
import { Record, Tuple } from './record';
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
export const ErrorType = Symbol.for('ErrorType');

View File

@ -1,10 +1,13 @@
import { canonicalEncode, canonicalString, Encoder } from "./codec";
import type { Encoder } from "./codec";
import { canonicalEncode, canonicalString } from "./codec";
import { Tag } from "./constants";
import { FlexMap, FlexSet, _iterMap } from "./flex";
import { PreserveOn } from "./symbols";
import { stringify } from "./text";
import { DefaultPointer, fromJS, Value } from "./values";
import { DefaultPointer, Value } from "./values";
import { Bytes } from './bytes';
import { fromJS } from "./fromjs";
export type DictionaryType = 'Dictionary' | 'Set';
export const DictionaryType = Symbol.for('DictionaryType');

View File

@ -1,4 +1,9 @@
import { Bytes, Value, Record, Set, Dictionary, Single, Double, Annotated, annotate, Float, Tuple } from "./values";
import { Record, Tuple } from "./record";
import { Bytes } from "./bytes";
import { Value } from "./values";
import { Set, Dictionary } from "./dictionary";
import { annotate, Annotated } from "./annotated";
import { Double, Float, Single } from "./float";
export type Fold<T extends object, R = Value<T>> = (v: Value<T>) => R;

View File

@ -0,0 +1,48 @@
import { Bytes } from "./bytes";
import { Record, Tuple } from "./record";
import { AsPreserve } from "./symbols";
import { DefaultPointer, Value } from "./values";
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<Value<T>, Tuple<Value<T>>, T>(x)) {
return x;
}
if (Array.isArray(x)) {
return x.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);
}

View File

@ -1,7 +1,15 @@
export * from './flex';
export * from './symbols';
export * from './annotated';
export * from './bytes';
export * from './codec';
export * from './values';
export * from './text';
export * from './dictionary';
export * from './flex';
export * from './float';
export * from './fold';
export * from './fromjs';
export * from './is';
export * from './record';
export * from './strip';
export * from './symbols';
export * from './text';
export * from './values';
export * as Constants from './constants';

View File

@ -0,0 +1,29 @@
import type { DefaultPointer } from "./values.js";
import type { Annotated } from "./annotated.js";
export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated');
export function isAnnotated<T extends object = DefaultPointer>(x: any): x is Annotated<T>
{
return !!x?.[IsPreservesAnnotated];
}
export function is(a: any, b: any): boolean {
if (isAnnotated(a)) a = a.item;
if (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)) {
const isRecord = 'label' in a;
if (isRecord !== 'label' in b) return false;
if (isRecord && !is((a as any).label, (b as any).label)) return false;
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;
}

View File

@ -1,7 +1,11 @@
// Patching to support node.js extensions.
import { Annotated } from './annotated';
import { Bytes } from './bytes';
import { Set, Dictionary } from './dictionary';
import { Record } from './record';
import * as util from 'util';
import { Record, Bytes, Annotated, Set, Dictionary } from './values';
[Bytes, Annotated, Set, Dictionary].forEach((C) => {
(C as any).prototype[util.inspect.custom] =

View File

@ -1,4 +1,5 @@
import { DefaultPointer, is, Value } from "./values";
import { is } from "./is";
import { DefaultPointer, Value } from "./values";
export type Tuple<T> = Array<T> | [T];
@ -69,3 +70,19 @@ export namespace Record {
};
}
}
Array.prototype.asPreservesText = function (): string {
if ('label' in (this as any)) {
const r = this as Record<Value, Tuple<Value>, DefaultPointer>;
return r.label.asPreservesText() +
'(' + r.map(f => {
try {
return f.asPreservesText();
} catch (e) {
return Record.fallbackToString(f);
}
}).join(', ') + ')';
} else {
return '[' + this.map(i => i.asPreservesText()).join(', ') + ']';
}
};

View File

@ -0,0 +1,40 @@
import { DefaultPointer, Value } from "./values";
import { Annotated } from "./annotated";
import { Record, Tuple } from "./record";
import { Set, Dictionary } from "./dictionary";
export function unannotate<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
return Annotated.isAnnotated<T>(v) ? v.item : v;
}
export function peel<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
return strip(v, 1);
}
export function strip<T extends object = DefaultPointer>(
v: Value<T>,
depth: number = Infinity): Value<T>
{
function step(v: Value<T>, depth: number): Value<T> {
if (depth === 0) return v;
if (!Annotated.isAnnotated<T>(v)) return v;
const nextDepth = depth - 1;
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v.item)) {
return Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return (v.item as Value<T>[]).map(walk);
} else if (Set.isSet<T>(v.item)) {
return v.item.map(walk);
} else if (Dictionary.isDictionary<Value<T>, T>(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (Annotated.isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else {
return v.item;
}
}
return step(v, depth);
}

View File

@ -1,17 +1,9 @@
// Preserves Values.
import { AsPreserve } from './symbols';
import { Bytes } from './bytes';
import { DoubleFloat, SingleFloat } from './float';
import { Record, Tuple } from './record';
import { Annotated } from './annotated';
import { Set, Dictionary } from './dictionary';
export * from './bytes';
export * from './float';
export * from './record';
export * from './annotated';
export * from './dictionary';
import type { Bytes } from './bytes';
import type { DoubleFloat, SingleFloat } from './float';
import type { Annotated } from './annotated';
import type { Set, Dictionary } from './dictionary';
export type DefaultPointer = object;
@ -39,70 +31,6 @@ export type Compound<T extends object = DefaultPointer> =
| 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<Value<T>, Tuple<Value<T>>, T>(x)) {
return x;
}
if (Array.isArray(x)) {
return x.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)) {
const isRecord = 'label' in a;
if (isRecord !== 'label' in b) return false;
if (isRecord && !is((a as any).label, (b as any).label)) return false;
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; }
}
@ -129,19 +57,3 @@ Symbol.prototype.asPreservesText = function (): string {
// TODO: escaping
return this.description ?? '||';
};
Array.prototype.asPreservesText = function (): string {
if ('label' in (this as any)) {
const r = this as Record<Value, Tuple<Value>, DefaultPointer>;
return r.label.asPreservesText() +
'(' + r.map(f => {
try {
return f.asPreservesText();
} catch (e) {
return Record.fallbackToString(f);
}
}).join(', ') + ')';
} else {
return '[' + this.map(i => i.asPreservesText()).join(', ') + ']';
}
};

View File

@ -1,4 +1,4 @@
import { Bytes, fromJS } from '../src/values';
import { Bytes, fromJS } from '../src/index';
import './test-utils';
describe('immutable byte arrays', () => {