mon-entreprise/publicodes/source/index.ts

176 lines
4.2 KiB
TypeScript

/* eslint-disable @typescript-eslint/ban-types */
import { map } from 'ramda'
import { evaluationError, warning } from './error'
import { evaluationFunctions } from './evaluation'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import { parse } from './parse'
import parseRules from './parseRules'
import * as utils from './ruleUtils'
import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from './types'
import { parseUnit } from './units'
const emptyCache = () => ({
_meta: { contextRule: [] }
})
type Cache = {
_meta: {
contextRule: Array<string>
inversionFail?:
| {
given: string
estimated: string
}
| true
inRecalcul?: boolean
filter?: string
}
}
type ParsedSituation<Names extends string> = Partial<ParsedRules<Names>>
export type EvaluationOptions = Partial<{
unit: string
}>
export * from './components'
export { default as cyclesLib } from './cyclesLib/index'
export { formatValue, serializeValue } from './format'
export { default as translateRules } from './translateRules'
export * from './types'
export { parseRules }
export { utils }
export type evaluationFunction = (
this: Engine<string>,
node: EvaluatedNode
) => EvaluatedNode
export default class Engine<Names extends string> {
parsedRules: ParsedRules<Names>
parsedSituation: ParsedSituation<Names> = {}
cache: Cache
private warnings: Array<string> = []
constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
this.cache = emptyCache()
this.resetCache()
this.parsedRules =
typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName
? parseRules(rules)
: (rules as ParsedRules<Names>)
}
private resetCache() {
this.cache = emptyCache()
}
private evaluateExpression(
expression: string,
context: string
): EvaluatedRule<Names> {
// 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 = simplifyNodeUnit(
this.evaluateNode(
parse(
this.parsedRules,
{ dottedName: context },
this.parsedRules
)(expression)
)
)
console.warn = originalWarn
if (Object.keys(result.defaultValue?.missingVariable ?? {}).length) {
throw evaluationError(
context,
"Impossible d'évaluer l'expression car celle ci fait appel à des variables manquantes"
)
}
return result
}
setSituation(
situation: Partial<Record<Names, string | number | object>> = {}
) {
this.resetCache()
this.parsedSituation = map(
value =>
typeof value === 'object'
? value
: parse(
this.parsedRules,
{ dottedName: '' },
this.parsedRules
)(value),
situation
)
return this
}
evaluate(expression: Names, options?: EvaluationOptions): EvaluatedRule<Names>
evaluate(
expression: string,
options?: EvaluationOptions
): EvaluatedNode<Names> | EvaluatedRule<Names>
evaluate(expression: string, options?: EvaluationOptions) {
let result = this.evaluateExpression(
expression,
`[evaluation] ${expression}`
)
if (result.category === 'reference' && result.explanation) {
result = {
...result.explanation,
nodeValue: result.nodeValue,
missingVariables: result.missingVariables,
...('unit' in result && { unit: result.unit }),
...('temporalValue' in result && {
temporalValue: result.temporalValue
}),
dottedName: result.dottedName
} as EvaluatedRule<Names>
}
if (options?.unit) {
try {
return convertNodeToUnit(
parseUnit(options.unit),
result as EvaluatedNode<Names, number>
)
} catch (e) {
warning(
`[evaluation] ${expression}`,
"L'unité demandée est incompatible avec l'expression évaluée"
)
}
}
return result
}
getWarnings() {
return this.warnings
}
inversionFail(): boolean {
return !!this.cache._meta.inversionFail
}
getParsedRules(): ParsedRules<Names> {
return this.parsedRules
}
evaluateNode(node) {
if (!node.nodeKind) {
throw Error('The provided node must have a "nodeKind" attribute')
} else if (!evaluationFunctions[node.nodeKind]) {
throw Error(`Unknown "nodeKind": ${node.nodeKind}`)
}
return evaluationFunctions[node.nodeKind].call(this, node)
}
}