export type Primitive = string | number | boolean | null export type JsonObject = { [key: string]: JsonValue } export type JsonArray = JsonValue[] export type JsonValue = Primitive | JsonObject | JsonArray export type CanonicalValue = CanonicalPrimitive | CanonicalObject | CanonicalArray // We do not use Map or Set as properties, they cant be serialized to JSON properly // Instead we use objects and arrays under the hood, and optimize speed/memory for runtime, not build time // TODO: In fromCanonicalJson the .x are not set (not critical actually) export class CanonicalBase { protected readonly x: number = 0 private readonly t?: string[] constructor(tags: string[]) { const clean = cleanTags(tags) this.t = clean.length ? clean : undefined } get tags(): readonly string[] { return this.t ?? [] } get delete(): boolean { return this.t?.includes('delete') ?? false } get replace(): boolean { return this.t?.includes('replace') ?? false } static fromCanonicalJson(json: JsonObject): CanonicalBase { const base = Object.create(CanonicalBase.prototype) base.t = json.t return base as CanonicalBase } mergeInto(other: CanonicalBase): CanonicalBase { throw new Error('Not implemented') } dropTags(except: string[]): CanonicalBase { throw new Error('Not implemented') } reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any { throw new Error('Not implemented') } } export class CanonicalPrimitive extends CanonicalBase { protected override readonly x: number = 1 private v: Primitive | string[] private e?: boolean get value(): Readonly { return this.v } get expand(): boolean { return this.e ?? false } constructor(tags: string[], value: Primitive | string[], expand?: boolean) { super(tags) this.v = value this.e = expand } static fromInputJson(outerTags: string[], primitive: Primitive): CanonicalPrimitive { let v: Primitive | string[] let e: boolean | undefined = undefined if (typeof primitive === 'string') { const segments = primitive.split(/\$\{(.*?)\}/) if (segments.length > 1) { v = segments e = true } else { v = primitive } } else { v = primitive } return new CanonicalPrimitive(outerTags, v, e) } static override fromCanonicalJson(json: JsonObject): CanonicalPrimitive { const primitive = super.fromCanonicalJson(json) as CanonicalPrimitive Object.setPrototypeOf(primitive, CanonicalPrimitive.prototype) primitive.v = json.v as Primitive | string[] primitive.e = json.e as boolean | undefined return primitive } override mergeInto(other: CanonicalPrimitive): CanonicalPrimitive { return this } override dropTags(except: string[]): CanonicalPrimitive { return new CanonicalPrimitive(this.tags.filter(t => except.includes(t)), this.v, this.e) } override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): CanonicalPrimitive | Primitive | undefined { if (!options.predicate(this.tags)) { return undefined } if (this.expand) { options.expand = true return this } return this.v as Primitive } resolve(resolver: (key: string) => JsonValue): string { return (this.v as string[]).map((p, i) => i % 2 ? resolver(p) : p).join('') } } export class CanonicalObject extends CanonicalBase { protected override readonly x: number = 2 private n?: Record private a?: CanonicalValue[] constructor( tags: string[], named: Record, anonymous: CanonicalValue[], ) { super(tags) if (Object.keys(named).length) { this.n = named } if (anonymous.length) { this.a = anonymous } } get named(): Readonly> { return this.n ?? {} } get anonymous(): readonly CanonicalValue[] { return this.a ?? [] } static fromInputJson(outerTags: string[], json: JsonObject): CanonicalObject { let n: Record = {} let a: CanonicalValue[] = [] for (const key in json) { if (key === '$tags') continue let [name, ...tags] = key.split('$') tags = cleanTags(tags) if (name) { n[name] = fromInputJson(tags, json[key] as JsonValue) } else if (tags.length) { a.push(fromInputJson(tags, json[key] as JsonValue)) } else { console.error(`No name and no tags found in key '${key}'`) } } return new CanonicalObject([...outerTags, ...getInnerTags(json)], n, a) } static override fromCanonicalJson(json: JsonObject): CanonicalObject { const object = super.fromCanonicalJson(json) as CanonicalObject Object.setPrototypeOf(object, CanonicalObject.prototype) if (json.n) { object.n = {} for (const [k, v] of Object.entries(json.n)) { object.n[k] = fromCanonicalJson(v) } } if (json.a) { object.a = (json.a as JsonValue[]).map(i => fromCanonicalJson(i)) } return object } override mergeInto(other: CanonicalObject): CanonicalObject { const n: Record = {} for (const key of new Set([...Object.keys(this.named), ...Object.keys(other.named)])) { let merged: CanonicalValue | undefined = undefined if (key in this.named && key in other.named) { merged = mergeInto(this.named[key]!, other.named[key]!) } else { merged = key in this.named ? this.named[key] : other.named[key] } if (merged) { n[key] = merged } } return new CanonicalObject([...this.tags], n, [...this.anonymous]) } override dropTags(except: string[]): CanonicalObject { const n = Object.fromEntries(Object.entries(this.named).map(([k, v]) => [k, v.dropTags(except)])) const a = this.anonymous.map(a => a.dropTags(except)) return new CanonicalObject(this.tags.filter(t => except.includes(t)), n, a) } override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any | undefined { if (!options.predicate(this.tags)) { return undefined } const n = Object.fromEntries(Object.entries(this.named).map(([k, v]) => [k, v.reduce(options)]).filter(([k, v]) => v !== undefined)) if (Object.keys(n).length) { return n } const a = this.anonymous.map(a => a.reduce(options)).filter(a => a !== undefined) if (a.length) { if (a.length > 1) { console.error(`Multiple anonymous object properties: ${JSON.stringify(a)}`) } return a[0] } return undefined } } export class CanonicalArray extends CanonicalBase { protected override readonly x: number = 3 private i?: CanonicalValue[] constructor(tags: string[], items: CanonicalValue[]) { super(tags) if (items.length) { this.i = items } } get items(): readonly CanonicalValue[] { return this.i ?? [] } static fromInputJson(outerTags: string[], json: JsonArray): CanonicalArray { const tags = [...outerTags] json = json.filter(item => { if (typeof item === 'object' && item !== null && Object.keys(item).length === 1 && '$tags' in item) { tags.push(...getInnerTags(item)) return false } return true }) return new CanonicalArray(tags, json.map(i => fromInputJson([], i))) } static override fromCanonicalJson(json: JsonObject): CanonicalArray { const array = super.fromCanonicalJson(json) as CanonicalArray Object.setPrototypeOf(array, CanonicalArray.prototype) if (json.i) { array.i = (json.i as JsonValue[]).map(i => fromCanonicalJson(i)) } return array } override mergeInto(other: CanonicalArray): CanonicalArray { console.error("Merging arrays not yet implemented") return this // TODO } override dropTags(except: string[]): CanonicalArray { const items = (this.i ?? []).map(i => i.dropTags(except)) return new CanonicalArray(this.tags.filter(t => except.includes(t)), items) } override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any | undefined { if (!options.predicate(this.tags)) { return undefined } return this.items.map(i => i.reduce(options)).filter(i => i !== undefined) } } export function fromInputJson(outerTags: string[], json: JsonValue): CanonicalValue { if (Array.isArray(json)) { return CanonicalArray.fromInputJson(outerTags, json) } else if (typeof json === 'object' && json !== null) { return CanonicalObject.fromInputJson(outerTags, json) } return CanonicalPrimitive.fromInputJson(outerTags, json) } export function fromCanonicalJson(json: JsonValue): CanonicalValue { if (typeof json === 'object' && json !== null) { json = json as JsonObject if (json.x === 1) { return CanonicalPrimitive.fromCanonicalJson(json) } else if (json.x == 2) { return CanonicalObject.fromCanonicalJson(json) } return CanonicalArray.fromCanonicalJson(json) } throw new Error(`Not a valid canonical object: ${json}`) } function mergeInto(self: CanonicalValue, other: CanonicalValue): CanonicalValue | undefined { if (self instanceof CanonicalBase) { if (self.delete) { return undefined } if (self.replace) { return self } } if (self instanceof CanonicalPrimitive && other instanceof CanonicalPrimitive || self instanceof CanonicalObject && other instanceof CanonicalObject || self instanceof CanonicalArray && other instanceof CanonicalArray) { return self.mergeInto(other) } return self } export function merge(items: CanonicalValue[]): CanonicalValue | undefined { if (!items.length) { return undefined } else if (items.length === 1) { return items[0] } return items.reduceRight((acc, cur) => mergeInto(cur, acc)) } function getInnerTags(json: JsonObject): string[] { const tags = json['$tags'] if (typeof tags === 'string') { return cleanTags(tags.split(' ')) } return [] } function cleanTags(tags: string[]): string[] { return [...new Set(tags)].map(t => t.trim()).filter(t => !!t).sort() }