mon-entreprise/publicodes/source/index.ts

162 lines
4.0 KiB
TypeScript

import { mapObjIndexed } from 'ramda'
import { evaluateControls } from './controls'
import { evaluationError, warning } from './error'
import { collectDefaults, evaluateNode } from './evaluation'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import { parse } from './parse'
import parseRules from './parseRules'
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
}
}
}
type Situation<Names extends string> = Partial<
Record<Names, object | string | number>
>
type EvaluatedSituation<Names extends string> = Partial<
Record<Names, object | number | EvaluatedNode<Names>>
>
export type EvaluationOptions = Partial<{
unit: string
}>
export * from './components'
export { formatValue } from './format'
export { default as translateRules } from './translateRules'
export { default as cyclesLib } from './cyclesLib/index'
export * from './types'
export { parseRules }
export default class Engine<Names extends string> {
parsedRules: ParsedRules<Names>
situation: Situation<Names> = {}
private cache: Cache
private warnings: Array<string> = []
constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
this.cache = emptyCache()
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
): EvaluatedNode<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(
evaluateNode(
this.cache,
this.situation,
this.parsedRules,
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.situation = mapObjIndexed(
(value, name) =>
typeof value === 'string'
? this.evaluateExpression(value, `[situation] ${name}`)
: value,
situation
) as EvaluatedSituation<Names>
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 = {
nodeValue: result.nodeValue,
missingVariables: result.missingVariables,
...('unit' in result && { unit: result.unit }),
...('temporalValue' in result && {
temporalValue: result.temporalValue
}),
...result.explanation
} 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
}
controls() {
return evaluateControls(this.cache, this.situation, this.parsedRules)
}
getWarnings() {
return this.warnings
}
inversionFail(): boolean {
return !!this.cache._meta.inversionFail
}
getParsedRules(): ParsedRules<Names> {
return this.parsedRules
}
}