Much better duck typing

This commit is contained in:
Tony Garnock-Jones 2021-01-11 16:54:52 +01:00
parent 10351b5369
commit 329cee7bd6
6 changed files with 197 additions and 143 deletions

View File

@ -12,27 +12,30 @@ import { Tag } from './constants';
import { PreserveOn } from './symbols';
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
export function isCodecError(e: any, t?: ErrorType): e is PreservesCodecError {
return typeof e === 'object' && e !== null &&
'_codecErrorType' in e &&
(!t || e._codecErrorType() === t);
}
export const isDecodeError = (e: any): e is DecodeError => isCodecError(e, 'DecodeError');
export const isEncodeError = (e: any): e is EncodeError => isCodecError(e, 'EncodeError');
export const isShortPacket = (e: any): e is ShortPacket => isCodecError(e, 'ShortPacket');
export const ErrorType = Symbol.for('ErrorType');
export abstract class PreservesCodecError {
abstract _codecErrorType(): ErrorType;
abstract get [ErrorType](): ErrorType;
static isCodecError(e: any, t: ErrorType): e is PreservesCodecError {
return (e?.[ErrorType] === t);
}
}
export class DecodeError extends Error {
_codecErrorType(): ErrorType { return 'DecodeError' }
get [ErrorType](): ErrorType { return 'DecodeError' }
static isDecodeError(e: any): e is DecodeError {
return PreservesCodecError.isCodecError(e, 'DecodeError');
}
}
export class EncodeError extends Error {
_codecErrorType(): ErrorType { return 'EncodeError' }
get [ErrorType](): ErrorType { return 'EncodeError' }
static isEncodeError(e: any): e is EncodeError {
return PreservesCodecError.isCodecError(e, 'EncodeError');
}
readonly irritant: any;
@ -43,7 +46,11 @@ export class EncodeError extends Error {
}
export class ShortPacket extends DecodeError {
_codecErrorType(): ErrorType { return 'ShortPacket' }
get [ErrorType](): ErrorType { return 'ShortPacket' }
static isShortPacket(e: any): e is ShortPacket {
return PreservesCodecError.isCodecError(e, 'ShortPacket');
}
}
export interface DecoderOptions {
@ -176,7 +183,7 @@ export class Decoder {
try {
return this.next();
} catch (e) {
if (e instanceof ShortPacket) {
if (ShortPacket.isShortPacket(e)) {
this.index = start;
return void 0;
}
@ -266,7 +273,7 @@ export class Encoder {
this.emitbyte(Tag.SignedInteger);
this.varint(bytecount);
}
const enc = (n, x) => {
const enc = (n: number, x: number) => {
if (n > 0) {
enc(n - 1, x >> 8);
this.emitbyte(x & 255);
@ -294,7 +301,7 @@ export class Encoder {
}
push(v: any) {
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
if (typeof v?.[PreserveOn] === 'function') {
v[PreserveOn](this);
}
else if (typeof v === 'boolean') {
@ -326,7 +333,7 @@ export class Encoder {
else if (Array.isArray(v)) {
this.encodevalues(Tag.Sequence, v);
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
else if (typeof v?.[Symbol.iterator] === 'function') {
this.encodevalues(Tag.Sequence, v as Iterable<Value>);
}
else {

View File

@ -22,6 +22,20 @@ export type IdentitySet<V> = Set<V>;
export const IdentityMap = Map;
export const IdentitySet = Set;
export const IsMap = Symbol.for('IsMap');
export const IsSet = Symbol.for('IsSet');
declare global {
interface Map<K, V> { [IsMap]: boolean; }
interface MapConstructor { isMap<K, V>(x: any): x is Map<K, V>; }
interface Set<T> { [IsSet]: boolean; }
interface SetConstructor { isSet<T>(x: any): x is Set<T>; }
}
Object.defineProperty(Map.prototype, IsMap, { get() { return true; } });
Map.isMap = <K,V> (x: any): x is Map<K, V> => !!x?.[IsMap];
Object.defineProperty(Set.prototype, IsSet, { get() { return true; } });
Set.isSet = <T> (x: any): x is Set<T> => !!x?.[IsSet];
export function _iterMap<S,T>(i: Iterator<S> | undefined, f : (s: S) => T): IterableIterator<T> {
if (!i) return void 0;
const _f = (r: IteratorResult<S>): IteratorResult<T> => {
@ -139,6 +153,10 @@ export class FlexMap<K, V> implements Map<K, V> {
canonicalKeys(): IterableIterator<string> {
return this.items.keys();
}
get [IsMap](): boolean {
return true;
}
}
export class FlexSet<V> implements Set<V> {
@ -238,4 +256,8 @@ export class FlexSet<V> implements Set<V> {
for (let k of this) if (!other.has(k)) result.add(k);
return result;
}
get [IsSet](): boolean {
return true;
}
}

View File

@ -1,7 +1,7 @@
import { Value } from './values';
export function stringify(x: any): string {
if (typeof x === 'object' && x !== null && 'asPreservesText' in x) {
if (typeof x?.asPreservesText === 'function') {
return x.asPreservesText();
} else {
try {

View File

@ -4,7 +4,7 @@ import { PreserveOn, AsPreserve } from './symbols';
import { Tag } from './constants';
import { Encoder, encode } from './codec';
import { stringify } from './text';
import { _iterMap, FlexMap, FlexSet, IdentityMap, IdentitySet } from './flex';
import { _iterMap, FlexMap, FlexSet } from './flex';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
@ -13,9 +13,9 @@ export type Value = Atom | Compound | Annotated;
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
export type Compound = Record | Array<Value> | Set | Dictionary<Value>;
export function isRecord(x: any): x is Record {
return Array.isArray(x) && 'label' in x;
}
export const IsPreservesRecord = Symbol.for('IsPreservesRecord');
export const IsPreservesBytes = Symbol.for('IsPreservesBytes');
export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated');
export function fromJS(x: any): Value {
switch (typeof x) {
@ -41,7 +41,7 @@ export function fromJS(x: any): Value {
if (typeof x[AsPreserve] === 'function') {
return x[AsPreserve]();
}
if (isRecord(x)) {
if (Record.isRecord(x)) {
return x;
}
if (Array.isArray(x)) {
@ -57,6 +57,7 @@ export function fromJS(x: any): Value {
}
export type FloatType = 'Single' | 'Double';
export const FloatType = Symbol.for('FloatType');
export abstract class Float {
readonly value: number;
@ -78,6 +79,14 @@ export abstract class Float {
}
abstract asPreservesText(): string;
abstract get [FloatType](): FloatType;
static isFloat(x: any, t: FloatType): x is Float {
return (x?.[FloatType] === t);
}
static isSingle = (x: any): x is Single => Float.isFloat(x, 'Single');
static isDouble = (x: any): x is Double => Float.isFloat(x, 'Double');
}
export class Single extends Float {
@ -92,7 +101,7 @@ export class Single extends Float {
encoder.index += 4;
}
_floatType(): FloatType {
get [FloatType](): FloatType {
return 'Single';
}
@ -113,7 +122,7 @@ export class Double extends Float {
encoder.index += 8;
}
_floatType(): FloatType {
get [FloatType](): FloatType {
return 'Double';
}
@ -122,22 +131,13 @@ export class Double extends Float {
}
}
function isFloat(x: any, t: FloatType): x is Float {
return typeof x === 'object' && x !== null &&
'value' in x && typeof x.value === 'number' &&
'_floatType' in x && x._floatType() === t;
}
export const isSingle = (x: any): x is Single => isFloat(x, 'Single');
export const isDouble = (x: any): x is Double => isFloat(x, 'Double');
export type BytesLike = Bytes | Uint8Array;
export class Bytes {
readonly _view: Uint8Array;
constructor(maybeByteIterable: any = new Uint8Array()) {
if (isBytes(maybeByteIterable)) {
if (Bytes.isBytes(maybeByteIterable)) {
this._view = maybeByteIterable._view;
} else if (ArrayBuffer.isView(maybeByteIterable)) {
this._view = new Uint8Array(maybeByteIterable.buffer,
@ -182,13 +182,13 @@ export class Bytes {
static fromIO(io: string | BytesLike): string | Bytes {
if (typeof io === 'string') return io;
if (isBytes(io)) return io;
if (Bytes.isBytes(io)) return io;
if (io instanceof Uint8Array) return new Bytes(io);
}
static toIO(b : string | BytesLike): string | Uint8Array {
if (typeof b === 'string') return b;
if (isBytes(b)) return b._view;
if (Bytes.isBytes(b)) return b._view;
if (b instanceof Uint8Array) return b;
}
@ -211,7 +211,7 @@ export class Bytes {
}
equals(other: any): boolean {
if (!isBytes(other)) return false;
if (!Bytes.isBytes(other)) return false;
if (other.length !== this.length) return false;
const va = this._view;
const vb = other._view;
@ -287,6 +287,14 @@ export class Bytes {
encoder.varint(this.length);
encoder.emitbytes(this._view);
}
get [IsPreservesBytes](): boolean {
return true;
}
static isBytes(x: any): x is Bytes {
return !!x?.[IsPreservesBytes];
}
}
export function hexDigit(n: number): string {
@ -300,12 +308,6 @@ export function unhexDigit(asciiCode: number) {
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
}
export function isBytes(x: any): x is Bytes {
return typeof x === 'object' && x !== null &&
'_view' in x &&
x._view instanceof Uint8Array;
}
export function underlying(b: Bytes | Uint8Array): Uint8Array {
return (b instanceof Uint8Array) ? b : b._view;
}
@ -437,7 +439,7 @@ export class Record extends Array<Value> {
}
equals(other: any): boolean {
return isRecord(other) &&
return Record.isRecord(other) &&
is(this.label, other.label) &&
this.every((f, i) => is(f, other.get(i)));
}
@ -483,9 +485,9 @@ export class Record extends Array<Value> {
}
return new Record(label, fields);
};
ctor.constructorInfo = { label, arity };
ctor.isClassOf =
(v: any): v is Record => (isRecord(v) && is(label, v.label) && v.length === arity);
const constructorInfo = { label, arity };
ctor.constructorInfo = constructorInfo;
ctor.isClassOf = (v: any): v is Record => Record.isClassOf(constructorInfo, v);
ctor._ = {};
fieldNames.forEach((name, i) => {
ctor._[name] = function (r: any): Value {
@ -505,6 +507,18 @@ export class Record extends Array<Value> {
this.forEach((f) => encoder.push(f));
encoder.emitbyte(Tag.End);
}
get [IsPreservesRecord](): boolean {
return true;
}
static isRecord(x: any): x is Record {
return !!x?.[IsPreservesRecord];
}
static isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
}
export interface RecordConstructor {
@ -520,8 +534,8 @@ export interface RecordConstructorInfo {
}
export function is(a: any, b: any): boolean {
if (isAnnotated(a)) a = a.item;
if (isAnnotated(b)) b = b.item;
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') {
@ -540,18 +554,8 @@ export function hash(a: Value): number {
throw new Error("shouldBeImplemented"); // TODO
}
export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
return (isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
export type DictionaryType = 'Dictionary' | 'Set';
export function is_Dictionary(x: any, t: DictionaryType): boolean {
return typeof x === 'object' && x !== null && x[Symbol.toStringTag] === t;
}
export const isDictionary = <T> (x: any): x is Dictionary<T> => is_Dictionary(x, 'Dictionary');
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
export const DictionaryType = Symbol.for('DictionaryType');
export function _canonicalString(item: Value): string {
const bs = encode(item, { canonical: true })._view;
@ -560,8 +564,16 @@ export function _canonicalString(item: Value): string {
}
export class Dictionary<T> extends FlexMap<Value, T> {
get [DictionaryType](): DictionaryType {
return 'Dictionary';
}
static isDictionary<T>(x: any): x is Dictionary<T> {
return x?.[DictionaryType] === 'Dictionary';
}
static fromJS(x: object): Dictionary<Value> {
if (isDictionary(x)) return x as Dictionary<Value>;
if (Dictionary.isDictionary(x)) return x as Dictionary<Value>;
const d = new Dictionary<Value>();
for (let key in x) {
const value = x[key];
@ -619,6 +631,14 @@ export class Dictionary<T> extends FlexMap<Value, T> {
}
export class Set extends FlexSet<Value> {
get [DictionaryType](): DictionaryType {
return 'Set';
}
static isSet(x: any): x is Set {
return x?.[DictionaryType] === 'Set';
}
constructor(items?: Iterable<any>) {
super(_canonicalString, _iterMap(items?.[Symbol.iterator](), fromJS));
}
@ -656,13 +676,6 @@ export class Set extends FlexSet<Value> {
}
}
export function isAnnotated(x: any): x is Annotated {
return typeof x === 'object' && x !== null &&
x.constructor.name === 'Annotated' &&
'annotations' in x &&
'item' in x;
}
export class Annotated {
readonly annotations: Array<Value>;
readonly item: Value;
@ -687,7 +700,7 @@ export class Annotated {
}
equals(other: any): boolean {
return is(this.item, isAnnotated(other) ? other.item : other);
return is(this.item, Annotated.isAnnotated(other) ? other.item : other);
}
hashCode(): number {
@ -702,6 +715,14 @@ export class Annotated {
const anns = this.annotations.map((a) => '@' + a.asPreservesText()).join(' ');
return (anns ? anns + ' ' : anns) + this.item.asPreservesText();
}
get [IsPreservesAnnotated](): boolean {
return true;
}
static isAnnotated(x: any): x is Annotated {
return !!x?.[IsPreservesAnnotated];
}
}
export function peel(v: Value): Value {
@ -711,20 +732,20 @@ export function peel(v: Value): Value {
export function strip(v: Value, depth: number = Infinity) {
function step(v: Value, depth: number): Value {
if (depth === 0) return v;
if (!isAnnotated(v)) return v;
if (!Annotated.isAnnotated(v)) return v;
const nextDepth = depth - 1;
function walk(v: Value) { return step(v, nextDepth); }
if (isRecord(v.item)) {
if (Record.isRecord(v.item)) {
return new Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return v.item.map(walk);
} else if (isSet(v.item)) {
} else if (Set.isSet(v.item)) {
return v.item.map(walk);
} else if (isDictionary(v.item)) {
} else if (Dictionary.isDictionary(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (isAnnotated(v.item)) {
} else if (Annotated.isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else {
return v.item;
@ -734,7 +755,7 @@ export function strip(v: Value, depth: number = Infinity) {
}
export function annotate(v0: Value, ...anns: Value[]) {
const v = isAnnotated(v0) ? v0 : new Annotated(v0);
const v = Annotated.isAnnotated(v0) ? v0 : new Annotated(v0);
anns.forEach((a) => v.annotations.push(a));
return v;
}

View File

@ -13,68 +13,70 @@ describe('immutable byte arrays', () => {
expect(bs.every((b) => b !== 50)).toBe(true);
expect(!(bs.every((b) => b !== 20))).toBe(true);
});
// it('should implement find', () => {
// assert.strictEqual(bs.find((b) => b > 20), 30);
// assert.strictEqual(bs.find((b) => b > 50), void 0);
// });
// it('should implement findIndex', () => {
// assert.strictEqual(bs.findIndex((b) => b > 20), 2);
// assert.strictEqual(bs.findIndex((b) => b > 50), -1);
// });
// it('should implement forEach', () => {
// const vs = [];
// bs.forEach((b) => vs.push(b));
// assert(is(fromJS(vs), fromJS([10, 20, 30, 40])));
// });
// it('should implement includes', () => {
// assert(bs.includes(20));
// assert(!bs.includes(50));
// });
// it('should implement indexOf', () => {
// assert.strictEqual(bs.indexOf(20), 1);
// assert.strictEqual(bs.indexOf(50), -1);
// });
// it('should implement join', () => assert.strictEqual(bs.join('-'), '10-20-30-40'));
// it('should implement keys', () => {
// assert(is(fromJS(Array.from(bs.keys())), fromJS([0,1,2,3])));
// });
// it('should implement values', () => {
// assert(is(fromJS(Array.from(bs.values())), fromJS([10,20,30,40])));
// });
// it('should implement filter', () => {
// assert(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40)));
// });
// it('should implement slice', () => {
// const vs = bs.slice(2);
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(vs._view.buffer.byteLength, 2);
// assert.strictEqual(vs.get(0), 30);
// assert.strictEqual(vs.get(1), 40);
// assert.strictEqual(vs.size, 2);
// });
// it('should implement subarray', () => {
// const vs = bs.subarray(2);
// assert(Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(vs._view.buffer.byteLength, 4);
// assert.strictEqual(vs.get(0), 30);
// assert.strictEqual(vs.get(1), 40);
// assert.strictEqual(vs.size, 2);
// });
// it('should implement reverse', () => {
// const vs = bs.reverse();
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(bs.get(0), 10);
// assert.strictEqual(bs.get(3), 40);
// assert.strictEqual(vs.get(0), 40);
// assert.strictEqual(vs.get(3), 10);
// });
// it('should implement sort', () => {
// const vs = bs.reverse().sort();
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(bs.get(0), 10);
// assert.strictEqual(bs.get(3), 40);
// assert.strictEqual(vs.get(0), 10);
// assert.strictEqual(vs.get(3), 40);
// });
it('should implement find', () => {
expect(bs.find((b) => b > 20)).toBe(30);
expect(bs.find((b) => b > 50)).toBe(void 0);
});
it('should implement findIndex', () => {
expect(bs.findIndex((b) => b > 20)).toBe(2);
expect(bs.findIndex((b) => b > 50)).toBe(-1);
});
it('should implement forEach', () => {
const vs = [];
bs.forEach((b) => vs.push(b));
expect(fromJS(vs)).is(fromJS([10, 20, 30, 40]));
});
it('should implement includes', () => {
expect(bs.includes(20)).toBe(true);
expect(!bs.includes(50)).toBe(true);
});
it('should implement indexOf', () => {
expect(bs.indexOf(20)).toBe(1);
expect(bs.indexOf(50)).toBe(-1);
});
it('should implement join', () => {
expect(bs.join('-')).toBe('10-20-30-40');
});
it('should implement keys', () => {
expect(fromJS(Array.from(bs.keys()))).is(fromJS([0,1,2,3]));
});
it('should implement values', () => {
expect(fromJS(Array.from(bs.values()))).is(fromJS([10,20,30,40]));
});
it('should implement filter', () => {
expect(bs.filter((b) => b !== 30)).is(Bytes.of(10,20,40));
});
it('should implement slice', () => {
const vs = bs.slice(2);
expect(Object.is(vs._view.buffer, bs._view.buffer)).toBe(false);
expect(vs._view.buffer.byteLength).toBe(2);
expect(vs.get(0)).toBe(30);
expect(vs.get(1)).toBe(40);
expect(vs.length).toBe(2);
});
it('should implement subarray', () => {
const vs = bs.subarray(2);
expect(Object.is(vs._view.buffer, bs._view.buffer)).toBe(true);
expect(vs._view.buffer.byteLength).toBe(4);
expect(vs.get(0)).toBe(30);
expect(vs.get(1)).toBe(40);
expect(vs.length).toBe(2);
});
it('should implement reverse', () => {
const vs = bs.reverse();
expect(Object.is(vs._view.buffer, bs._view.buffer)).toBe(false);
expect(bs.get(0)).toBe(10);
expect(bs.get(3)).toBe(40);
expect(vs.get(0)).toBe(40);
expect(vs.get(3)).toBe(10);
});
it('should implement sort', () => {
const vs = bs.reverse().sort();
expect(Object.is(vs._view.buffer, bs._view.buffer)).toBe(false);
expect(bs.get(0)).toBe(10);
expect(bs.get(3)).toBe(40);
expect(vs.get(0)).toBe(10);
expect(vs.get(3)).toBe(40);
});
});
});

View File

@ -2,7 +2,7 @@ import {
Value,
Dictionary,
decode, decodeWithAnnotations, encodeWithAnnotations,
isDecodeError, isShortPacket,
DecodeError, ShortPacket,
Bytes, Record,
annotate,
strip, peel,
@ -150,7 +150,9 @@ describe('common test suite', () => {
describe(tName, () => {
it('should fail with DecodeError', () => {
expect(() => D(strip(t[0]) as Bytes))
.toThrowFilter(e => isDecodeError(e) && !isShortPacket(e));
.toThrowFilter(e =>
DecodeError.isDecodeError(e) &&
!ShortPacket.isShortPacket(e));
});
});
break;
@ -159,7 +161,7 @@ describe('common test suite', () => {
describe(tName, () => {
it('should fail with ShortPacket', () => {
expect(() => D(strip(t[0]) as Bytes))
.toThrowFilter(e => isShortPacket(e));
.toThrowFilter(e => ShortPacket.isShortPacket(e));
});
});
break;