/* eslint-disable @typescript-eslint/ban-types */ import { compose, mapObjIndexed } from 'ramda' import { ASTNode, EvaluatedNode, NodeKind } from './AST/types' import { evaluationFunctions } from './evaluationFunctions' import { simplifyNodeUnit } from './nodeUnits' import parse from './parse' import parsePublicodes, { disambiguateReference } from './parsePublicodes' import { getReplacements, inlineReplacements, ReplacementRule, } from './replacement' import { Rule, RuleNode } from './rule' import * as utils from './ruleUtils' import { reduceAST, transformAST } from './AST' const emptyCache = () => ({ _meta: { contextRule: [] }, }) type Cache = { _meta: { contextRule: Array parentEvaluationStack?: Array inversionFail?: | { given: string estimated: string } | true inRecalcul?: boolean filter?: string } } export type EvaluationOptions = Partial<{ unit: string }> export { reduceAST, transformAST } from './AST/index' export * as cyclesLib from './AST/graph' export { Evaluation, Unit } from './AST/types' export * from './components' export { formatValue } from './format' export { default as translateRules } from './translateRules' export { ASTNode, EvaluatedNode } export { parsePublicodes } export { utils } export { Rule } export type EvaluationFunction = ( this: Engine, node: ASTNode & { nodeKind: Kind } ) => ASTNode & { nodeKind: Kind } & EvaluatedNode export type ParsedRules = Record< Name, RuleNode & { dottedName: Name } > export default class Engine { parsedRules: ParsedRules parsedSituation: Record = {} replacements: Record> = {} cache: Cache // A number that is incremented every time the situation changes, and is used // for inline cache invalidation. situationVersion = 0 private warnings: Array = [] constructor(rules: string | Record | ParsedRules) { this.cache = emptyCache() this.resetCache() if (typeof rules === 'string') { this.parsedRules = parsePublicodes(rules) as ParsedRules } const firstRuleObject = Object.values(rules)[0] as Rule | RuleNode if ( typeof firstRuleObject === 'object' && firstRuleObject != null && 'nodeKind' in firstRuleObject ) { this.parsedRules = rules as ParsedRules return } this.parsedRules = parsePublicodes( rules as Record ) as ParsedRules this.replacements = getReplacements(this.parsedRules) } private resetCache() { this.cache = emptyCache() } setSituation( situation: Partial> = {} ) { this.resetCache() this.situationVersion++ this.parsedSituation = mapObjIndexed((value, key) => { if (value && typeof value === 'object' && 'nodeKind' in value) { return value as ASTNode } return compose( inlineReplacements(this.replacements), disambiguateReference(this.parsedRules) )( parse(value, { dottedName: `situation [${key}]`, parsedRules: {}, }) ) }, situation) return this } evaluate(expression: string | Object): EvaluatedNode { /* TODO EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker console.warn */ const originalWarn = console.warn console.warn = (warning: string) => { this.warnings.push(warning) originalWarn(warning) } const result = this.evaluateNode( compose( inlineReplacements(this.replacements), disambiguateReference(this.parsedRules) )( parse(expression, { dottedName: "evaluation'''", parsedRules: {}, }) ) ) console.warn = originalWarn return result } getWarnings() { return this.warnings } inversionFail(): boolean { return !!this.cache._meta.inversionFail } getParsedRules(): ParsedRules { return this.parsedRules } evaluateNode( node: N & { lastEvaluation?: number; res?: EvaluatedNode } ): N & EvaluatedNode { if (!node.nodeKind) { throw Error('The provided node must have a "nodeKind" attribute') } else if (!evaluationFunctions[node.nodeKind]) { throw Error(`Unknown "nodeKind": ${node.nodeKind}`) } if (!node.res || node.lastEvaluation !== this.situationVersion) { node.res = evaluationFunctions[node.nodeKind].call(this, node) node.lastEvaluation = this.situationVersion } return node.res! } } // This function is an util for allowing smother migration to the new Engine API export function evaluateRule( engine: Engine, dottedName: DottedName, modifiers: Object = {} ): EvaluatedRule { const evaluation = simplifyNodeUnit( engine.evaluate({ valeur: dottedName, ...modifiers }) ) const rule = engine.getParsedRules()[dottedName] as RuleNode & { dottedName: DottedName } // HACK while waiting for applicability to have its own type const isNotApplicable = reduceAST( function (isNotApplicable, node, fn) { if (isNotApplicable) return isNotApplicable if (!('nodeValue' in node)) { return isNotApplicable } if (node.nodeKind === 'variations') { return node.explanation.some( ({ consequence }) => fn(consequence) || ((consequence as any).nodeValue === false && (consequence as any).dottedName !== dottedName) ) } if (node.nodeKind === 'reference' && node.dottedName === dottedName) { return fn(engine.evaluateNode(rule)) } if (node.nodeKind === 'applicable si') { return (node.explanation.condition as any).nodeValue === false } if (node.nodeKind === 'non applicable si') { return ( (node.explanation.condition as any).nodeValue !== false && (node.explanation.condition as any).nodeValue !== null ) } }, false, evaluation ) return { isNotApplicable, ...rule.rawNode, ...rule, ...evaluation, } as EvaluatedRule } export type EvaluatedRule = EvaluatedNode & Omit< (ASTNode & { nodeKind: 'rule' }) & (ASTNode & { nodeKind: 'rule' })['rawNode'] & { dottedName: Name; isNotApplicable: boolean }, 'nodeKind' >