diff --git a/implementations/javascript/packages/core/src/decoder.ts b/implementations/javascript/packages/core/src/decoder.ts index ec87d1b..51b61f8 100644 --- a/implementations/javascript/packages/core/src/decoder.ts +++ b/implementations/javascript/packages/core/src/decoder.ts @@ -7,13 +7,14 @@ import { Record } from "./record"; import { Bytes, BytesLike, underlying } from "./bytes"; import { Value } from "./values"; import { is } from "./is"; +import { embed, neverPointerType, Pointer, PointerType } from "./pointer"; export interface DecoderOptions { includeAnnotations?: boolean; } export interface DecoderPointerOptions extends DecoderOptions { - decodePointer?(d: TypedDecoder): T; + pointerType?: PointerType; } export interface TypedDecoder { @@ -24,13 +25,14 @@ export interface TypedDecoder { skip(): void; next(): Value; - withPointerDecoder(decodePointer: (d: TypedDecoder) => S, - body: (d: TypedDecoder) => R): R; + withPointerType( + pointerType: PointerType, + body: (d: TypedDecoder) => R): R; nextBoolean(): boolean | undefined; nextFloat(): SingleFloat | undefined; nextDouble(): DoubleFloat | undefined; - nextPointer(): T | undefined; + nextPointer(): Pointer | undefined; nextSignedInteger(): number | undefined; nextString(): string | undefined; nextByteString(): Bytes | undefined; @@ -51,10 +53,6 @@ export function asLiteral, Annotated>>( return is(actual, expected) ? expected : void 0; } -function _defaultDecodePointer(): T { - throw new DecodeError("No decodePointer function supplied"); -} - export class DecoderState { packet: Uint8Array; index = 0; @@ -163,25 +161,23 @@ export class DecoderState { } } -type DecoderFn = (d: DecoderState) => T; - export class Decoder implements TypedDecoder { state: DecoderState; - decodePointer: DecoderFn = _defaultDecodePointer; + pointerType: PointerType; - constructor(state: DecoderState, decodePointer: DecoderFn); - constructor(packet?: BytesLike, options?: DecoderOptions); + constructor(state: DecoderState, pointerType?: PointerType); + constructor(packet?: BytesLike, options?: DecoderPointerOptions); constructor( packet_or_state: (DecoderState | BytesLike) = new Uint8Array(0), - options_or_decoder?: (DecoderOptions | DecoderFn)) + options_or_pointerType?: (DecoderPointerOptions | PointerType)) { if (packet_or_state instanceof DecoderState) { this.state = packet_or_state; - this.decodePointer = options_or_decoder as DecoderFn; + this.pointerType = (options_or_pointerType as PointerType) ?? neverPointerType; } else { - this.state = new DecoderState( - packet_or_state, - (options_or_decoder as DecoderOptions) ?? {}); + const options = (options_or_pointerType as DecoderPointerOptions) ?? {}; + this.state = new DecoderState(packet_or_state, options); + this.pointerType = options.pointerType ?? neverPointerType; } } @@ -217,7 +213,7 @@ export class Decoder implements TypedDecoder { const v = this.next() as Annotated; return this.state.unshiftAnnotation(a, v); } - case Tag.Pointer: return this.state.wrap(this.decodePointer(this.state)); + case Tag.Pointer: return this.state.wrap(embed(this.pointerType.decode(this.state))); case Tag.SignedInteger: return this.state.wrap(this.state.nextint(this.state.varint())); case Tag.String: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8()); case Tag.ByteString: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint()))); @@ -261,14 +257,11 @@ export class Decoder implements TypedDecoder { this.next(); } - pushPointerDecoder(decodePointer: (d: TypedDecoder) => S): Decoder { - return new Decoder(this.state, (_s: DecoderState) => decodePointer(this)); - } - - withPointerDecoder(decodePointer: (d: TypedDecoder) => S, - body: (d: TypedDecoder) => R): R + withPointerType( + pointerType: PointerType, + body: (d: TypedDecoder) => R): R { - return body(this.pushPointerDecoder(decodePointer)); + return body(new Decoder(this.state, pointerType)); } skipAnnotations(): void { @@ -303,10 +296,10 @@ export class Decoder implements TypedDecoder { } } - nextPointer(): T | undefined { + nextPointer(): Pointer | undefined { this.skipAnnotations(); switch (this.state.nextbyte()) { - case Tag.Pointer: return this.decodePointer(this.state); + case Tag.Pointer: return embed(this.pointerType.decode(this.state)); default: return void 0; } } @@ -372,9 +365,7 @@ export class Decoder implements TypedDecoder { } export function decode(bs: BytesLike, options: DecoderPointerOptions = {}): Value { - return new Decoder(bs, options).withPointerDecoder>( - options.decodePointer ?? _defaultDecodePointer, - d => d.next()); + return new Decoder(bs, options).next(); } export function decodeWithAnnotations(bs: BytesLike, diff --git a/implementations/javascript/packages/core/src/encoder.ts b/implementations/javascript/packages/core/src/encoder.ts index b8677f9..54b07ff 100644 --- a/implementations/javascript/packages/core/src/encoder.ts +++ b/implementations/javascript/packages/core/src/encoder.ts @@ -4,6 +4,7 @@ import { Value } from "./values"; import { PreserveOn } from "./symbols"; import { EncodeError } from "./codec"; import { Record, Tuple } from "./record"; +import { identityPointerType, PointerType } from "./pointer"; export type Encodable = Value | Preservable | Iterable> | ArrayBufferView; @@ -22,7 +23,7 @@ export interface EncoderOptions { } export interface EncoderPointerOptions extends EncoderOptions { - encodePointer?: EncodePointerFunction; + pointerType?: PointerType; } export function asLatin1(bs: Uint8Array): string { @@ -33,10 +34,6 @@ function isIterable(v: any): v is Iterable { return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; } -function _defaultEncodePointer(s: EncoderState, v: T): void { - new Encoder(s).push(pointerId(v)); -} - export class EncoderState { chunks: Array; view: DataView; @@ -139,31 +136,30 @@ export class EncoderState { } } -export type EncodePointerFunction = (s: EncoderState, v: T) => void; - -export class Encoder { +export class Encoder { state: EncoderState; - encodePointer: EncodePointerFunction; + pointerType: PointerType | undefined; - constructor(options: EncoderOptions); - constructor(state: EncoderState, encodePointer?: EncodePointerFunction); + constructor(options: EncoderPointerOptions); + constructor(state: EncoderState, pointerType?: PointerType); constructor( - state_or_options: (EncoderState | EncoderOptions) = {}, - encodePointer?: EncodePointerFunction) + state_or_options: (EncoderState | EncoderPointerOptions) = {}, + pointerType?: PointerType) { if (state_or_options instanceof EncoderState) { this.state = state_or_options; - this.encodePointer = encodePointer ?? _defaultEncodePointer; + this.pointerType = pointerType; } else { this.state = new EncoderState(state_or_options); - this.encodePointer = _defaultEncodePointer; + this.pointerType = state_or_options.pointerType; } } - withPointerEncoder(encodePointer: EncodePointerFunction, - body: (e: Encoder) => void): this + withPointerType( + pointerType: PointerType, + body: (e: Encoder) => void): this { - body(new Encoder(this.state, encodePointer)); + body(new Encoder(this.state, pointerType)); return this; } @@ -236,33 +232,25 @@ export class Encoder { } else { this.state.emitbyte(Tag.Pointer); - this.encodePointer(this.state, v); + (this.pointerType ?? identityPointerType).encode(this.state, v.embeddedValue); } return this; // for chaining } } -export function encode(v: Encodable, options: EncoderPointerOptions = {}): Bytes { - return new Encoder(options).withPointerEncoder( - options.encodePointer ?? _defaultEncodePointer, - e => e.push(v)).contents(); -} - -let _nextId = 0; -const _registry = new WeakMap(); -export function pointerId(v: any): number { - let id = _registry.get(v); - if (id === void 0) { - id = _nextId++; - _registry.set(v, id); - } - return id; +export function encode( + v: Encodable, + options: EncoderPointerOptions = {}): Bytes +{ + return new Encoder(options).push(v).contents(); } const _canonicalEncoder = new Encoder({ canonical: true }); let _usingCanonicalEncoder = false; -export function canonicalEncode(v: Encodable, options?: EncoderPointerOptions): Bytes -{ + +export function canonicalEncode(v: Encodable, options?: EncoderPointerOptions): Bytes; +export function canonicalEncode(v: Encodable, options?: EncoderPointerOptions): Bytes; +export function canonicalEncode(v: any, options?: EncoderPointerOptions): Bytes { if (options === void 0 && !_usingCanonicalEncoder) { _usingCanonicalEncoder = true; const bs = _canonicalEncoder.push(v).contents(); diff --git a/implementations/javascript/packages/core/src/fold.ts b/implementations/javascript/packages/core/src/fold.ts index 147086b..ba40d50 100644 --- a/implementations/javascript/packages/core/src/fold.ts +++ b/implementations/javascript/packages/core/src/fold.ts @@ -4,6 +4,7 @@ import { Value } from "./values"; import { Set, Dictionary } from "./dictionary"; import { annotate, Annotated } from "./annotated"; import { Double, Float, Single } from "./float"; +import { Pointer } from "./pointer"; export type Fold> = (v: Value) => R; @@ -23,7 +24,7 @@ export interface FoldMethods { annotated(a: Annotated, k: Fold): R; - pointer(t: T, k: Fold): R; + pointer(t: Pointer, k: Fold): R; } export abstract class ValueFold implements FoldMethods> { @@ -63,16 +64,16 @@ export abstract class ValueFold implements FoldMethods> { annotated(a: Annotated, k: Fold>): Value { return annotate(k(a.item), ...a.annotations.map(k)); } - abstract pointer(t: T, k: Fold>): Value; + abstract pointer(t: Pointer, k: Fold>): Value; } export class IdentityFold extends ValueFold { - pointer(t: T, _k: Fold>): Value { + pointer(t: Pointer, _k: Fold>): Value { return t; } } -export class MapFold extends ValueFold { +export class MapFold extends ValueFold { readonly f: (t: T) => Value; constructor(f: (t: T) => Value) { @@ -80,12 +81,12 @@ export class MapFold extends ValueFold { this.f = f; } - pointer(t: T, _k: Fold>): Value { - return this.f(t); + pointer(t: Pointer, _k: Fold>): Value { + return this.f(t.embeddedValue); } } -export const IDENTITY_FOLD = new IdentityFold(); +export const IDENTITY_FOLD = new IdentityFold(); export function fold(v: Value, o: FoldMethods): R { const walk = (v: Value): R => { @@ -121,44 +122,19 @@ export function fold(v: Value, o: FoldMethods): R { } else if (Float.isDouble(v)) { return o.double(v.value); } else { - /* fall through */ + return o.pointer(v, walk); } default: - return o.pointer(v, walk); + ((_v: never): never => { throw new Error("Internal error"); })(v); } }; return walk(v); } -export function mapPointers( +export function mapPointers( v: Value, f: (t: T) => Value, ): Value { return fold(v, new MapFold(f)); } - -export function isPointer(v: Value): v is T { - return fold(v, { - boolean(_b: boolean): boolean { return false; }, - single(_f: number): boolean { return false; }, - double(_f: number): boolean { return false; }, - integer(_i: number): boolean { return false; }, - string(_s: string): boolean { return false; }, - bytes(_b: Bytes): boolean { return false; }, - symbol(_s: symbol): boolean { return false; }, - - record(_r: Record, Tuple>, T>, _k: Fold): boolean { - return false; - }, - array(_a: Array>, _k: Fold): boolean { return false; }, - set(_s: Set, _k: Fold): boolean { return false; }, - dictionary(_d: Dictionary, _k: Fold): boolean { - return false; - }, - - annotated(_a: Annotated, _k: Fold): boolean { return false; }, - - pointer(_t: T, _k: Fold): boolean { return true; }, - }); -} diff --git a/implementations/javascript/packages/core/src/fromjs.ts b/implementations/javascript/packages/core/src/fromjs.ts index b8d4026..ee939b0 100644 --- a/implementations/javascript/packages/core/src/fromjs.ts +++ b/implementations/javascript/packages/core/src/fromjs.ts @@ -1,8 +1,9 @@ -import { DefaultPointer } from "./pointer"; +import { embed, DefaultPointer } from "./pointer"; import { Bytes } from "./bytes"; import { Record, Tuple } from "./record"; import { AsPreserve } from "./symbols"; import { Value } from "./values"; +import { Dictionary, Set } from "./dictionary"; export function fromJS(x: any): Value { switch (typeof x) { @@ -38,8 +39,18 @@ export function fromJS(x: any): Value { if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) { return Bytes.from(x); } + if (Map.isMap(x)) { + const d = new Dictionary(); + x.forEach((v, k) => d.set(fromJS(k), fromJS(v))); + return d; + } + if (Set.isSet(x)) { + const s = new Set(); + x.forEach(v => s.add(fromJS(v))); + return s; + } // Just... assume it's a T. - return (x as T); + return embed(x as T); default: break; diff --git a/implementations/javascript/packages/core/src/is.ts b/implementations/javascript/packages/core/src/is.ts index 36a0ff0..3bbea91 100644 --- a/implementations/javascript/packages/core/src/is.ts +++ b/implementations/javascript/packages/core/src/is.ts @@ -1,5 +1,5 @@ -import type { DefaultPointer } from "./pointer.js"; -import type { Annotated } from "./annotated.js"; +import type { DefaultPointer } from "./pointer"; +import type { Annotated } from "./annotated"; export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated'); diff --git a/implementations/javascript/packages/core/src/merge.ts b/implementations/javascript/packages/core/src/merge.ts index 39761e6..aa858f7 100644 --- a/implementations/javascript/packages/core/src/merge.ts +++ b/implementations/javascript/packages/core/src/merge.ts @@ -1,11 +1,12 @@ import { Record, Tuple } from "./record"; import { Bytes } from "./bytes"; -import { fold, isPointer } from "./fold"; +import { fold } from "./fold"; import { is } from "./is"; import { Value } from "./values"; import { Set, Dictionary } from "./dictionary"; import { Annotated } from "./annotated"; import { unannotate } from "./strip"; +import { embed, isPointer, Pointer } from "./pointer"; export function merge( mergePointers: (a: T, b: T) => T | undefined, @@ -53,11 +54,11 @@ export function merge( return walk(a, unannotate(b)); }, - pointer(t: T) { - if (!isPointer(b)) die(); - const r = mergePointers(t, b); + pointer(t: Pointer) { + if (!isPointer(b)) die(); + const r = mergePointers(t.embeddedValue, b.embeddedValue); if (r === void 0) die(); - return r; + return embed(r); }, }); } diff --git a/implementations/javascript/packages/core/src/pointer.ts b/implementations/javascript/packages/core/src/pointer.ts index 266a43c..1d8e3d0 100644 --- a/implementations/javascript/packages/core/src/pointer.ts +++ b/implementations/javascript/packages/core/src/pointer.ts @@ -1,33 +1,117 @@ import { Encoder, EncoderState } from "./encoder"; -import type { TypedDecoder } from "./decoder"; +import { Decoder, DecoderState } from "./decoder"; import type { Value } from "./values"; - import { strip } from "./strip"; -export class DefaultPointer { - v: Value; +export type PointerType = { + decode(s: DecoderState): T; + encode(s: EncoderState, v: T): void; - constructor(v: Value) { - this.v = v; + fromValue(v: Value): T; + toValue(v: T): Value; +} + +export class Pointer { + embeddedValue: T; + + constructor(embeddedValue: T) { + this.embeddedValue = embeddedValue; } equals(other: any, is: (a: any, b: any) => boolean) { - return Object.is(other.constructor, this.constructor) && is(this.v, other.v); + return isPointer(other) && is(this.embeddedValue, other.embeddedValue); } asPreservesText(): string { - return '#!' + this.v.asPreservesText(); + return '#!' + (this.embeddedValue as any).asPreservesText(); } } -export function readDefaultPointer(v: Value): DefaultPointer { - return new DefaultPointer(strip(v)); +export function embed(embeddedValue: T): Pointer { + return new Pointer(embeddedValue); } -export function decodeDefaultPointer(d: TypedDecoder): DefaultPointer { - return readDefaultPointer(d.withPointerDecoder(decodeDefaultPointer, d => d.next())); +export function isPointer(v: Value): v is Pointer { + return typeof v === 'object' && 'embeddedValue' in v; } -export function encodeDefaultPointer(e: EncoderState, w: DefaultPointer): void { - new Encoder(e, encodeDefaultPointer).push(w.v); +export class DefaultPointer { + generic: Value; + + constructor(generic: Value) { + this.generic = generic; + } + + equals(other: any, is: (a: any, b: any) => boolean) { + return typeof other === 'object' && 'generic' in other && is(this.generic, other.generic); + } + + asPreservesText(): string { + return this.generic.asPreservesText(); + } } + +export const genericPointerType: PointerType = { + decode(s: DecoderState): DefaultPointer { + return new DefaultPointer(new Decoder(s, this).next()); + }, + + encode(s: EncoderState, v: DefaultPointer): void { + new Encoder(s, this).push(v.generic); + }, + + fromValue(v: Value): DefaultPointer { + return new DefaultPointer(strip(v)); + }, + + toValue(v: DefaultPointer): Value { + return v.generic; + } +}; + +export const neverPointerType: PointerType = { + decode(_s: DecoderState): never { + throw new Error("Pointers not permitted at this point in Preserves document"); + }, + + encode(_s: EncoderState, _v: never): void { + throw new Error("Pointers not permitted encoding Preserves document"); + }, + + fromValue(_v: Value): never { + throw new Error("Pointers not permitted at this point in Preserves document"); + }, + + toValue(_v: never): Value { + throw new Error("Pointers not permitted encoding Preserves document"); + } +}; + +let _nextId = 0; +const _registry = new WeakMap(); +export function pointerId(v: any): number { + let id = _registry.get(v); + if (id === void 0) { + id = _nextId++; + _registry.set(v, id); + } + return id; +} + +export const identityPointerType: PointerType = { + decode(_s: DecoderState): any { + throw new Error("Cannot decode identityPointerType"); + }, + + encode(s: EncoderState, v: any): void { + new Encoder(s, this).push(pointerId(v)); + }, + + fromValue(_v: Value): any { + throw new Error("Cannot decode identityPointerType"); + }, + + toValue(v: any): Value { + return pointerId(v); + } +}; diff --git a/implementations/javascript/packages/core/src/reader.ts b/implementations/javascript/packages/core/src/reader.ts index 55fe5b8..8c7f8de 100644 --- a/implementations/javascript/packages/core/src/reader.ts +++ b/implementations/javascript/packages/core/src/reader.ts @@ -10,17 +10,15 @@ import { Record } from './record'; import { Annotated, newPosition, Position, updatePosition } from './annotated'; import { Double, DoubleFloat, Single, SingleFloat } from './float'; import { stringify } from './text'; -import { decodeDefaultPointer, DefaultPointer, readDefaultPointer } from './pointer'; +import { embed, DefaultPointer, genericPointerType, neverPointerType, PointerType } from './pointer'; export interface ReaderStateOptions { includeAnnotations?: boolean; name?: string | Position; } -export type DecodePointerFunction = (v: Value) => T; - export interface ReaderOptions extends ReaderStateOptions { - decodePointer?: DecodePointerFunction; + pointerType?: PointerType; } type IntOrFloat = 'int' | 'float'; @@ -279,21 +277,21 @@ export class ReaderState { export class Reader { state: ReaderState; - decodePointer?: DecodePointerFunction; + pointerType: PointerType; - constructor(state: ReaderState, decodePointer?: DecodePointerFunction); + constructor(state: ReaderState, pointerType: PointerType); constructor(buffer: string, options?: ReaderOptions); constructor( state_or_buffer: (ReaderState | string) = '', - decodePointer_or_options?: (DecodePointerFunction | ReaderOptions)) + pointerType_or_options?: (PointerType | ReaderOptions)) { if (state_or_buffer instanceof ReaderState) { this.state = state_or_buffer; - this.decodePointer = decodePointer_or_options as DecodePointerFunction; + this.pointerType = pointerType_or_options as PointerType; } else { - const options = (decodePointer_or_options as ReaderOptions) ?? {}; + const options = (pointerType_or_options as ReaderOptions) ?? {}; this.state = new ReaderState(state_or_buffer, options); - this.decodePointer = options.decodePointer; + this.pointerType = options.pointerType ?? neverPointerType; } } @@ -380,16 +378,12 @@ export class Reader { if (!Bytes.isBytes(bs)) this.state.error('ByteString must follow #=', startPos); return decode(bs, { - decodePointer: decodeDefaultPointer, + pointerType: this.pointerType, includeAnnotations: this.state.options.includeAnnotations, }); } - case '!': { - if (this.decodePointer === void 0) { - this.state.error("No decodePointer function supplied", startPos); - } - return this.decodePointer(new Reader(this.state, readDefaultPointer).next()); - } + case '!': return embed(this.pointerType.fromValue( + new Reader(this.state, genericPointerType).next())); default: this.state.error(`Invalid # syntax: ${c}`, startPos); } diff --git a/implementations/javascript/packages/core/src/text.ts b/implementations/javascript/packages/core/src/text.ts index d232d44..ae26596 100644 --- a/implementations/javascript/packages/core/src/text.ts +++ b/implementations/javascript/packages/core/src/text.ts @@ -12,7 +12,7 @@ export function stringify(x: any): string { } } -export function preserves(pieces: TemplateStringsArray, ...values: Value[]): string { +export function preserves(pieces: TemplateStringsArray, ...values: Value[]): string { const result = [pieces[0]]; values.forEach((v, i) => { result.push(stringify(v)); @@ -29,7 +29,9 @@ declare global { Object.defineProperty(Object.prototype, 'asPreservesText', { enumerable: false, writable: true, - value: function(): string { return '#!' + JSON.stringify(this); } + value: function(): string { + return JSON.stringify(this); + } }); Boolean.prototype.asPreservesText = function (): string { diff --git a/implementations/javascript/packages/core/src/values.ts b/implementations/javascript/packages/core/src/values.ts index f690a58..aca9246 100644 --- a/implementations/javascript/packages/core/src/values.ts +++ b/implementations/javascript/packages/core/src/values.ts @@ -4,12 +4,12 @@ import type { Bytes } from './bytes'; import type { DoubleFloat, SingleFloat } from './float'; import type { Annotated } from './annotated'; import type { Set, Dictionary } from './dictionary'; -import type { DefaultPointer } from './pointer'; +import type { Pointer, DefaultPointer } from './pointer'; export type Value = | Atom | Compound - | T + | Pointer | Annotated; export type Atom = | boolean diff --git a/implementations/javascript/packages/core/test/codec.test.ts b/implementations/javascript/packages/core/test/codec.test.ts index c4be85a..28a4a90 100644 --- a/implementations/javascript/packages/core/test/codec.test.ts +++ b/implementations/javascript/packages/core/test/codec.test.ts @@ -9,12 +9,15 @@ import { preserves, fromJS, Constants, - TypedDecoder, Encoder, DefaultPointer, - decodeDefaultPointer, - encodeDefaultPointer, EncoderState, + PointerType, + DecoderState, + Decoder, + Pointer, + embed, + genericPointerType, } from '../src/index'; const { Tag } = Constants; import './test-utils'; @@ -84,9 +87,43 @@ describe('reusing buffer space', () => { }); describe('encoding and decoding pointers', () => { + class LookasidePointerType implements PointerType { + readonly objects: object[]; + + constructor(objects: object[]) { + this.objects = objects; + } + + decode(d: DecoderState): object { + return this.fromValue(new Decoder(d).next()); + } + + encode(e: EncoderState, v: object): void { + new Encoder(e).push(this.toValue(v)); + } + + equals(a: object, b: object): boolean { + return Object.is(a, b); + } + + fromValue(v: Value): object { + if (typeof v !== 'number' || v < 0 || v >= this.objects.length) { + throw new Error("Unknown pointer target"); + } + return this.objects[v]; + } + + toValue(v: object): number { + let i = this.objects.indexOf(v); + if (i !== -1) return i; + this.objects.push(v); + return this.objects.length - 1; + } + } + it('should encode using pointerId when no function has been supplied', () => { - const A1 = ({a: 1}); - const A2 = ({a: 1}); + const A1 = embed({a: 1}); + const A2 = embed({a: 1}); const bs1 = canonicalEncode(A1); const bs2 = canonicalEncode(A2); const bs3 = canonicalEncode(A1); @@ -100,62 +137,53 @@ describe('encoding and decoding pointers', () => { }); it('should refuse to decode pointers when no function has been supplied', () => { expect(() => decode(Bytes.from([Tag.Pointer, Tag.SmallInteger_lo]))) - .toThrow('No decodePointer function supplied'); + .toThrow("Pointers not permitted at this point in Preserves document"); }); it('should encode properly', () => { const objects: object[] = []; - const A = {a: 1}; - const B = {b: 2}; - expect(encode( - [A, B], - { - encodePointer(e: EncoderState, v: object): void { - objects.push(v); - new Encoder(e, encodeDefaultPointer).push(objects.length - 1); - } - })).is(Bytes.from([Tag.Sequence, - Tag.Pointer, Tag.SmallInteger_lo, - Tag.Pointer, Tag.SmallInteger_lo + 1, - Tag.End])); - expect(objects).is([A, B]); + const pt = new LookasidePointerType(objects); + const A = embed({a: 1}); + const B = embed({b: 2}); + expect(encode([A, B], { pointerType: pt })).is( + Bytes.from([Tag.Sequence, + Tag.Pointer, Tag.SmallInteger_lo, + Tag.Pointer, Tag.SmallInteger_lo + 1, + Tag.End])); + expect(objects).toEqual([A.embeddedValue, B.embeddedValue]); }); it('should decode properly', () => { - const X = {x: 123}; - const Y = {y: 456}; - const objects: object[] = [X, Y]; + const objects: object[] = []; + const pt = new LookasidePointerType(objects); + const X: Pointer = embed({x: 123}); + const Y: Pointer = embed({y: 456}); + objects.push(X.embeddedValue); + objects.push(Y.embeddedValue); expect(decode(Bytes.from([ Tag.Sequence, Tag.Pointer, Tag.SmallInteger_lo, Tag.Pointer, Tag.SmallInteger_lo + 1, Tag.End - ]), { - decodePointer(d: TypedDecoder): object { - const v = d.next(); - if (typeof v !== 'number' || v < 0 || v >= objects.length) { - throw new Error("Unknown pointer target"); - } - return objects[v]; - } - })).is([X, Y]); + ]), { pointerType: pt })).is([X, Y]); }); it('should store pointers embedded in map keys correctly', () => { - const A1 = ({a: 1}); - const A2 = ({a: 1}); + const A1a = {a: 1}; + const A1: Pointer = embed(A1a); + const A2: Pointer = embed({a: 1}); const m = new Dictionary(); m.set([A1], 1); m.set([A2], 2); expect(m.get(A1)).toBeUndefined(); expect(m.get([A1])).toBe(1); expect(m.get([A2])).toBe(2); - expect(m.get([{a: 1}])).toBeUndefined(); - A1.a = 3; + expect(m.get([embed({a: 1})])).toBeUndefined(); + A1a.a = 3; expect(m.get([A1])).toBe(1); }); }); describe('common test suite', () => { const samples_bin = fs.readFileSync(__dirname + '/../../../../../tests/samples.bin'); - const samples = decodeWithAnnotations(samples_bin, { decodePointer: decodeDefaultPointer }); + const samples = decodeWithAnnotations(samples_bin, { pointerType: genericPointerType }); const TestCases = Record.makeConstructor<{ cases: Dictionary @@ -163,13 +191,13 @@ describe('common test suite', () => { type TestCases = ReturnType; function DS(bs: Bytes) { - return decode(bs, { decodePointer: decodeDefaultPointer }); + return decode(bs, { pointerType: genericPointerType }); } function D(bs: Bytes) { - return decodeWithAnnotations(bs, { decodePointer: decodeDefaultPointer }); + return decodeWithAnnotations(bs, { pointerType: genericPointerType }); } function E(v: Value) { - return encodeWithAnnotations(v, { encodePointer: encodeDefaultPointer }); + return encodeWithAnnotations(v, { pointerType: genericPointerType }); } interface ExpectedValues { diff --git a/implementations/javascript/packages/core/test/reader.test.ts b/implementations/javascript/packages/core/test/reader.test.ts index 03539cb..0f4a67e 100644 --- a/implementations/javascript/packages/core/test/reader.test.ts +++ b/implementations/javascript/packages/core/test/reader.test.ts @@ -1,4 +1,4 @@ -import { Bytes, decodeDefaultPointer, Decoder, encode, encodeDefaultPointer, readDefaultPointer, Reader } from '../src/index'; +import { Bytes, Decoder, genericPointerType, encode, Reader } from '../src/index'; import './test-utils'; import * as fs from 'fs'; @@ -8,25 +8,21 @@ describe('reading common test suite', () => { const samples_pr = fs.readFileSync(__dirname + '/../../../../../tests/samples.pr', 'utf-8'); it('should read equal to decoded binary without annotations', () => { - const s1 = new Reader(samples_pr, { decodePointer: readDefaultPointer, includeAnnotations: false }).next(); - const s2 = new Decoder(samples_bin, { includeAnnotations: false }).withPointerDecoder( - decodeDefaultPointer, - d => d.next()); + const s1 = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: false }).next(); + const s2 = new Decoder(samples_bin, { pointerType: genericPointerType, includeAnnotations: false }).next(); expect(s1).is(s2); }); it('should read equal to decoded binary with annotations', () => { - const s1 = new Reader(samples_pr, { decodePointer: readDefaultPointer, includeAnnotations: true }).next(); - const s2 = new Decoder(samples_bin, { includeAnnotations: true }).withPointerDecoder( - decodeDefaultPointer, - d => d.next()); + const s1 = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: true }).next(); + const s2 = new Decoder(samples_bin, { pointerType: genericPointerType, includeAnnotations: true }).next(); expect(s1).is(s2); }); it('should read and encode back to binary with annotations', () => { - const s = new Reader(samples_pr, { decodePointer: readDefaultPointer, includeAnnotations: true }).next(); + const s = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: true }).next(); const bs = Bytes.toIO(encode(s, { - encodePointer: encodeDefaultPointer, + pointerType: genericPointerType, includeAnnotations: true, canonical: true, })); diff --git a/implementations/javascript/packages/core/test/test-utils.ts b/implementations/javascript/packages/core/test/test-utils.ts index a90469a..251864d 100644 --- a/implementations/javascript/packages/core/test/test-utils.ts +++ b/implementations/javascript/packages/core/test/test-utils.ts @@ -1,10 +1,10 @@ -import { Value, is, preserves, strip, TypedDecoder, Encoder } from '../src/index'; +import { Value, is, preserves } from '../src/index'; import '../src/node_support'; declare global { namespace jest { interface Matchers { - is(expected: Value): R; + is(expected: Value): R; toThrowFilter(f: (e: Error) => boolean): R; } } diff --git a/implementations/javascript/packages/core/test/values.test.ts b/implementations/javascript/packages/core/test/values.test.ts index 5bc6212..ab60c99 100644 --- a/implementations/javascript/packages/core/test/values.test.ts +++ b/implementations/javascript/packages/core/test/values.test.ts @@ -1,4 +1,4 @@ -import { Single, Double, fromJS, Dictionary, IDENTITY_FOLD, fold, mapPointers, Value } from '../src/index'; +import { Single, Double, fromJS, Dictionary, IDENTITY_FOLD, fold, mapPointers, Value, embed } from '../src/index'; import './test-utils'; describe('Single', () => { @@ -33,6 +33,6 @@ describe('fold', () => { const w1 = new Date(); const v1 = mkv(w1); expect(fold(v, IDENTITY_FOLD)).not.is(v1); - expect(mapPointers(v, _t => w1)).is(v1); + expect(mapPointers(v, _t => embed(w1))).is(v1); }); }); diff --git a/implementations/javascript/packages/schema/src/compiler/value.ts b/implementations/javascript/packages/schema/src/compiler/value.ts index 2383b4a..0170b1c 100644 --- a/implementations/javascript/packages/schema/src/compiler/value.ts +++ b/implementations/javascript/packages/schema/src/compiler/value.ts @@ -1,7 +1,7 @@ -import { Annotated, Bytes, Dictionary, Fold, fold, Record, Tuple, Value, stringify } from "@preserves/core"; +import { Annotated, Bytes, Set, Dictionary, Fold, fold, Record, Tuple, Value, stringify } from "@preserves/core"; import { brackets, Item, parens, seq } from "./block"; -export function sourceCodeFor(v: Value): Item { +export function sourceCodeFor(v: Value): Item { return fold(v, { boolean(b: boolean): Item { return b.toString(); }, single(f: number): Item { return f.toString(); }, @@ -13,25 +13,25 @@ export function sourceCodeFor(v: Value): Item { }, symbol(s: symbol): Item { return `Symbol.for(${JSON.stringify(s.description!)})`; }, - record(r: Record, Tuple>, any>, k: Fold): Item { + record(r: Record, Tuple>, never>, k: Fold): Item { return seq(`_.Record<_val, _.Tuple<_val>, _ptr>`, parens(k(r.label), brackets(... r.map(k)))); }, - array(a: Array>, k: Fold): Item { + array(a: Array>, k: Fold): Item { return brackets(... a.map(k)); }, - set(s: Set, k: Fold): Item { + set(s: Set, k: Fold): Item { return seq('new _.Set<_val>', parens(brackets(... Array.from(s).map(k)))); }, - dictionary(d: Dictionary, k: Fold): Item { + dictionary(d: Dictionary, k: Fold): Item { return seq('new _.Dictionary<_ptr>', parens(brackets(... Array.from(d).map(([kk,vv]) => brackets(k(kk), k(vv)))))); }, - annotated(a: Annotated, k: Fold): Item { + annotated(a: Annotated, k: Fold): Item { return seq('_.annotate<_ptr>', parens(k(a.item), ... a.annotations.map(k))); }, - pointer(t: any, _k: Fold): Item { + pointer(t: never, _k: Fold): Item { throw new Error(`Cannot emit source code for construction of pointer ${stringify(t)}`); }, });