mon-entreprise/source/engine/parseRules.ts

123 lines
3.9 KiB
TypeScript

import parseRule from 'Engine/parseRule'
import { safeLoad } from 'js-yaml'
import { lensPath, set } from 'ramda'
import { compilationError } from './error'
import { parseReference } from './parseReference'
import { ParsedRules, Rules } from './types'
export default function parseRules<Names extends string>(
rawRules: Rules<Names> | string
): ParsedRules<Names> {
const rules =
typeof rawRules === 'string'
? (safeLoad(rawRules.replace(/\t/g, ' ')) as Rules<Names>)
: { ...rawRules }
extractInlinedNames(rules)
/* First we parse each rule one by one. When a mechanism is encountered, it is
recursively parsed. When a reference to a variable is encountered, a
'variable' node is created, we don't parse variables recursively. */
let parsedRules = {}
/* A rule `A` can disable a rule `B` using the rule `rend non applicable: B`
in the definition of `A`. We need to map these exonerations to be able to
retreive them from `B` */
let nonApplicableMapping: Record<string, any> = {}
let replacedByMapping: Record<string, any> = {}
;(Object.keys(rules) as Names[]).map(dottedName => {
const parsedRule = parseRule(rules, dottedName, parsedRules)
if (parsedRule['rend non applicable']) {
nonApplicableMapping[parsedRule.dottedName] =
parsedRule['rend non applicable']
}
const replaceDescriptors = parsedRule['remplace']
if (replaceDescriptors) {
replaceDescriptors.forEach(
descriptor =>
(replacedByMapping[descriptor.referenceName] = [
...(replacedByMapping[descriptor.referenceName] ?? []),
{ ...descriptor, referenceName: parsedRule.dottedName }
])
)
}
})
Object.entries(nonApplicableMapping).forEach(([a, b]) => {
b.forEach(ruleName => {
parsedRules[ruleName].isDisabledBy.push(
parseReference(rules, parsedRules[ruleName], parsedRules)(a)
)
})
})
Object.entries(replacedByMapping).forEach(([a, b]) => {
parsedRules[a].replacedBy = b.map(({ referenceName, ...other }) => ({
referenceNode: parseReference(
rules,
parsedRules[referenceName],
parsedRules
)(referenceName),
...other
}))
})
return parsedRules as ParsedRules<Names>
}
// We recursively traverse the YAML tree in order to extract named parameters
// into their own dedicated rules, and replace the inline definition with a
// reference to the newly created rule.
function extractInlinedNames(rules: Record<string, Object>) {
const extractNamesInRule = (dottedName: string) => {
rules[dottedName] !== null &&
Object.entries(rules[dottedName]).forEach(
extractNamesInObject(dottedName)
)
}
const extractNamesInObject = (
dottedName: string,
context: Array<string | number> = []
) => ([key, value]: [string, Object]) => {
const match = key.match(/\[ref( (.+))?\]$/)
if (match) {
const argumentType = key.replace(match[0], '').trim()
const argumentName = match[2]?.trim() || argumentType
const extractedReferenceName = `${dottedName} . ${argumentName}`
if (typeof rules[extractedReferenceName] !== 'undefined') {
compilationError(
dottedName,
`Le paramètre [ref] ${argumentName} entre en conflit avec la règle déjà existante ${extractedReferenceName}`
)
}
rules[extractedReferenceName] = {
formule: value,
// TODO: The `virtualRule` parameter should be used to avoid creating a
// dedicated documentation page.
virtualRule: true
}
rules[dottedName] = set(
lensPath([...context, argumentType]),
extractedReferenceName,
rules[dottedName]
)
extractNamesInRule(extractedReferenceName)
} else if (Array.isArray(value)) {
value.forEach((content: Object, i) =>
Object.entries(content).forEach(
extractNamesInObject(dottedName, [...context, key, i])
)
)
} else if (value && typeof value === 'object') {
Object.entries(value).forEach(
extractNamesInObject(dottedName, [...context, key])
)
}
}
Object.keys(rules).forEach(extractNamesInRule)
}