Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 1m7s
382 lines
10 KiB
TypeScript
382 lines
10 KiB
TypeScript
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<Primitive | string[]> {
|
|
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<string, CanonicalValue>
|
|
private a?: CanonicalValue[]
|
|
|
|
constructor(
|
|
tags: string[],
|
|
named: Record<string, CanonicalValue>,
|
|
anonymous: CanonicalValue[],
|
|
) {
|
|
super(tags)
|
|
|
|
if (Object.keys(named).length) {
|
|
this.n = named
|
|
}
|
|
|
|
if (anonymous.length) {
|
|
this.a = anonymous
|
|
}
|
|
}
|
|
|
|
get named(): Readonly<Record<string, CanonicalValue>> {
|
|
return this.n ?? {}
|
|
}
|
|
|
|
get anonymous(): readonly CanonicalValue[] {
|
|
return this.a ?? []
|
|
}
|
|
|
|
static fromInputJson(outerTags: string[], json: JsonObject): CanonicalObject {
|
|
let n: Record<string, CanonicalValue> = {}
|
|
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<string, CanonicalValue> = {}
|
|
|
|
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()
|
|
}
|