Simplify Engine constructor

* remove ParsedRules argument to constructor (was undocumented, so no
  harm for Publicodes users)
* replace by Engine.shallowCopy()
* simplify EngineContext in mon-entreprise.
pull/1472/head
Alexandre Hajjar 2021-02-16 14:30:42 +01:00
parent edb999ee85
commit abba62fcf3
11 changed files with 59 additions and 63 deletions

View File

@ -2,27 +2,28 @@
Ce paquet contient les règles publicodes utilisées sur https://mon-entreprise.fr
pour le calcul des cotisations sociales, des impôts et des droits sociaux.
### Installation
```
npm install publicodes modele-social
npm install publicodes modele-social
```
### Exemple d'utilisation
```js
import Engine, { formatValue } from "publicodes";
import rules from "modele-social";
const engine = new Engine(rules);
```js
import Engine, { formatValue } from 'publicodes'
import rules from 'modele-social'
const engine = new Engine(rules)
const net = engine
.setSituation({
"contrat salarié . rémunération . brut de base": "3000 €/mois",
})
.evaluate("contrat salarié . rémunération . net");
console.log(formatValue(net));
.setSituation({
'contrat salarié . rémunération . brut de base': '3000 €/mois',
})
.evaluate('contrat salarié . rémunération . net')
console.log(formatValue(net))
```
👉 **[Voir le tutoriel complet](https://mon-entreprise.fr/int%C3%A9gration/biblioth%C3%A8que-de-calcul)**

View File

@ -4,14 +4,13 @@ import Header from 'Components/layout/Header'
import Route404 from 'Components/Route404'
import 'Components/ui/index.css'
import {
engineOptions,
engineFactory,
EngineProvider,
Rules,
SituationProvider,
} from 'Components/utils/EngineContext'
import { SitePathsContext } from 'Components/utils/SitePathsContext'
import 'iframe-resizer'
import { DottedName } from 'modele-social'
import Engine, { Rule } from 'publicodes'
import { useContext, useMemo } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
@ -70,16 +69,13 @@ const middlewares = [createSentryMiddleware(Sentry as any)]
type RootProps = {
basename: ProviderProps['basename']
rules: Record<DottedName, Rule>
rules: Rules
}
export default function Root({ basename, rules }: RootProps) {
const { language } = useTranslation().i18n
const paths = constructLocalizedSitePath(language as 'fr' | 'en')
const engine = useMemo(() => new Engine(rules, engineOptions), [
rules,
engineOptions,
])
const engine = useMemo(() => engineFactory(rules), [rules])
return (
<Provider
basename={basename}

View File

@ -9,8 +9,6 @@ import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import Value from 'Components/EngineValue'
import InfoBulle from 'Components/ui/InfoBulle'
import revenusSVG from 'Images/revenus.svg'
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { useCallback, useMemo, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
@ -18,7 +16,7 @@ import { useSelector } from 'react-redux'
import { situationSelector } from 'Selectors/simulationSelectors'
import dirigeantComparaison from '../pages/Simulateurs/configs/rémunération-dirigeant.yaml'
import './SchemeComparaison.css'
import { engineOptions, useEngine } from './utils/EngineContext'
import { useEngine } from './utils/EngineContext'
import useSimulationConfig from './utils/useSimulationConfig'
type SchemeComparaisonProps = {
@ -45,13 +43,12 @@ export default function SchemeComparaison({
setConversationStarted,
])
const parsedRules = engine.getParsedRules()
const situation = useSelector(situationSelector)
const displayResult =
useSelector(situationSelector)['entreprise . charges'] != undefined
const assimiléEngine = useMemo(
() =>
new Engine<DottedName>(parsedRules, engineOptions).setSituation({
engine.shallowCopy().setSituation({
...situation,
dirigeant: "'assimilé salarié'",
}),
@ -59,7 +56,7 @@ export default function SchemeComparaison({
)
const autoEntrepreneurEngine = useMemo(
() =>
new Engine<DottedName>(parsedRules, engineOptions).setSituation({
engine.shallowCopy().setSituation({
...situation,
dirigeant: "'auto-entrepreneur'",
}),
@ -67,7 +64,7 @@ export default function SchemeComparaison({
)
const indépendantEngine = useMemo(
() =>
new Engine<DottedName>(parsedRules, engineOptions).setSituation({
engine.shallowCopy().setSituation({
...situation,
dirigeant: "'indépendant'",
}),

View File

@ -1,14 +1,12 @@
import Engine from 'publicodes'
import Engine, { Rule } from 'publicodes'
import React, { createContext, useContext } from 'react'
import { DottedName } from 'modele-social'
import i18n from '../../locales/i18n'
export const EngineContext = createContext<Engine>(new Engine({}))
export const EngineProvider = EngineContext.Provider
export type Rules = Record<DottedName, Rule>
const unitsTranslations = Object.entries(i18n.getResourceBundle('fr', 'units'))
export const engineOptions = {
const engineOptions = {
getUnitKey(unit: string): string {
const key = unitsTranslations
.find(([, trans]) => trans === unit)?.[0]
@ -19,6 +17,12 @@ export const engineOptions = {
return i18n?.t(`units:${unit}`, { count })
},
}
export function engineFactory(rules: Rules) {
return new Engine(rules, engineOptions)
}
export const EngineContext = createContext<Engine>(new Engine())
export const EngineProvider = EngineContext.Provider
export function useEngine(): Engine<DottedName> {
return useContext(EngineContext) as Engine<DottedName>

View File

@ -10,7 +10,7 @@ import { useSimulationProgress } from 'Components/utils/useNextQuestion'
import { useParamsFromSituation } from 'Components/utils/useSearchParamsSimulationSharing'
import useSimulationConfig from 'Components/utils/useSimulationConfig'
import { DottedName } from 'modele-social'
import Engine, { formatValue } from 'publicodes'
import { formatValue } from 'publicodes'
import { partition } from 'ramda'
import { useContext } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@ -229,7 +229,7 @@ function Results() {
const progress = useSimulationProgress()
const baseEngine = useEngine()
const aidesEngines = aides.map((aide) => {
const engine = new Engine(baseEngine.parsedRules)
const engine = baseEngine.shallowCopy()
engine.setSituation({ ...aide.situation, ...baseEngine.parsedSituation })
const isActive =
typeof engine.evaluate(aide.dottedName).nodeValue === 'number'

View File

@ -6,7 +6,7 @@ import rules from 'modele-social'
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
let parsedRules = parsePublicodes(rules)
const engine = new Engine(parsedRules)
const engine = new Engine(rules)
let runExamples = (examples, rule) =>
examples.map((ex) => {
const expected = ex['valeur attendue']

View File

@ -6,9 +6,8 @@
// renamed the test configuration may be adapted but the persisted snapshot will remain unchanged).
/* eslint-disable no-undef */
import Engine from 'publicodes'
import rules from '../../../modele-social'
import { engineOptions } from '../../source/components/utils/EngineContext'
import { engineFactory } from '../../source/components/utils/EngineContext'
import aideDéclarationConfig from '../../source/pages/Gérer/AideDéclarationIndépendant/config.yaml'
import artisteAuteurConfig from '../../source/pages/Simulateurs/configs/artiste-auteur.yaml'
import autoentrepreneurConfig from '../../source/pages/Simulateurs/configs/auto-entrepreneur.yaml'
@ -26,7 +25,7 @@ import remunerationDirigeantSituations from './simulations-rémunération-dirige
import employeeSituations from './simulations-salarié.yaml'
const roundResult = (arr) => arr.map((x) => Math.round(x))
const engine = new Engine(rules, engineOptions)
const engine = engineFactory(rules)
const runSimulations = (situations, targets, baseSituation = {}) =>
Object.entries(situations).map(([name, situations]) =>
situations.forEach((situation) => {

View File

@ -26,14 +26,13 @@ describe('identifiant court', () => {
})
describe('useSearchParamsSimulationSharing', () => {
const someRules = parsePublicodes(`
const engine = new Engine(`
rule with:
identifiant court: panta
formule: 0
rule without:
formule: 0
`)
const engine = new Engine(someRules)
const dottedNameParamName = getRulesParamNames(engine.getParsedRules())
describe('getSearchParamsFromSituation', () => {
@ -92,7 +91,7 @@ rule without:
})
describe('useSearchParamsSimulationSharing hook', () => {
const someRules = parsePublicodes(`
const parsedRules = parsePublicodes(`
rule with:
identifiant court: panta
formule: 0
@ -100,9 +99,7 @@ rule without:
formule: 0
`)
const dottedNameParamName = getRulesParamNames(
new Engine(someRules).getParsedRules()
)
const dottedNameParamName = getRulesParamNames(parsedRules)
let setSearchParams
beforeEach(() => {

View File

@ -67,6 +67,7 @@ export type ParsedRules<Name extends string> = Record<
Name,
RuleNode & { dottedName: Name }
>
export default class Engine<Name extends string = string> {
parsedRules: ParsedRules<Name>
parsedSituation: Record<string, ASTNode> = {}
@ -75,25 +76,11 @@ export default class Engine<Name extends string = string> {
options: Options
constructor(
rules: string | Record<string, Rule> | ParsedRules<Name> = {},
rules: string | Record<string, Rule> = {},
options: Partial<Options> = {}
) {
this.options = { ...options, logger: options.logger ?? console }
if (typeof rules === 'string') {
rules = parsePublicodes(rules, this.options) as ParsedRules<Name>
}
const firstRuleObject = Object.values(rules)[0] as Rule | RuleNode
if (
typeof firstRuleObject !== 'object' ||
firstRuleObject == null ||
!('nodeKind' in firstRuleObject)
) {
rules = parsePublicodes(
rules as Record<string, Rule>,
this.options
) as ParsedRules<Name>
}
this.parsedRules = rules as ParsedRules<Name>
this.parsedRules = parsePublicodes(rules, this.options) as ParsedRules<Name>
this.replacements = getReplacements(this.parsedRules)
}
@ -150,6 +137,10 @@ export default class Engine<Name extends string = string> {
return this.parsedRules
}
getOptions(): Options {
return this.options
}
evaluate<N extends ASTNode = ASTNode>(value: N): N & EvaluatedNode
evaluate(value: PublicodesExpression): EvaluatedNode
evaluate(value: PublicodesExpression | ASTNode): EvaluatedNode {
@ -180,6 +171,17 @@ export default class Engine<Name extends string = string> {
this.cache.nodes.set(value, evaluatedNode)
return evaluatedNode
}
/**
* Shallow Engine instance copy. Keeps references to the original Engine instance attributes.
*/
shallowCopy(): Engine<Name> {
const newEngine = new Engine<Name>()
newEngine.options = this.options
newEngine.parsedRules = this.parsedRules
newEngine.replacements = this.replacements
return newEngine
}
}
/**

View File

@ -51,7 +51,7 @@ describe('inversions', () => {
assiette: brut
taux: 77%
brut:
brut:
formule:
inversion numérique:
unité:
@ -147,7 +147,7 @@ describe('inversions', () => {
formule:
produit:
assiette: assiette
taux:
taux:
variations:
- si: cadre
alors: 80%

View File

@ -64,7 +64,7 @@ export const NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => {
}}
>
{formatValue(simplifyNodeUnit({ nodeValue: data, unit }), {
formatUnit: engine?.options?.formatUnit,
formatUnit: engine?.getOptions()?.formatUnit,
})}
</small>
)