mirror of
https://github.com/betagouv/mon-entreprise
synced 2025-02-09 02:55:01 +00:00
* Définition à partir du nom complet en notation pointée (plutôt que comme deux attributs indépendants "name" et "espace") * Structure de données de premier niveau "dictionnaire" plutôt que liste, s'aligne mieux avec notre contrainte d'unicité des noms * Possibilité de définir les règles à partir d'une liste dans les tests, dans ce cas il ne faut plus utiliser l'attribut "espace" mais renseigner directement la notation pointée dans le "nom".
273 lines
7.3 KiB
JavaScript
273 lines
7.3 KiB
JavaScript
import { parseUnit } from 'Engine/units'
|
|
import {
|
|
assoc,
|
|
chain,
|
|
dropLast,
|
|
find,
|
|
fromPairs,
|
|
has,
|
|
is,
|
|
isNil,
|
|
join,
|
|
last,
|
|
map,
|
|
path,
|
|
pipe,
|
|
propEq,
|
|
props,
|
|
range,
|
|
reduce,
|
|
reduced,
|
|
reject,
|
|
split,
|
|
take,
|
|
toPairs,
|
|
trim,
|
|
when
|
|
} from 'ramda'
|
|
import rawRules from 'Règles/base.yaml'
|
|
import translations from 'Règles/externalized.yaml'
|
|
// TODO - should be in UI, not engine
|
|
import { capitalise0, coerceArray } from '../utils'
|
|
import possibleVariableTypes from './possibleVariableTypes.yaml'
|
|
|
|
/***********************************
|
|
Functions working on one rule */
|
|
|
|
export let enrichRule = rule => {
|
|
try {
|
|
let unit = rule.unité && parseUnit(rule.unité)
|
|
const dottedName = rule.dottedName || rule.nom
|
|
const name = nameLeaf(dottedName)
|
|
return {
|
|
...rule,
|
|
dottedName,
|
|
name,
|
|
type: possibleVariableTypes.find(t => has(t, rule) || rule.type === t),
|
|
title: capitalise0(rule['titre'] || name),
|
|
defaultValue: rule['par défaut'],
|
|
examples: rule['exemples'],
|
|
icons: rule['icônes'],
|
|
summary: rule['résumé'],
|
|
unit
|
|
}
|
|
} catch (e) {
|
|
console.log(e)
|
|
throw new Error('Problem enriching ' + JSON.stringify(rule))
|
|
}
|
|
}
|
|
|
|
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
|
|
// comme dans sa formule
|
|
export let disambiguateExampleSituation = (rules, rule) =>
|
|
pipe(
|
|
toPairs,
|
|
map(([k, v]) => [disambiguateRuleReference(rules, rule, k), v]),
|
|
fromPairs
|
|
)
|
|
|
|
export let hasKnownRuleType = rule => rule && enrichRule(rule).type
|
|
|
|
export let splitName = split(' . '),
|
|
joinName = join(' . ')
|
|
|
|
export let parentName = pipe(
|
|
splitName,
|
|
dropLast(1),
|
|
joinName
|
|
)
|
|
export let nameLeaf = pipe(
|
|
splitName,
|
|
last
|
|
)
|
|
|
|
export let encodeRuleName = name =>
|
|
encodeURI(
|
|
name
|
|
.replace(/\s\.\s/g, '/')
|
|
.replace(/-/g, '\u2011') // replace with a insecable tiret to differenciate from space
|
|
.replace(/\s/g, '-')
|
|
)
|
|
export let decodeRuleName = name =>
|
|
decodeURI(
|
|
name
|
|
.replace(/\//g, ' . ')
|
|
.replace(/-/g, ' ')
|
|
.replace(/\u2011/g, '-')
|
|
)
|
|
|
|
export let ruleParents = dottedName => {
|
|
let fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture]
|
|
return range(1, fragments.length)
|
|
.map(nbEl => take(nbEl)(fragments))
|
|
.reverse() // -> [ [CDD . événements . rupture], [CDD . événements], [CDD] ]
|
|
}
|
|
/* Les variables peuvent être exprimées dans la formule d'une règle relativement à son propre espace de nom, pour une plus grande lisibilité. Cette fonction résoud cette ambiguité.
|
|
*/
|
|
export let disambiguateRuleReference = (
|
|
allRules,
|
|
{ dottedName, name },
|
|
partialName
|
|
) => {
|
|
let pathPossibilities = [
|
|
[], // the top level namespace
|
|
...ruleParents(dottedName), // the parents namespace
|
|
splitName(dottedName) // the rule's own namespace
|
|
],
|
|
found = reduce(
|
|
(res, path) => {
|
|
let dottedNameToCheck = [...path, partialName].join(' . ')
|
|
return when(is(Object), reduced)(
|
|
findRuleByDottedName(allRules, dottedNameToCheck)
|
|
)
|
|
},
|
|
null,
|
|
pathPossibilities
|
|
)
|
|
|
|
if (found?.dottedName) {
|
|
return found.dottedName
|
|
}
|
|
|
|
throw new Error(
|
|
`OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`
|
|
)
|
|
}
|
|
|
|
export let collectDefaults = pipe(
|
|
map(props(['dottedName', 'defaultValue'])),
|
|
reject(([, v]) => v === undefined),
|
|
fromPairs
|
|
)
|
|
|
|
/****************************************
|
|
Méthodes de recherche d'une règle */
|
|
|
|
export let findRuleByName = (allRules, query) =>
|
|
(Array.isArray(allRules) ? allRules : Object.values(allRules)).find(
|
|
({ name }) => name === query
|
|
)
|
|
|
|
export let findRulesByName = (allRules, query) =>
|
|
(Array.isArray(allRules) ? allRules : Object.values(allRules)).filter(
|
|
({ name }) => name === query
|
|
)
|
|
|
|
export let findRuleByDottedName = (allRules, dottedName) =>
|
|
Array.isArray(allRules)
|
|
? allRules.find(rule => rule.dottedName == dottedName)
|
|
: allRules[dottedName]
|
|
|
|
export let findRule = (rules, nameOrDottedName) =>
|
|
nameOrDottedName.includes(' . ')
|
|
? findRuleByDottedName(rules, nameOrDottedName)
|
|
: findRuleByName(rules, nameOrDottedName)
|
|
|
|
export let findRuleByNamespace = (allRules, ns) =>
|
|
allRules.filter(rule => parentName(rule.dottedName) === ns)
|
|
|
|
/*********************************
|
|
Autres */
|
|
|
|
export let queryRule = rule => query => path(query.split(' . '))(rule)
|
|
|
|
export let nestedSituationToPathMap = situation => {
|
|
if (situation == undefined) return {}
|
|
let rec = (o, currentPath) =>
|
|
typeof o === 'object'
|
|
? chain(([k, v]) => rec(v, [...currentPath, trim(k)]), toPairs(o))
|
|
: [[currentPath.join(' . '), o + '']]
|
|
|
|
return fromPairs(rec(situation, []))
|
|
}
|
|
|
|
/* Traduction */
|
|
|
|
export let translateAll = (translations, flatRules) => {
|
|
let translationsOf = rule => translations[rule.dottedName],
|
|
translateProp = (lang, translation) => (rule, prop) => {
|
|
let propTrans = translation[prop + '.' + lang]
|
|
if (prop === 'suggestions' && propTrans)
|
|
return assoc(
|
|
'suggestions',
|
|
pipe(
|
|
toPairs,
|
|
map(([key, translatedKey]) => [
|
|
translatedKey,
|
|
rule.suggestions[key]
|
|
]),
|
|
fromPairs
|
|
)(propTrans),
|
|
rule
|
|
)
|
|
return propTrans ? assoc(prop, propTrans, rule) : rule
|
|
},
|
|
translateRule = (lang, translations, props) => rule => {
|
|
let ruleTrans = translationsOf(rule)
|
|
return ruleTrans
|
|
? reduce(translateProp(lang, ruleTrans), rule, props)
|
|
: rule
|
|
}
|
|
|
|
let targets = [
|
|
'titre',
|
|
'description',
|
|
'question',
|
|
'résumé',
|
|
'suggestions',
|
|
'contrôles'
|
|
]
|
|
|
|
return map(translateRule('en', translations, targets), flatRules)
|
|
}
|
|
|
|
const rulesList = Object.entries(rawRules).map(([dottedName, rule]) => ({
|
|
dottedName,
|
|
...rule
|
|
}))
|
|
|
|
// On enrichit la base de règles avec des propriétés dérivées de celles du YAML
|
|
export let rules = translateAll(translations, rulesList).map(rule =>
|
|
enrichRule(rule)
|
|
)
|
|
|
|
export let rulesFr = rulesList.map(rule => enrichRule(rule))
|
|
|
|
export let findParentDependency = (rules, rule) => {
|
|
// A parent dependency means that one of a rule's parents is not just a namespace holder, it is a boolean question. E.g. is it a fixed-term contract, yes / no
|
|
// When it is resolved to false, then the whole branch under it is disactivated (non applicable)
|
|
// It lets those children omit obvious and repetitive parent applicability tests
|
|
let parentDependencies = ruleParents(rule.dottedName).map(joinName)
|
|
return pipe(
|
|
map(parent => findRuleByDottedName(rules, parent)),
|
|
reject(isNil),
|
|
find(
|
|
//Find the first "calculable" parent
|
|
({ question, unit, formule }) =>
|
|
(question && !unit && !formule) ||
|
|
(question && formule?.['une possibilité'] !== undefined) ||
|
|
(typeof formule === 'string' && formule.includes(' = ')) //implicitly, the format is boolean
|
|
)
|
|
)(parentDependencies)
|
|
}
|
|
|
|
export let getRuleFromAnalysis = analysis => dottedName => {
|
|
if (!analysis) {
|
|
throw new Error("[getRuleFromAnalysis] The analysis can't be nil !")
|
|
}
|
|
let rule = coerceArray(analysis) // In some simulations, there are multiple "branches" : the analysis is run with e.g. 3 different input situations
|
|
.map(
|
|
analysis =>
|
|
analysis.cache[dottedName]?.explanation || // the cache stores a reference to a variable, the variable is contained in the 'explanation' attribute
|
|
analysis.targets.find(propEq('dottedName', dottedName))
|
|
)
|
|
.filter(Boolean)[0]
|
|
|
|
if (!rule) {
|
|
throw new Error(
|
|
`[getRuleFromAnalysis] Unable to find the rule ${dottedName}`
|
|
)
|
|
}
|
|
|
|
return rule
|
|
}
|