From 6d2120989bcb67d446bd5bca427d3c6ae9d7ca99 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 2 Mar 2021 22:43:10 +0100 Subject: [PATCH] Fix *almost* all cyclic dependencies in js impl --- implementations/javascript/src/annotated.ts | 42 +------- implementations/javascript/src/codec.ts | 15 ++- implementations/javascript/src/dictionary.ts | 7 +- implementations/javascript/src/fold.ts | 7 +- implementations/javascript/src/fromjs.ts | 48 ++++++++++ implementations/javascript/src/index.ts | 16 +++- implementations/javascript/src/is.ts | 29 ++++++ .../javascript/src/node_support.ts | 6 +- implementations/javascript/src/record.ts | 19 +++- implementations/javascript/src/strip.ts | 40 ++++++++ implementations/javascript/src/values.ts | 96 +------------------ implementations/javascript/test/bytes.test.ts | 2 +- 12 files changed, 177 insertions(+), 150 deletions(-) create mode 100644 implementations/javascript/src/fromjs.ts create mode 100644 implementations/javascript/src/is.ts create mode 100644 implementations/javascript/src/strip.ts diff --git a/implementations/javascript/src/annotated.ts b/implementations/javascript/src/annotated.ts index d854315..356caa3 100644 --- a/implementations/javascript/src/annotated.ts +++ b/implementations/javascript/src/annotated.ts @@ -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 { readonly annotations: Array>; @@ -52,43 +49,10 @@ export class Annotated { } static isAnnotated(x: any): x is Annotated { - return !!x?.[IsPreservesAnnotated]; + return isAnnotated(x); } } -export function unannotate(v: Value): Value { - return Annotated.isAnnotated(v) ? v.item : v; -} - -export function peel(v: Value): Value { - return strip(v, 1); -} - -export function strip(v: Value, depth: number = Infinity): Value { - function step(v: Value, depth: number): Value { - if (depth === 0) return v; - if (!Annotated.isAnnotated(v)) return v; - - const nextDepth = depth - 1; - function walk(v: Value): Value { return step(v, nextDepth); } - - if (Record.isRecord, Tuple>, 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[]).map(walk); - } else if (Set.isSet(v.item)) { - return v.item.map(walk); - } else if (Dictionary.isDictionary, 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(v0: Value, ...anns: Value[]): Annotated { const v = Annotated.isAnnotated(v0) ? v0 : new Annotated(v0); anns.forEach((a) => v.annotations.push(a)); diff --git a/implementations/javascript/src/codec.ts b/implementations/javascript/src/codec.ts index d69e478..8320459 100644 --- a/implementations/javascript/src/codec.ts +++ b/implementations/javascript/src/codec.ts @@ -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'); diff --git a/implementations/javascript/src/dictionary.ts b/implementations/javascript/src/dictionary.ts index 0da35e3..3c1a2c1 100644 --- a/implementations/javascript/src/dictionary.ts +++ b/implementations/javascript/src/dictionary.ts @@ -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'); diff --git a/implementations/javascript/src/fold.ts b/implementations/javascript/src/fold.ts index 012ee74..266c67f 100644 --- a/implementations/javascript/src/fold.ts +++ b/implementations/javascript/src/fold.ts @@ -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> = (v: Value) => R; diff --git a/implementations/javascript/src/fromjs.ts b/implementations/javascript/src/fromjs.ts new file mode 100644 index 0000000..8a038e6 --- /dev/null +++ b/implementations/javascript/src/fromjs.ts @@ -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(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': + case 'bigint': + break; + + case 'object': + if (x === null) { + break; + } + if (typeof x[AsPreserve] === 'function') { + return x[AsPreserve](); + } + if (Record.isRecord, Tuple>, T>(x)) { + return x; + } + if (Array.isArray(x)) { + return x.map>(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); +} diff --git a/implementations/javascript/src/index.ts b/implementations/javascript/src/index.ts index 7fe8752..8307f84 100644 --- a/implementations/javascript/src/index.ts +++ b/implementations/javascript/src/index.ts @@ -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'; diff --git a/implementations/javascript/src/is.ts b/implementations/javascript/src/is.ts new file mode 100644 index 0000000..310ff6b --- /dev/null +++ b/implementations/javascript/src/is.ts @@ -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(x: any): x is Annotated +{ + 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; +} diff --git a/implementations/javascript/src/node_support.ts b/implementations/javascript/src/node_support.ts index 7734048..ebf15e3 100644 --- a/implementations/javascript/src/node_support.ts +++ b/implementations/javascript/src/node_support.ts @@ -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] = diff --git a/implementations/javascript/src/record.ts b/implementations/javascript/src/record.ts index d266724..b51abaa 100644 --- a/implementations/javascript/src/record.ts +++ b/implementations/javascript/src/record.ts @@ -1,4 +1,5 @@ -import { DefaultPointer, is, Value } from "./values"; +import { is } from "./is"; +import { DefaultPointer, Value } from "./values"; export type Tuple = Array | [T]; @@ -69,3 +70,19 @@ export namespace Record { }; } } + +Array.prototype.asPreservesText = function (): string { + if ('label' in (this as any)) { + const r = this as Record, 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(', ') + ']'; + } +}; diff --git a/implementations/javascript/src/strip.ts b/implementations/javascript/src/strip.ts new file mode 100644 index 0000000..bdec501 --- /dev/null +++ b/implementations/javascript/src/strip.ts @@ -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(v: Value): Value { + return Annotated.isAnnotated(v) ? v.item : v; +} + +export function peel(v: Value): Value { + return strip(v, 1); +} + +export function strip( + v: Value, + depth: number = Infinity): Value +{ + function step(v: Value, depth: number): Value { + if (depth === 0) return v; + if (!Annotated.isAnnotated(v)) return v; + + const nextDepth = depth - 1; + function walk(v: Value): Value { return step(v, nextDepth); } + + if (Record.isRecord, Tuple>, 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[]).map(walk); + } else if (Set.isSet(v.item)) { + return v.item.map(walk); + } else if (Dictionary.isDictionary, 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); +} diff --git a/implementations/javascript/src/values.ts b/implementations/javascript/src/values.ts index b7a0640..6efabbc 100644 --- a/implementations/javascript/src/values.ts +++ b/implementations/javascript/src/values.ts @@ -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 = | Set | Dictionary, T>; -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': - case 'bigint': - break; - - case 'object': - if (x === null) { - break; - } - if (typeof x[AsPreserve] === 'function') { - return x[AsPreserve](); - } - if (Record.isRecord, Tuple>, T>(x)) { - return x; - } - if (Array.isArray(x)) { - return x.map>(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, 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(', ') + ']'; - } -}; diff --git a/implementations/javascript/test/bytes.test.ts b/implementations/javascript/test/bytes.test.ts index 19b960b..bf19173 100644 --- a/implementations/javascript/test/bytes.test.ts +++ b/implementations/javascript/test/bytes.test.ts @@ -1,4 +1,4 @@ -import { Bytes, fromJS } from '../src/values'; +import { Bytes, fromJS } from '../src/index'; import './test-utils'; describe('immutable byte arrays', () => {