mon-entreprise/source/engine/rules.js

264 lines
7.6 KiB
JavaScript
Raw Normal View History

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é);
return {
...rule,
type: possibleVariableTypes.find(t => has(t, rule) || rule.type === t),
name: rule["nom"],
title: capitalise0(rule["titre"] || rule["nom"]),
ns: rule["espace"],
dottedName: buildDottedName(rule),
subquestionMarkdown: rule["sous-question"],
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));
}
};
export let buildDottedName = rule =>
rule["espace"] ? [rule["espace"], rule["nom"]].join(" . ") : rule["nom"];
// 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(/\s/g, "-"));
export let decodeRuleName = name =>
decodeURI(name.replace(/\//g, " . ").replace(/-/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) =>
when(is(Object), reduced)(
do {
let dottedNameToCheck = [...path, partialName].join(" . ");
findRuleByDottedName(allRules, dottedNameToCheck);
}
),
null,
pathPossibilities
);
return (
(found && found.dottedName) ||
do {
throw new Error(
`OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`
);
}
);
};
2018-01-08 15:07:26 +00:00
export let collectDefaults = pipe(
map(props(["dottedName", "defaultValue"])),
reject(([, v]) => v === undefined),
fromPairs
);
/****************************************
Méthodes de recherche d'une règle */
2016-12-07 18:08:10 +00:00
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(propEq("ns", ns));
/*********************************
Autres */
export let queryRule = rule => query => path(query.split(" . "))(rule);
2018-02-06 16:02:13 +00:00
// Redux-form stores the form values as a nested object
// This helper makes a dottedName => value Map
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[buildDottedName(rule)],
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",
"sous-question",
"résumé",
"suggestions",
"contrôles"
];
return map(translateRule("en", translations, targets), flatRules);
};
// On enrichit la base de règles avec des propriétés dérivées de celles du YAML
export let rules = translateAll(translations, rawRules).map(rule =>
enrichRule(rule)
);
export let rulesFr = rawRules.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 //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;
};