Mark Embeddable objects specially, so that plain JS objects can be used as symbol-to-value maps.

This commit is contained in:
Tony Garnock-Jones 2024-03-27 09:13:04 +01:00
parent 055a7f90e9
commit 4f4ff6e108
19 changed files with 444 additions and 173 deletions

View File

@ -219,13 +219,17 @@ export class Decoder<T extends Embeddable = never> implements TypedDecoder<T> {
}
static dictionaryFromArray<T extends Embeddable>(vs: Value<T>[]): Dictionary<T> {
const d = new Dictionary<T>();
const entries: [Value<T>, Value<T>][] = [];
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
for (let i = 0; i < vs.length; i += 2) {
if (d.has(vs[i])) throw new DecodeError(`Duplicate key: ${stringify(vs[i])}`);
d.set(vs[i], vs[i+1]);
entries.push([vs[i], vs[i+1]]);
}
const r = Dictionary.from<T, typeof entries[0]>(entries, true);
if ('duplicate' in r) {
throw new DecodeError("Duplicate key");
} else {
return r.ok;
}
return d;
}
next(): Value<T> {

View File

@ -1,19 +1,21 @@
import { Encoder, canonicalString } from "./encoder";
import { Tag } from "./constants";
import { FlexMap, FlexSet, _iterMap, IdentitySet } from "./flex";
import { FlexMap, FlexSet, _iterMap, IdentitySet, Equivalence } from "./flex";
import { Value } from "./values";
import { Bytes } from './bytes';
import type { Embeddable, GenericEmbedded } from "./embedded";
import { Embeddable, GenericEmbedded, isEmbedded } from "./embedded";
import type { Preservable } from "./encoder";
import type { Writer, PreserveWritable } from "./writer";
import { annotations, Annotated } from "./annotated";
import { Float } from "./float";
import { JsDictionary, JsDictionaryMap } from "./jsdictionary";
export type DictionaryType = 'Dictionary' | 'Set';
export const DictionaryType = Symbol.for('DictionaryType');
export type CompoundKey<T extends Embeddable> = Value<T> | (Preservable<T> & PreserveWritable<T>);
export class EncodableDictionary<K, V, T extends Embeddable = GenericEmbedded> extends FlexMap<K, V>
export class EncodableDictionary<T extends Embeddable, K, V> extends FlexMap<K, V>
implements Preservable<T>, PreserveWritable<T>
{
constructor(
@ -39,16 +41,16 @@ export class EncodableDictionary<K, V, T extends Embeddable = GenericEmbedded> e
}
}
export class KeyedDictionary<K extends CompoundKey<T>, V, T extends Embeddable = GenericEmbedded>
extends EncodableDictionary<K, V, T>
export class KeyedDictionary<T extends Embeddable = GenericEmbedded, K extends CompoundKey<T> = Value<T>, V = Value<T>>
extends EncodableDictionary<T, K, V>
{
get [DictionaryType](): DictionaryType {
return 'Dictionary';
}
static isKeyedDictionary<K extends CompoundKey<T>, V, T extends Embeddable = GenericEmbedded>(
static isKeyedDictionary<T extends Embeddable = GenericEmbedded, K extends CompoundKey<T> = Value<T>, V = Value<T>>(
x: any,
): x is KeyedDictionary<K, V, T> {
): x is KeyedDictionary<T, K, V> {
return x?.[DictionaryType] === 'Dictionary';
}
@ -58,35 +60,110 @@ export class KeyedDictionary<K extends CompoundKey<T>, V, T extends Embeddable =
super(k => k, v => v as CompoundKey<T>, items);
}
mapEntries<W, S extends Value<R>, R extends Embeddable = GenericEmbedded>(
f: (entry: [K, V]) => [S, W],
): KeyedDictionary<S, W, R> {
const result = new KeyedDictionary<S, W, R>();
for (let oldEntry of this.entries()) {
const newEntry = f(oldEntry);
result.set(newEntry[0], newEntry[1])
}
return result;
}
clone(): KeyedDictionary<K, V, T> {
clone(): KeyedDictionary<T, K, V> {
return new KeyedDictionary(this);
}
get [Symbol.toStringTag]() { return 'Dictionary'; }
equals(otherAny: any, eqv: Equivalence<V> = (v1, v2) => v1 === v2): boolean {
const otherMap = Dictionary.asMap(otherAny);
if (!otherMap) return false;
return super.equals(otherMap, eqv);
}
}
export class Dictionary<T extends Embeddable = GenericEmbedded, V = Value<T>> extends KeyedDictionary<Value<T>, V, T> {
static isDictionary<T extends Embeddable = GenericEmbedded, V = Value<T>>(x: any): x is Dictionary<T, V> {
return x?.[DictionaryType] === 'Dictionary';
export type Dictionary<T extends Embeddable = GenericEmbedded, V = Value<T>> =
JsDictionary<V> | KeyedDictionary<T, Value<T>, V>;
export namespace Dictionary {
export function isDictionary<T extends Embeddable = GenericEmbedded, V = Value<T>>(
x: any
): x is Dictionary<T, V> {
if (typeof x !== 'object' || x === null) return false;
switch (x[DictionaryType]) {
case 'Dictionary': return true;
case void 0:
if (Array.isArray(x)) return false;
if (isEmbedded(x)) return false;
if (Float.isFloat(x)) return false;
if (Bytes.isBytes(x)) return false;
return true;
default: return false;
}
}
static __from_preserve__<T extends Embeddable>(v: Value<T>): undefined | Dictionary<T> {
export function mapInto<K, V, S, W, O extends Map<S, W>>(
i: Map<K, V>,
o: O,
f: (entry: [K, V]) => [S, W],
): O {
for (let oldEntry of i.entries()) {
const newEntry = f(oldEntry);
o.set(newEntry[0], newEntry[1]);
}
return o;
}
export function asMap<T extends Embeddable = GenericEmbedded, V = Value<T>>(
x: Dictionary<T>
): Map<Value<T>, V>;
export function asMap<T extends Embeddable = GenericEmbedded, V = Value<T>>(
x: any
): Map<Value<T>, V> | undefined;
export function asMap<T extends Embeddable = GenericEmbedded, V = Value<T>>(
x: any
): Map<Value<T>, V> | undefined {
if (!isDictionary<T, V>(x)) return void 0;
if (DictionaryType in x) return x;
return new JsDictionaryMap(x);
}
export function from<T extends Embeddable, E extends [Value<T>, Value<T>, ... any[]]>(
entries: Iterable<E>, failOnDuplicateKeys: true): {ok: Dictionary<T>} | {duplicate: E};
export function from<T extends Embeddable, E extends [Value<T>, Value<T>, ... any[]]>(
entries: Iterable<E>, failOnDuplicateKeys: false): Dictionary<T>;
export function from<T extends Embeddable, E extends [Value<T>, Value<T>, ... any[]]>(
entries: Iterable<E>): Dictionary<T>;
export function from<T extends Embeddable, E extends [Value<T>, Value<T>, ... any[]]>(
entries0: Iterable<E>,
failOnDuplicateKeys: boolean = false,
): Dictionary<T> | {ok: Dictionary<T>} | {duplicate: E} {
const typeSet = new IdentitySet();
const entries: E[] = [];
for (const e of entries0) {
typeSet.add(typeof e[0]);
entries.push(e);
}
const types = Array.from(typeSet);
if (types.length === 1 && types[0] === 'symbol') {
const result: JsDictionary<Value<T>> = {};
for (const e of entries) {
const k = (e[0] as symbol).description!;
if (failOnDuplicateKeys && Object.prototype.hasOwnProperty.call(result, k)) {
return {duplicate: e};
}
result[k] = e[1];
}
return failOnDuplicateKeys ? {ok: result} : result;
} else {
const result = new KeyedDictionary<T>();
for (const e of entries) {
if (failOnDuplicateKeys && result.has(e[0])) {
return {duplicate: e};
}
result.set(e[0], e[1]);
}
return failOnDuplicateKeys ? {ok: result} : result;
}
}
export function __from_preserve__<T extends Embeddable>(v: Value<T>): undefined | Dictionary<T> {
return Dictionary.isDictionary<T>(v) ? v : void 0;
}
}
export function encodeDictionaryOn<K, V, T extends Embeddable>(
export function encodeDictionaryOn<T extends Embeddable, K, V>(
dict: Map<K, V>,
encoder: Encoder<T>,
encodeK: (k: K, encoder: Encoder<T>) => void,
@ -116,7 +193,7 @@ export function encodeDictionaryOn<K, V, T extends Embeddable>(
}
}
export function writeDictionaryOn<K, V, T extends Embeddable>(
export function writeDictionaryOn<T extends Embeddable, K, V>(
dict: Map<K, V>,
w: Writer<T>,
writeK: (k: K, w: Writer<T>) => void,
@ -137,7 +214,7 @@ export function writeDictionaryOn<K, V, T extends Embeddable>(
});
}
export class EncodableSet<V, T extends Embeddable = GenericEmbedded> extends FlexSet<V>
export class EncodableSet<T extends Embeddable, V> extends FlexSet<V>
implements Preservable<T>, PreserveWritable<T>
{
constructor(
@ -156,16 +233,16 @@ export class EncodableSet<V, T extends Embeddable = GenericEmbedded> extends Fle
}
}
export class KeyedSet<K extends CompoundKey<T>, T extends Embeddable = GenericEmbedded>
extends EncodableSet<K, T>
export class KeyedSet<T extends Embeddable = GenericEmbedded, K extends CompoundKey<T> = Value<T>>
extends EncodableSet<T, K>
{
get [DictionaryType](): DictionaryType {
return 'Set';
}
static isKeyedSet<K extends CompoundKey<T>, T extends Embeddable = GenericEmbedded>(
static isKeyedSet<T extends Embeddable = GenericEmbedded, K extends CompoundKey<T> = Value<T>>(
x: any,
): x is KeyedSet<K, T> {
): x is KeyedSet<T, K> {
return x?.[DictionaryType] === 'Set';
}
@ -173,26 +250,26 @@ export class KeyedSet<K extends CompoundKey<T>, T extends Embeddable = GenericEm
super(k => k, items);
}
map<S extends Value<R>, R extends Embeddable = GenericEmbedded>(
map<R extends Embeddable = GenericEmbedded, S extends Value<R> = Value<R>>(
f: (value: K) => S,
): KeyedSet<S, R> {
): KeyedSet<R, S> {
return new KeyedSet(_iterMap(this[Symbol.iterator](), f));
}
filter(f: (value: K) => boolean): KeyedSet<K, T> {
const result = new KeyedSet<K, T>();
filter(f: (value: K) => boolean): KeyedSet<T, K> {
const result = new KeyedSet<T, K>();
for (let k of this) if (f(k)) result.add(k);
return result;
}
clone(): KeyedSet<K, T> {
clone(): KeyedSet<T, K> {
return new KeyedSet(this);
}
get [Symbol.toStringTag]() { return 'Set'; }
}
export class Set<T extends Embeddable = GenericEmbedded> extends KeyedSet<Value<T>, T> {
export class Set<T extends Embeddable = GenericEmbedded> extends KeyedSet<T> {
static isSet<T extends Embeddable = GenericEmbedded>(x: any): x is Set<T> {
return x?.[DictionaryType] === 'Set';
}
@ -202,7 +279,7 @@ export class Set<T extends Embeddable = GenericEmbedded> extends KeyedSet<Value<
}
}
export function encodeSetOn<V, T extends Embeddable>(
export function encodeSetOn<T extends Embeddable, V>(
s: IdentitySet<V>,
encoder: Encoder<T>,
encodeV: (v: V, encoder: Encoder<T>) => void,
@ -223,7 +300,7 @@ export function encodeSetOn<V, T extends Embeddable>(
}
}
export function writeSetOn<V, T extends Embeddable>(
export function writeSetOn<T extends Embeddable, V>(
s: IdentitySet<V>,
w: Writer<T>,
writeV: (v: V, w: Writer<T>) => void,

View File

@ -1,13 +1,17 @@
import type { DecoderState } from "./decoder";
import type { EncoderState } from "./encoder";
import type { Value } from "./values";
import { Annotated } from "./annotated";
import { Bytes } from "./bytes";
import { Float } from "./float";
import { Set, Dictionary } from "./dictionary";
import { ReaderStateOptions } from "./reader";
export type Embeddable = object;
export const IsEmbedded = Symbol.for('IsEmbedded');
export interface Embeddable {
readonly [IsEmbedded]: true;
}
export function isEmbedded<T extends Embeddable>(v: any): v is T {
return !!v?.[IsEmbedded];
}
export type EmbeddedTypeEncode<T extends Embeddable> = {
encode(s: EncoderState, v: T): void;
@ -20,28 +24,20 @@ export type EmbeddedTypeDecode<T> = {
export type EmbeddedType<T extends Embeddable> = EmbeddedTypeEncode<T> & EmbeddedTypeDecode<T>;
export function isEmbedded<T extends Embeddable>(v: Value<T>): v is T {
return (typeof(v) === 'object')
&& (v !== null)
&& !Array.isArray(v)
&& !Bytes.isBytes(v)
&& !Float.isFloat(v)
&& !Set.isSet<T>(v)
&& !Dictionary.isDictionary<T>(v)
&& !Annotated.isAnnotated<T>(v)
&& ((_v: T) => true)(v);
}
export class Embedded<T> {
get [IsEmbedded](): true { return true; }
constructor(public readonly value: T) {}
equals(other: any): boolean {
return typeof other === 'object' && 'value' in other && Object.is(this.value, other.value);
}
}
export class GenericEmbedded {
generic: Value;
get [IsEmbedded](): true { return true; }
constructor(generic: Value) {
this.generic = generic;
}
constructor(public readonly generic: Value) {}
equals(other: any, is: (a: any, b: any) => boolean) {
return typeof other === 'object' && 'generic' in other && is(this.generic, other.generic);

View File

@ -3,8 +3,10 @@ import { Bytes, unhexDigit } from "./bytes";
import { Value } from "./values";
import { EncodeError } from "./codec";
import { Record, Tuple } from "./record";
import { EmbeddedTypeEncode } from "./embedded";
import { EmbeddedTypeEncode, isEmbedded } from "./embedded";
import type { Embeddable } from "./embedded";
import { encodeDictionaryOn } from "./dictionary";
import { JsDictionaryMap } from "./jsdictionary";
export type Encodable<T extends Embeddable> =
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
@ -288,11 +290,16 @@ export class Encoder<T extends Embeddable> {
for (let i of v) this.push(i);
});
}
else if (isEmbedded<T>(v)) {
this.state.emitbyte(Tag.Embedded);
this.embeddedEncode.encode(this.state, v);
}
else {
((v: T) => {
this.state.emitbyte(Tag.Embedded);
this.embeddedEncode.encode(this.state, v);
})(v);
encodeDictionaryOn(
new JsDictionaryMap(v),
this,
(k, e) => e.push(k),
(v, e) => e.push(v));
}
return this; // for chaining
}

View File

@ -1,10 +1,11 @@
import { Record, Tuple } from "./record";
import { Bytes } from "./bytes";
import { Value } from "./values";
import { Set, Dictionary } from "./dictionary";
import { Set, KeyedDictionary, Dictionary } from "./dictionary";
import { JsDictionary, JsDictionaryMap } from "./jsdictionary";
import { annotate, Annotated } from "./annotated";
import { Double, Float } from "./float";
import { Embeddable } from "./embedded";
import { Embeddable, isEmbedded } from "./embedded";
export enum ValueClass {
Boolean,
@ -34,7 +35,8 @@ export interface FoldMethods<T extends Embeddable, R> {
record(r: Record<Value<T>, Tuple<Value<T>>, T>, k: Fold<T, R>): R;
array(a: Array<Value<T>>, k: Fold<T, R>): R;
set(s: Set<T>, k: Fold<T, R>): R;
dictionary(d: Dictionary<T>, k: Fold<T, R>): R;
jsDictionary(d: JsDictionary<Value<T>>, k: Fold<T, R>): R;
keyedDictionary(d: KeyedDictionary<T>, k: Fold<T, R>): R;
annotated(a: Annotated<T>, k: Fold<T, R>): R;
@ -54,7 +56,10 @@ export class VoidFold<T extends Embeddable> implements FoldMethods<T, void> {
}
array(a: Value<T>[], k: Fold<T, void>): void { a.forEach(k); }
set(s: Set<T>, k: Fold<T, void>): void { s.forEach(k); }
dictionary(d: Dictionary<T>, k: Fold<T, void>): void {
jsDictionary(d: JsDictionary<Value<T>>, k: Fold<T, void>): void {
new JsDictionaryMap(d).forEach((value, key) => { k(key); k(value); });
}
keyedDictionary(d: KeyedDictionary<T>, k: Fold<T, void>): void {
d.forEach((value, key) => { k(key); k(value); });
}
annotated(a: Annotated<T>, k: Fold<T, void>): void { k(a.item); a.annotations.forEach(k); }
@ -94,8 +99,15 @@ export abstract class ValueFold<T extends Embeddable, R extends Embeddable = T>
set(s: Set<T>, k: Fold<T, Value<R>>): Value<R> {
return s.map(k);
}
dictionary(d: Dictionary<T>, k: Fold<T, Value<R>>): Value<R> {
return d.mapEntries(([key, value]) => [k(key), k(value)]);
jsDictionary(d: JsDictionary<Value<T>>, k: Fold<T, Value<R>>): Value<R> {
const results: [Value<R>, Value<R>][] = [];
new JsDictionaryMap(d).forEach((value, key) => results.push([k(key), k(value)]));
return Dictionary.from(results);
}
keyedDictionary(d: KeyedDictionary<T>, k: Fold<T, Value<R>>): Value<R> {
const r = new KeyedDictionary<R>();
Dictionary.mapInto(d, r, ([key, value]) => [k(key), k(value)]);
return r;
}
annotated(a: Annotated<T>, k: Fold<T, Value<R>>): Value<R> {
return annotate(k(a.item), ...a.annotations.map(k));
@ -188,16 +200,18 @@ export function fold<T extends Embeddable, R>(v: Value<T>, o: FoldMethods<T, R>)
return o.array(v, walk);
} else if (Set.isSet<T>(v)) {
return o.set(v, walk);
} else if (Dictionary.isDictionary<T>(v)) {
return o.dictionary(v, walk);
} else if (isEmbedded(v)) {
return o.embedded(v, walk);
} else if (Annotated.isAnnotated<T>(v)) {
return o.annotated(v, walk);
} else if (Bytes.isBytes(v)) {
return o.bytes(v);
} else if (Float.isDouble(v)) {
return o.double(v.value);
} else if (KeyedDictionary.isKeyedDictionary<T>(v)) {
return o.keyedDictionary(v, walk);
} else {
return ((_v: T) => o.embedded(v, walk))(v);
return o.jsDictionary(v, walk);
}
default:
((_v: never): never => { throw new Error("Internal error"); })(v);

View File

@ -1,8 +1,9 @@
import { Embeddable, GenericEmbedded } from "./embedded";
import { Embeddable, GenericEmbedded, isEmbedded } from "./embedded";
import { Bytes } from "./bytes";
import { Record, Tuple } from "./record";
import { Value } from "./values";
import { Dictionary, Set } from "./dictionary";
import { Dictionary, KeyedDictionary, Set } from "./dictionary";
import { JsDictionary } from "./jsdictionary";
export function fromJS<T extends Embeddable = GenericEmbedded>(x: any): Value<T> {
switch (typeof x) {
@ -39,7 +40,7 @@ export function fromJS<T extends Embeddable = GenericEmbedded>(x: any): Value<T>
return Bytes.from(x);
}
if (Map.isMap(x)) {
const d = new Dictionary<T>();
const d = new KeyedDictionary<T>();
x.forEach((v, k) => d.set(fromJS(k), fromJS(v)));
return d;
}
@ -48,8 +49,15 @@ export function fromJS<T extends Embeddable = GenericEmbedded>(x: any): Value<T>
x.forEach(v => s.add(fromJS(v)));
return s;
}
// Just... assume it's a T.
return x as T;
if (isEmbedded<T>(x)) {
return x;
}
// Handle plain JS objects to build a JsDictionary
{
const r: JsDictionary<Value<T>> = {};
Object.entries(x).forEach(([k, v]) => r[k] = fromJS(v));
return r;
}
default:
break;
@ -60,17 +68,16 @@ export function fromJS<T extends Embeddable = GenericEmbedded>(x: any): Value<T>
declare module "./dictionary" {
namespace Dictionary {
export function fromJS<T extends Embeddable = GenericEmbedded, V extends Embeddable = GenericEmbedded>(
export function stringMap<T extends Embeddable = GenericEmbedded>(
x: object
): Dictionary<T, Value<V>>;
): KeyedDictionary<T, string, Value<T>>;
}
}
Dictionary.fromJS = function <T extends Embeddable = GenericEmbedded, V extends Embeddable = GenericEmbedded>(
Dictionary.stringMap = function <T extends Embeddable = GenericEmbedded>(
x: object
): Dictionary<T, Value<V>> {
if (Dictionary.isDictionary<T, Value<V>>(x)) return x;
const d = new Dictionary<T, Value<V>>();
Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value)));
return d;
): KeyedDictionary<T, string, Value<T>> {
const r = new KeyedDictionary<T, string, Value<T>>();
Object.entries(x).forEach(([key, value]) => r.set(key, fromJS(value)));
return r;
};

View File

@ -1,5 +1,6 @@
import type { Embeddable, GenericEmbedded } from "./embedded";
import type { Annotated } from "./annotated";
import { Dictionary } from "./dictionary";
export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated');
@ -30,6 +31,17 @@ export function is(a: any, b: any): boolean {
for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false;
return true;
}
{
const aMap = Dictionary.asMap(a);
const bMap = Dictionary.asMap(b);
if (!aMap || !bMap) return false;
if (aMap.size !== bMap.size) return false;
for (const k of aMap.keys()) {
if (!bMap.has(k)) return false;
if (!is(aMap.get(k), bMap.get(k))) return false;
}
return true;
}
}
return false;
}

View File

@ -0,0 +1,89 @@
import { Equivalence, IsMap, _iterMap } from './flex';
import { Dictionary, DictionaryType } from './dictionary';
export interface JsDictionary<V> {
[key: string]: V;
}
export class JsDictionaryMap<V> implements Map<symbol, V> {
_size: number | undefined = void 0;
constructor(public readonly j: JsDictionary<V>) {}
get [IsMap](): boolean { return true; }
clear(): void {
for (const key in this.j) delete this.j[key];
this._size = void 0;
}
delete(key: symbol): boolean {
const result = this.has(key);
delete this.j[key.description!];
if (result) this._size = void 0;
return result;
}
forEach(
callbackfn: (value: V, key: symbol, map: Map<symbol, V>) => void,
thisArg?: any,
): void {
Object.entries(this.j).forEach(([key, val]) =>
callbackfn.call(thisArg, val, Symbol.for(key), this));
}
get(key: symbol): V | undefined {
return this.j[key.description!];
}
has(key: symbol): boolean {
return Object.hasOwnProperty.call(this.j, key.description!);
}
set(key: symbol, value: V): this {
this.j[key.description!] = value;
this._size = void 0;
return this;
}
get size(): number {
if (this._size === void 0) {
this._size = Object.keys(this.j).length;
}
return this._size;
}
entries(): IterableIterator<[symbol, V]> {
return _iterMap(Object.entries(this.j).values(), ([k, v]) => [Symbol.for(k), v]);
}
keys(): IterableIterator<symbol> {
return _iterMap(Object.keys(this.j).values(), k => Symbol.for(k));
}
values(): IterableIterator<V> {
return Object.values(this.j).values();
}
[Symbol.iterator](): IterableIterator<[symbol, V]> {
return this.entries();
}
get [Symbol.toStringTag]() { return 'JsDictionaryMap'; }
equals(other: any, eqv: Equivalence<V> = (v1, v2) => v1 === v2): boolean {
if (!('size' in other && 'has' in other && 'get' in other)) return false;
if (this.size !== other.size) return false;
for (let [k, v] of Object.entries(this.j)) {
if (!other.has(k)) return false;
if (!eqv(v, other.get(k))) return false;
}
return true;
}
}
export namespace JsDictionary {
export function isJsDictionary<V>(x: any): x is JsDictionary<V> {
return Dictionary.isDictionary(x) && (x as any)[DictionaryType] === void 0;
}
}

View File

@ -3,12 +3,13 @@ import { Bytes } from "./bytes";
import { fold } from "./fold";
import { is } from "./is";
import { Value } from "./values";
import { Set, Dictionary } from "./dictionary";
import { Set, Dictionary, KeyedDictionary } from "./dictionary";
import { Annotated } from "./annotated";
import { unannotate } from "./strip";
import { isEmbedded } from "./embedded";
import { isCompound } from "./compound";
import type { Embeddable } from "./embedded";
import { JsDictionary, JsDictionaryMap } from "./jsdictionary";
export function merge<T extends Embeddable>(
mergeEmbeddeds: (a: T, b: T) => T | undefined,
@ -48,17 +49,15 @@ export function merge<T extends Embeddable>(
return walkMany(a, b);
},
set(_s: Set<T>) { die(); },
dictionary(d: Dictionary<T>) {
if (!Dictionary.isDictionary<T>(b)) die();
const r = new Dictionary<T>();
d.forEach((av,ak) => {
const bv = b.get(ak);
r.set(ak, bv === void 0 ? av : walk(av, bv));
});
b.forEach((bv, bk) => {
if (!d.has(bk)) r.set(bk, bv);
});
return r;
jsDictionary(a: JsDictionary<Value<T>>) {
const bMap = Dictionary.asMap<T>(b);
if (bMap === void 0) die();
return walkMaps(new JsDictionaryMap(a), bMap);
},
keyedDictionary(a: KeyedDictionary<T>) {
const bMap = Dictionary.asMap<T>(b);
if (bMap === void 0) die();
return walkMaps(a, bMap);
},
annotated(a: Annotated<T>) {
@ -82,5 +81,17 @@ export function merge<T extends Embeddable>(
}
}
function walkMaps(a: Map<Value<T>, Value<T>>, b: Map<Value<T>, Value<T>>): Dictionary<T> {
const r = new KeyedDictionary<T>();
a.forEach((av,ak) => {
const bv = b.get(ak);
r.set(ak, bv === void 0 ? av : walk(av, bv));
});
b.forEach((bv, bk) => {
if (!a.has(bk)) r.set(bk, bv);
});
return Dictionary.from(r);
}
return items.reduce(walk, item0);
}

View File

@ -2,12 +2,12 @@
import { Annotated } from './annotated';
import { Bytes } from './bytes';
import { Set, Dictionary } from './dictionary';
import { Set, EncodableDictionary } from './dictionary';
import { stringify } from './text';
import * as util from 'util';
[Bytes, Annotated, Set, Dictionary].forEach((C) => {
[Bytes, Annotated, Set, EncodableDictionary].forEach((C) => {
(C as any).prototype[util.inspect.custom] =
function (_depth: any, _options: any) {
return stringify(this, { indent: 2 });

View File

@ -89,8 +89,8 @@ export function compare<T extends Embeddable>(
return cmp(va, vb);
}
case 10: {
const va = Array.from(a as Dictionary<T>).sort(cmp);
const vb = Array.from(b as Dictionary<T>).sort(cmp);
const va = Array.from(Dictionary.asMap<T>(a)!.entries()).sort(cmp);
const vb = Array.from(Dictionary.asMap<T>(b)!.entries()).sort(cmp);
return cmp(va, vb);
}
case 11:

View File

@ -398,22 +398,24 @@ export class Reader<T extends Embeddable> {
}
readDictionary(): Dictionary<T> {
return this.seq(true,
new Dictionary<T>(),
(k, acc) => {
this.state.skipws();
switch (this.state.peek()) {
case ':':
if (acc.has(k)) this.state.error(
`Duplicate key: ${stringify(k)}`, this.state.pos);
this.state.advance();
acc.set(k, this.next());
break;
default:
this.state.error('Missing key/value separator', this.state.pos);
}
},
'}');
const r = Dictionary.from<T, [Value<T>, Value<T>, Position]>(
this.seq(true, [] as [Value<T>, Value<T>, Position][], (k, acc) => {
this.state.skipws();
switch (this.state.peek()) {
case ':':
this.state.advance();
acc.push([k, this.next(), this.state.copyPos()]);
break;
default:
this.state.error('Missing key/value separator', this.state.pos);
}
}, '}'),
true as const);
if ('duplicate' in r) {
this.state.error(`Duplicate key: ${stringify(r.duplicate[0])}`, r.duplicate[2]);
} else {
return r.ok;
}
}
readSet(): Set<T> {

View File

@ -13,6 +13,7 @@ export * from './float';
export * from './fold';
export * from './fromjs';
export * from './is';
export * from './jsdictionary';
export * from './merge';
export * from './order';
export * from './reader';

View File

@ -1,7 +1,8 @@
import { Value } from "./values";
import { Annotated } from "./annotated";
import { Record, Tuple } from "./record";
import { Set, Dictionary } from "./dictionary";
import { Set, Dictionary, KeyedDictionary } from "./dictionary";
import { JsDictionary, JsDictionaryMap } from "./jsdictionary";
import type { Embeddable, GenericEmbedded } from "./embedded";
export function unannotate<T extends Embeddable = GenericEmbedded>(v: Value<T>): Value<T> {
@ -33,8 +34,12 @@ export function strip<T extends Embeddable = GenericEmbedded>(
return (v.item as Value<T>[]).map(walk);
} else if (Set.isSet<T>(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 (KeyedDictionary.isKeyedDictionary<T>(v.item)) {
const result = new KeyedDictionary<T>();
return Dictionary.mapInto(v.item, result, (e) => [walk(e[0]), walk(e[1])]);
} else if (JsDictionary.isJsDictionary<T>(v.item)) {
const i = new JsDictionaryMap(v.item);
return Dictionary.mapInto(i, new JsDictionaryMap<T>({}), (e) => [e[0], walk(e[1])]).j;
} else {
return v.item;
}

View File

@ -3,7 +3,8 @@
import type { Bytes } from './bytes';
import type { DoubleFloat } from './float';
import type { Annotated } from './annotated';
import type { Set, Dictionary } from './dictionary';
import type { JsDictionary } from './jsdictionary';
import { Set, KeyedDictionary } from './dictionary';
import type { Embeddable, GenericEmbedded } from './embedded';
export type Value<T extends Embeddable = GenericEmbedded> =
@ -27,4 +28,7 @@ export type Compound<T extends Embeddable = GenericEmbedded> =
// Value<T> to any.
| Array<Value<T>>
| Set<T>
| Dictionary<T>;
// v Expanded from definition of Dictionary<> in dictionary.ts,
// because of circular-use-of-Value<T> issues.
| JsDictionary<Value<T>>
| KeyedDictionary<T>;

View File

@ -1,10 +1,12 @@
import { isAnnotated } from './is';
import { Record, Tuple } from "./record";
import type { Embeddable, GenericEmbedded, EmbeddedTypeEncode } from "./embedded";
import { Embeddable, GenericEmbedded, EmbeddedTypeEncode, isEmbedded } from "./embedded";
import { Encoder, EncoderState } from "./encoder";
import type { Value } from "./values";
import { NUMBER_RE } from './reader';
import { encodeBase64 } from './base64';
import { writeDictionaryOn } from './dictionary';
import { JsDictionaryMap } from './jsdictionary';
export type Writable<T extends Embeddable> =
Value<T> | PreserveWritable<T> | Iterable<Value<T>> | ArrayBufferView;
@ -298,16 +300,19 @@ export class Writer<T extends Embeddable> {
else if (isIterable(v)) {
this.state.writeSeq('[', ']', v, vv => this.push(vv));
}
else {
((v: T) => {
this.state.pieces.push('#:');
if ('write' in this.embeddedWrite) {
this.embeddedWrite.write(this.state, v);
} else {
new Writer(this.state, genericEmbeddedTypeEncode)
.push(this.embeddedWrite.toValue(v));
}
})(v);
else if (isEmbedded(v)) {
this.state.pieces.push('#:');
if ('write' in this.embeddedWrite) {
this.embeddedWrite.write(this.state, v);
} else {
new Writer(this.state, genericEmbeddedTypeEncode)
.push(this.embeddedWrite.toValue(v));
}
} else {
writeDictionaryOn(new JsDictionaryMap(v),
this,
(k, w) => w.push(k),
(v, w) => w.push(v));
}
break;
default:

View File

@ -18,6 +18,8 @@ import {
genericEmbeddedTypeDecode,
genericEmbeddedTypeEncode,
parse,
Embedded,
KeyedDictionary,
} from '../src/index';
const { Tag } = Constants;
import './test-utils';
@ -75,7 +77,7 @@ describe('parsing from subarray', () => {
describe('reusing buffer space', () => {
it('should be done safely, even with nested dictionaries', () => {
expect(canonicalEncode(fromJS(['aaa', Dictionary.fromJS({a: 1}), 'zzz'])).toHex()).is(
expect(canonicalEncode(fromJS(['aaa', Dictionary.stringMap({a: 1}), 'zzz'])).toHex()).is(
`b5
b103616161
b7
@ -87,33 +89,29 @@ describe('reusing buffer space', () => {
});
describe('encoding and decoding embeddeds', () => {
class LookasideEmbeddedType implements EmbeddedType<object> {
readonly objects: object[];
class LookasideEmbeddedType implements EmbeddedType<Embedded<object>> {
readonly objects: Embedded<object>[];
constructor(objects: object[]) {
constructor(objects: Embedded<object>[]) {
this.objects = objects;
}
decode(d: DecoderState): object {
decode(d: DecoderState): Embedded<object> {
return this.fromValue(new Decoder<GenericEmbedded>(d).next());
}
encode(e: EncoderState, v: object): void {
encode(e: EncoderState, v: Embedded<object>): void {
new Encoder(e).push(this.toValue(v));
}
equals(a: object, b: object): boolean {
return Object.is(a, b);
}
fromValue(v: Value<GenericEmbedded>): object {
fromValue(v: Value<GenericEmbedded>): Embedded<object> {
if (typeof v !== 'number' || v < 0 || v >= this.objects.length) {
throw new Error("Unknown embedded target");
throw new Error(`Unknown embedded target: ${stringify(v)}`);
}
return this.objects[v];
}
toValue(v: object): number {
toValue(v: Embedded<object>): number {
let i = this.objects.indexOf(v);
if (i !== -1) return i;
this.objects.push(v);
@ -122,8 +120,8 @@ describe('encoding and decoding embeddeds', () => {
}
it('should encode using embeddedId when no function has been supplied', () => {
const A1 = {a: 1};
const A2 = {a: 1};
const A1 = new Embedded({a: 1});
const A2 = new Embedded({a: 1});
const bs1 = canonicalEncode(A1);
const bs2 = canonicalEncode(A2);
const bs3 = canonicalEncode(A1);
@ -140,10 +138,10 @@ describe('encoding and decoding embeddeds', () => {
.toThrow("Embeddeds not permitted at this point in Preserves document");
});
it('should encode properly', () => {
const objects: object[] = [];
const objects: Embedded<object>[] = [];
const pt = new LookasideEmbeddedType(objects);
const A = {a: 1};
const B = {b: 2};
const A = new Embedded({a: 1});
const B = new Embedded({b: 2});
expect(encode([A, B], { embeddedEncode: pt })).is(
Bytes.from([Tag.Sequence,
Tag.Embedded, Tag.SignedInteger, 0,
@ -152,10 +150,10 @@ describe('encoding and decoding embeddeds', () => {
expect(objects).toEqual([A, B]);
});
it('should decode properly', () => {
const objects: object[] = [];
const objects: Embedded<object>[] = [];
const pt = new LookasideEmbeddedType(objects);
const X = {x: 123};
const Y = {y: 456};
const X = new Embedded({x: 123});
const Y = new Embedded({y: 456});
objects.push(X);
objects.push(Y);
expect(decode(Bytes.from([
@ -166,17 +164,17 @@ describe('encoding and decoding embeddeds', () => {
]), { embeddedDecode: pt })).is([X, Y]);
});
it('should store embeddeds embedded in map keys correctly', () => {
const A1a = {a: 1};
const A1a = new Embedded({a: 1});
const A1 = A1a;
const A2 = {a: 1};
const m = new Dictionary<object, number>();
const A2 = new Embedded({a: 1});
const m = new KeyedDictionary<Embedded<object>, Value<Embedded<object>>, number>();
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();
A1a.a = 3;
A1a.value.a = 3;
expect(m.get([A1])).toBe(1);
});
});
@ -306,7 +304,7 @@ describe('common test suite', () => {
const tests = (peel(TestCases._.cases(peel(samples) as TestCases)) as
Dictionary<GenericEmbedded>);
tests.forEach((t0: Value<GenericEmbedded>, tName0: Value<GenericEmbedded>) => {
Dictionary.asMap(tests).forEach((t0, tName0) => {
const tName = Symbol.keyFor(strip(tName0) as symbol)!;
const t = peel(t0) as Record<symbol, any, GenericEmbedded>;
switch (t.label) {

View File

@ -1,4 +1,4 @@
import { Bytes, Decoder, genericEmbeddedType, encode, Reader, Double } from '../src/index';
import { Bytes, Decoder, genericEmbeddedType, encode, Reader, Double, KeyedDictionary, Value } from '../src/index';
import './test-utils';
import * as fs from 'fs';
@ -36,4 +36,23 @@ describe('reading common test suite', () => {
expect(new Reader('123.0').next()).toEqual(Double(123.0));
expect(new Reader('123.00').next()).toEqual(Double(123.0));
});
it('should produce a sensible JS object for symbol-keyed dictionaries', () => {
expect(new Reader('{a: 1, b: 2}').next()).toEqual({a: 1, b: 2});
});
it('should produce a sensible dictionary for mixed-keyed dictionaries', () => {
expect(new Reader('{a: 1, "b": 2}').next()).is(
new KeyedDictionary([[Symbol.for('a'), 1], ["b", 2]] as [Value, Value][]));
});
it('should produce a sensible dictionary for string-keyed dictionaries', () => {
expect(new Reader('{"a": 1, "b": 2}').next()).is(
new KeyedDictionary([["a", 1], ["b", 2]] as [Value, Value][]));
});
it('should produce a sensible dictionary for integer-keyed dictionaries', () => {
expect(new Reader('{9: 1, 8: 2}').next()).is(
new KeyedDictionary([[9, 1], [8, 2]] as [Value, Value][]));
});
});

View File

@ -1,4 +1,4 @@
import { Double, fromJS, Dictionary, IDENTITY_FOLD, fold, mapEmbeddeds, Value, preserves } from '../src/index';
import { Double, fromJS, IDENTITY_FOLD, fold, mapEmbeddeds, Value, preserves, KeyedDictionary, Embeddable, Embedded } from '../src/index';
import './test-utils';
describe('Double', () => {
@ -8,13 +8,13 @@ describe('Double', () => {
});
describe('fold', () => {
function mkv<T extends object>(t: T): Value<T> {
function mkv<T extends Embeddable>(t: T): Value<T> {
return fromJS<T>([
1,
2,
new Dictionary([[[3, 4], fromJS([5, 6])],
['a', 1],
['b', true]]),
new KeyedDictionary<T>([[[3, 4], fromJS([5, 6])],
['a', 1],
['b', true]]),
Double(3.4),
t,
]);
@ -22,12 +22,12 @@ describe('fold', () => {
it('should support identity', () => {
const w = new Date();
const v = mkv(w);
const v = mkv(new Embedded(w));
expect(fold(v, IDENTITY_FOLD)).is(v);
const w1 = new Date();
const v1 = mkv(w1);
const v1 = mkv(new Embedded(w1));
expect(fold(v, IDENTITY_FOLD)).not.is(v1);
expect(mapEmbeddeds(v, _t => w1)).is(v1);
expect(mapEmbeddeds(v, _t => new Embedded(w1))).is(v1);
});
});
@ -69,6 +69,12 @@ describe('is()', () => {
expect(c).not.toBe(3);
expect(c).not.is(3);
});
it('should compare equivalent JsDictionary and KeyedDictionary values sensibly', () => {
const a = {a: 1, b: 2};
const b = new KeyedDictionary(
[[Symbol.for('a'), 1], [Symbol.for('b'), 2]] as [Value, Value][]);
expect(a).is(b);
});
});
describe('`preserves` formatter', () => {
@ -82,4 +88,18 @@ describe('`preserves` formatter', () => {
expect(preserves`>${BigInt("12345678123456781234567812345678")}<`)
.toBe('>12345678123456781234567812345678<');
});
it('should format regular JS objects', () => {
expect(preserves`>${({a: 1, b: 2})}<`)
.toBe('>{a: 1 b: 2}<');
});
it('should format dictionaries with string keys', () => {
const v = new KeyedDictionary([["a", 1], ["b", 2]]);
expect(preserves`>${v}<`)
.toBe('>{"a": 1 "b": 2}<');
});
it('should format dictionaries with symbol keys', () => {
const v = new KeyedDictionary([[Symbol.for("a"), 1], [Symbol.for("b"), 2]]);
expect(preserves`>${v}<`)
.toBe('>{a: 1 b: 2}<');
});
});