import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import { ConversionContext } from "./ConversionContext" export interface DesugaringContext { tagRenderings: Map /** * Order of appearance in questions.json */ tagRenderingOrder: string[] sharedLayers: Map publicLayers?: Set } export type ConversionMsgLevel = "debug" | "information" | "warning" | "error" export interface ConversionMessage { readonly context: ConversionContext readonly message: string readonly 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) let fixed: TOut try { fixed = this.convert(json, context) } catch (e) { console.error(e) context.err("ERROR WHILE RUNNING STEP " + this.name + ": " + e) fixed = undefined } for (const msg of context.messages) { if (msg.level === "debug") { continue } ConversionContext.print(msg) } if (context.hasErrors()) { throw new Error( [ "Detected one or more errors, stopping now:", context.getAll("error").map((e) => e.context.path.join(".") + ": " + e.message), ].join("\n\t") ) } 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 private readonly _failfast: boolean constructor(step0: Conversion, step1: Conversion, failfast = false) { super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) this._step0 = step0 this._step1 = step1 this._failfast = failfast } convert(json: TIn, context: ConversionContext): TOut { const r0 = this._step0.convert(json, context.inOperation(this._step0.name)) if (context.hasErrors() && this._failfast) { return undefined } 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 Bypass extends DesugaringStep { private readonly _applyIf: (t: T) => boolean private readonly _step: DesugaringStep constructor(applyIf: (t: T) => boolean, step: DesugaringStep) { super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass") this._applyIf = applyIf this._step = step } convert(json: T, context: ConversionContext): T { if (!this._applyIf(json)) { return json } return this._step.convert(json, context) } } export class Each extends Conversion { private readonly _step: Conversion private readonly _msg: string constructor(step: Conversion, options?: { msg?: string }) { super( "Applies the given step on every element of the list", [], "OnEach(" + step.name + ")" ) this._step = step this._msg = options?.msg } convert(values: X[], context: ConversionContext): Y[] { if (values === undefined || values === null) { return values } const step = this._step const result: Y[] = [] const c = context.inOperation("each") for (let i = 0; i < values.length; i++) { if (this._msg) { console.log( this._msg, `: ${i + 1}/${values.length}`, values[i]?.["id"] !== undefined ? values[i]?.["id"] : "" ) } const r = step.convert(values[i], c.enter(i)) 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, _: 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 Cached extends Conversion { private _step: Conversion private readonly key: string constructor(step: Conversion) { super("Secretly caches the output for the given input", [], "cached") this._step = step this.key = "__super_secret_caching_key_" + step.name } convert(json: TIn, context: ConversionContext): TOut { if (json[this.key]) { return json[this.key] } const converted = this._step.convert(json, context) Object.defineProperty(json, this.key, { value: converted, enumerable: false, }) return converted } } export class Fuse extends DesugaringStep { protected debug = false 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) } public enableDebugging(): Fuse { this.debug = true return this } convert(json: T, context: ConversionContext): T { const timings = [] for (let i = 0; i < this.steps.length; i++) { const start = new Date() 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 } if (this.debug) { const stop = new Date() const timeNeededMs = stop.getTime() - start.getTime() timings.push(timeNeededMs) } } if (this.debug) { console.log("Time needed,", timings.join(", ")) } 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, _: 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 } }