import { filter, find, get, isEqual, range, some, toPairs } from 'lodash-es' import { Interval } from 'ohm-js/index' import { RailID, Source } from '../result' export const META_PATH = '_meta' export type Attrs = Attr[] export type FieldType = 'scalar' | 'set' type FieldOptionals = { desc?: string, displayFn?: displayFn } type displayFn = (v: V) => string abstract class AbstractField { public readonly name: string public readonly path: string public readonly desc?: string readonly displayFn?: displayFn abstract readonly type: FieldType constructor(name: string, path: string, optionals: FieldOptionals = {}) { this.name = name this.path = path this.desc = optionals.desc this.displayFn = optionals.displayFn } // Create new `ValueDef` for this field value(value: V, desc?: string): ValueDef { return new ValueDef(this, value, desc) } // Compare this field with another (only checks field type) is(other: AbstractField): boolean { return this.type === other.type && this.path === other.path } // Finds the first instance of this field in a list of `Attr` find(attrs: Attr[]): Attr | undefined { return find(attrs, a => a.def.field.is(this)) } // Finds all instances of this field in a list of `Attr` findAll(attrs: Attr[]): Attrs { return filter(attrs, a => a.def.field.is(this)) } } export class Field extends AbstractField { type: FieldType = 'scalar' } export class SetField extends AbstractField { type: FieldType = 'set' } export class ValueDef { public readonly field: Field public readonly value: V public readonly desc?: string public readonly footnotes: string[] constructor(field: Field, value: V, desc?: string, footnotes?: string[]) { this.field = field this.value = value this.desc = desc this.footnotes = footnotes ?? [] } // Creates new instance of this def with the speficied footnotes notes(...footnotes: string[]): ValueDef { return new ValueDef(this.field, this.value, this.desc, [ ...footnotes, ...this.footnotes ]) } // Used to represent an instance of this value for a particular part of the code at(...intervals: Interval[]) { const source = intervals.map(iv => range(iv.startIdx, iv.endIdx)).flat() return this.atSource(source) } // Used to represent an instance of this value for a particular part of the code atSource(...source: Source[]) { return new Attr(this, source.flat(), this.footnotes) } // Used to represent a value which doesn't correspond to a specific part of the code. absent() { return new Attr(this, [], this.footnotes) } // Tests a result object for the presence of this field value matches(o: RailID): boolean { if (this.field.type === 'scalar') return isEqual(get(o, this.field.path, undefined), this.value) else if (this.field.type === 'set') return some(get(o, this.field.path, []), (val: V) => isEqual(val, this.value)) else return false } // Compares the value of this def with that of another def (ignores description and footnotes) compare(other: ValueDef) { return this.field.is(other.field) && isEqual(this.value, other.value) } // Generate a human display version of this fields value displayValue(): string { const vType = typeof this.value // If a literal then use `toString()` if (vType === 'string' || vType === 'number') return this.value.toString() // Use `Field`-provided function if one exists if (this.field.displayFn) return this.field.displayFn(this.value) // If an object (should be by this point) then make a decent key/value string if (vType === 'object') return toPairs(this.value as object).map(([k, v]) => `${k}=${v}`).join(', ') // Give up return '' } } export class Attr { public readonly def: ValueDef public readonly source: Source = [] public readonly footnotes: string[] = [] constructor(def: ValueDef, source: Source, footnotes: string[]) { this.def = def this.source = source this.footnotes = footnotes } compare(other: Attr) { return this.def.compare(other.def) } }