import parseRule from './parseRule' import yaml from 'yaml' import { compose, dissoc, lensPath, over, set } from 'ramda' import { compilationError } from './error' import { parseReference } from './parseReference' import { ParsedRules, Rules } from './types' export default function parseRules( rawRules: Rules | string ): ParsedRules { const rules = typeof rawRules === 'string' ? (yaml.parse(rawRules.replace(/\t/g, ' ')) as Rules) : { ...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. */ const 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` */ const nonApplicableMapping: Record = {} const replacedByMapping: Record = {} ;(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, undefined)(a) ) }) }) Object.entries(replacedByMapping).forEach(([a, b]) => { parsedRules[a].replacedBy = b.map(({ referenceName, ...other }) => ({ referenceNode: parseReference( rules, parsedRules[referenceName], parsedRules, undefined )(referenceName), ...other })) }) return parsedRules as ParsedRules } // 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>) { const extractNamesInRule = (dottedName: string) => { rules[dottedName] !== null && Object.entries(rules[dottedName]).forEach( extractNamesInObject(dottedName) ) } const extractNamesInObject = ( dottedName: string, context: Array = [] ) => ([key, value]: [string, Record]) => { const match = /\[ref( (.+))?\]$/.exec(key) 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, // The `virtualRule` parameter is used to avoid creating a // dedicated documentation page. virtualRule: true } rules[dottedName] = compose( over(lensPath(context), dissoc(key)) as any, set(lensPath([...context, argumentType]), extractedReferenceName) )(rules[dottedName]) as any extractNamesInRule(extractedReferenceName) } else if (Array.isArray(value)) { value.forEach((content: Record, 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) }