Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 50s
417 lines
10 KiB
TypeScript
417 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()
|
|
}
|