import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" export interface DesugaringContext { tagRenderings: Map sharedLayers: Map publicLayers?: Set } export class ConversionContext { /** * The path within the data structure where we are currently operating */ readonly path: ReadonlyArray /** * Some information about the current operation */ readonly operation: ReadonlyArray readonly messages: ConversionMessage[] private constructor( messages: ConversionMessage[], path: ReadonlyArray, operation?: ReadonlyArray ) { this.path = path this.operation = operation ?? [] // Messages is shared by reference amonst all 'context'-objects for performance this.messages = messages } public static construct(path: (string | number)[], operation: string[]) { return new ConversionContext([], [...path], [...operation]) } public static test(msg?: string) { return new ConversionContext([], msg ? [msg] : [], ["test"]) } static print(msg: ConversionMessage) { const noString = msg.context.path.filter( (p) => typeof p !== "string" && typeof p !== "number" ) if (noString.length > 0) { console.warn("Non-string value in path:", ...noString) } if (msg.level === "error") { console.error( ConversionContext.red("ERR "), msg.context.path.join("."), ConversionContext.red(msg.message), msg.context.operation.join(".") ) } else if (msg.level === "warning") { console.warn( ConversionContext.red(" "), msg.context.path.join("."), ConversionContext.yellow(msg.message), msg.context.operation.join(".") ) } else { console.log(" ", msg.context.path.join("."), msg.message) } } private static yellow(s) { return "\x1b[33m" + s + "\x1b[0m" } private static red(s) { return "\x1b[31m" + s + "\x1b[0m" } public enter(key: string | number | (string | number)[]) { if (!Array.isArray(key)) { return new ConversionContext(this.messages, [...this.path, key], this.operation) } return new ConversionContext(this.messages, [...this.path, ...key], this.operation) } public enters(...key: (string | number)[]) { return this.enter(key) } public inOperation(key: string) { return new ConversionContext(this.messages, this.path, [...this.operation, key]) } warn(message: string) { this.messages.push({ context: this, level: "warning", message }) } err(message: string) { this.messages.push({ context: this, level: "error", message }) } info(message: string) { this.messages.push({ context: this, level: "information", message }) } getAll(mode: ConversionMsgLevel): ConversionMessage[] { return this.messages.filter((m) => m.level === mode) } public hasErrors() { return this.messages?.find((m) => m.level === "error") !== undefined } } export type ConversionMsgLevel = "debug" | "information" | "warning" | "error" export interface ConversionMessage { context: ConversionContext message: string level: ConversionMsgLevel } export abstract class Conversion { public readonly modifiedAttributes: string[] public readonly name: string protected readonly doc: string constructor(doc: string, modifiedAttributes: string[] = [], name: string) { this.modifiedAttributes = modifiedAttributes this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ") this.name = name } public convertStrict(json: TIn, context?: ConversionContext): TOut { context ??= ConversionContext.construct([], []) context = context.inOperation(this.name) const fixed = this.convert(json, context) for (const msg of context.messages) { ConversionContext.print(msg) } if (context.hasErrors()) { throw "Detected one or more errors, stopping now" } return fixed } public andThenF(f: (tout: TOut) => X): Conversion { return new Pipe(this, new Pure(f)) } public abstract convert(json: TIn, context: ConversionContext): TOut } export abstract class DesugaringStep extends Conversion {} export class Pipe extends Conversion { private readonly _step0: Conversion private readonly _step1: Conversion constructor(step0: Conversion, step1: Conversion) { super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) this._step0 = step0 this._step1 = step1 } convert(json: TIn, context: ConversionContext): TOut { const r0 = this._step0.convert(json, context.inOperation(this._step0.name)) return this._step1.convert(r0, context.inOperation(this._step1.name)) } } export class Pure extends Conversion { private readonly _f: (t: TIn) => TOut constructor(f: (t: TIn) => TOut) { super("Wrapper around a pure function", [], "Pure") this._f = f } convert(json: TIn, context: ConversionContext): TOut { return this._f(json) } } export class Each extends Conversion { private readonly _step: Conversion constructor(step: Conversion) { super( "Applies the given step on every element of the list", [], "OnEach(" + step.name + ")" ) this._step = step } convert(values: X[], context: ConversionContext): Y[] { if (values === undefined || values === null) { return values } const step = this._step const result: Y[] = [] for (let i = 0; i < values.length; i++) { const context_ = context.enter(i).inOperation("each") const r = step.convert(values[i], context_) result.push(r) } return result } } export class On extends DesugaringStep { private readonly key: string private readonly step: (t: T) => Conversion constructor(key: string, step: Conversion | ((t: T) => Conversion)) { super( "Applies " + step.name + " onto property `" + key + "`", [key], `On(${key}, ${step.name})` ) if (typeof step === "function") { this.step = step } else { this.step = (_) => step } this.key = key } convert(json: T, context: ConversionContext): T { const key = this.key const value: P = json[key] if (value === undefined || value === null) { return json } json = { ...json } const step = this.step(json) json[key] = step.convert(value, context.enter(key).inOperation("on[" + key + "]")) return json } } export class Pass extends Conversion { constructor(message?: string) { super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass") } convert(json: T, context: ConversionContext): T { return json } } export class Concat extends Conversion { private readonly _step: Conversion constructor(step: Conversion) { super( "Executes the given step, flattens the resulting list", [], "Concat(" + step.name + ")" ) this._step = step } convert(values: X[], context: ConversionContext): T[] { if (values === undefined || values === null) { // Move on - nothing to see here! return values } const vals: T[][] = new Each(this._step).convert(values, context.inOperation("concat")) return [].concat(...vals) } } export class FirstOf extends Conversion { private readonly _conversion: Conversion constructor(conversion: Conversion) { super( "Picks the first result of the conversion step", [], "FirstOf(" + conversion.name + ")" ) this._conversion = conversion } convert(json: T, context: ConversionContext): X { const values = this._conversion.convert(json, context.inOperation("firstOf")) if (values.length === 0) { return undefined } return values[0] } } export class Fuse extends DesugaringStep { private readonly steps: DesugaringStep[] constructor(doc: string, ...steps: DesugaringStep[]) { super( (doc ?? "") + "This fused pipeline of the following steps: " + steps.map((s) => s.name).join(", "), Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))), "Fuse(" + steps.map((s) => s.name).join(", ") + ")" ) this.steps = Utils.NoNull(steps) } convert(json: T, context: ConversionContext): T { for (let i = 0; i < this.steps.length; i++) { const step = this.steps[i] try { const r = step.convert(json, context.inOperation(step.name)) if (r === undefined || r === null) { break } json = r } catch (e) { console.error("Step " + step.name + " failed due to ", e, e.stack) throw e } } return json } } export class SetDefault extends DesugaringStep { private readonly value: any private readonly key: string private readonly _overrideEmptyString: boolean constructor(key: string, value: any, overrideEmptyString = false) { super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key) this.key = key this.value = value this._overrideEmptyString = overrideEmptyString } convert(json: T, context: ConversionContext): T { if (json === undefined) { return undefined } if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { json = { ...json } json[this.key] = this.value } return json } }