Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit 5e76846

Browse files
authored
Merge pull request #43 from publicodes/migration-refactoring
refactor!: simplification of the migrateSituation code
2 parents c717888 + af8380e commit 5e76846

16 files changed

+322
-515
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"license": "MIT",
6060
"dependencies": {
6161
"@types/node": "^18.11.18",
62-
"publicodes": "^1.1.1"
62+
"publicodes": "^1.3.0"
6363
},
6464
"devDependencies": {
6565
"@types/jest": "^29.2.5",

src/commons.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { basename } from 'path'
2-
import { Rule, Logger, ExprAST, reduceAST, ASTNode } from 'publicodes'
2+
import {
3+
Rule,
4+
Logger,
5+
ExprAST,
6+
reduceAST,
7+
ASTNode,
8+
Evaluation,
9+
} from 'publicodes'
310
import yaml from 'yaml'
411

512
/**
@@ -233,3 +240,22 @@ Avec :
233240
${yaml.stringify(secondDef, { indent: 2 })}`,
234241
)
235242
}
243+
244+
/**
245+
* Unquote a string value.
246+
*
247+
* @param value - The value to parse.
248+
*
249+
* @returns The value without quotes if it is a string, null otherwise.
250+
*/
251+
export function getValueWithoutQuotes(value: Evaluation) {
252+
if (
253+
typeof value !== 'string' ||
254+
!value.startsWith("'") ||
255+
value === 'oui' ||
256+
value === 'non'
257+
) {
258+
return null
259+
}
260+
return value.slice(1, -1)
261+
}

src/migration/index.ts

+32-30
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
11
/** @packageDocumentation
22
3-
## Migrate a situation
3+
## Situation migration
44
5-
{@link migrateSituation | `migrateSituation`} allows to migrate situation and foldedSteps based on migration instructions. It's useful in forms when a model is updated and we want old answers to be kept and taken into account in the new model.
5+
### Why?
66
7-
### Usage
8-
9-
For instance, we have a simple set of rules:
10-
11-
```yaml
12-
age:
13-
question: "Quel est votre âge ?"
14-
````
7+
In time, the `publicodes` models evolve. When a model is updated (e.g. a rule
8+
is renamed, a value is changed, a new rule is added, etc.), we want to ensure
9+
that the previous situations (i.e. answers to questions) are still valid.
1510
16-
and the following situation:
17-
```json
18-
{
19-
age: 25
20-
}
21-
```
11+
This is where the sitation migration comes in.
2212
23-
If I change my model because I want to fix the accent to:
13+
### Usage
2414
25-
```yaml
26-
âge:
27-
question: "Quel est votre âge ?"
28-
```
15+
{@link migrateSituation | `migrateSituation`} allows to migrate a situation from
16+
an old version of a model to a new version according to the provided _migration
17+
instructions_.
2918
30-
I don't want to lose the previous answer, so I can use `migrateSituation` with the following migration instructions:
3119
32-
```yaml
33-
keysToMigrate:
34-
age: âge
35-
```
20+
```typescript
21+
import { migrateSituation } from '@publicodes/tools/migration'
3622
37-
Then, calling `migrateSituation` with the situation and the migration instructions will return:
23+
const situation = {
24+
"age": 25,
25+
"job": "developer",
26+
"city": "Paris"
27+
}
3828
39-
```json
40-
{
41-
âge: 25
29+
const instructions = {
30+
keysToMigrate: {
31+
// The rule `age` has been renamed to `âge`.
32+
age: 'âge',
33+
// The rule `city` has been removed.
34+
city: ''
35+
},
36+
valuesToMigrate: {
37+
job: {
38+
// The value `developer` has been translated to `développeur`.
39+
developer: 'développeur'
40+
}
41+
}
4242
}
43+
44+
migrateSituation(situation, instructions) // { "âge": 25, "job": "'développeur'" }
4345
```
4446
*/
4547

src/migration/migrateSituation.ts

+139-91
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,161 @@
1-
import { getValueWithoutQuotes } from './migrateSituation/getValueWithoutQuotes'
2-
import { handleSituationKeysMigration } from './migrateSituation/handleSituationKeysMigration'
3-
import { handleSituationValuesMigration } from './migrateSituation/handleSituationValuesMigration'
4-
import { handleSpecialCases } from './migrateSituation/handleSpecialCases'
5-
import { Evaluation } from 'publicodes'
1+
import { Evaluation, Situation } from 'publicodes'
2+
import { getValueWithoutQuotes, RuleName } from '../commons'
63

7-
export type NodeValue = Evaluation
8-
9-
export type Situation = {
10-
[key: string]: NodeValue
11-
}
12-
13-
export type DottedName = string
4+
/**
5+
* Associate a old value to a new value.
6+
*/
7+
export type ValueMigration = Record<string, string>
148

15-
export type MigrationType = {
16-
keysToMigrate: Record<DottedName, DottedName>
17-
valuesToMigrate: Record<DottedName, Record<string, NodeValue>>
9+
/**
10+
* Migration instructions. It contains the rules and values to migrate.
11+
*/
12+
export type Migration = {
13+
keysToMigrate: Record<RuleName, RuleName>
14+
valuesToMigrate: Record<RuleName, ValueMigration>
1815
}
1916

2017
/**
21-
* Migrate rules and answers from a situation which used to work with an old version of a model to a new version according to the migration instructions.
18+
* Migrate a situation from an old version of a model to a new version
19+
* according to the provided migration instructions.
20+
*
21+
* @param situation - The situation object containing all answers for a given simulation.
22+
* @param instructions - The migration instructions object.
2223
*
23-
* @param {Object} options - The options object.
24-
* @param {Situation} options.situation - The `situation` as Publicodes object containing all answers for a given simulation.
25-
* @param {DottedName[]} [options.foldedSteps=[]] - In case of form app, an array containing answered questions.
26-
* @param {MigrationType} options.migrationInstructions - An object containing keys and values to migrate formatted as follows:
24+
* @returns The migrated situation (and foldedSteps if specified).
2725
*
2826
* @example
29-
* ```
30-
* {
31-
* keysToMigrate: {
32-
* oldKey: newKey
33-
* }
34-
* valuesToMigrate: {
35-
* key: {
36-
* oldValue: newValue
27+
* ```typescript
28+
* import { migrateSituation } from '@publicodes/tools/migration'
29+
*
30+
* const situation = {
31+
* "age": 25
32+
* "job": "developer",
33+
* "city": "Paris"
3734
* }
35+
*
36+
* const instructions = {
37+
* keysToMigrate: {
38+
* // The rule `age` will be renamed to `âge`.
39+
* age: 'âge',
40+
* // The rule `city` will be removed.
41+
* city: ''
42+
* },
43+
* valuesToMigrate: {
44+
* job: {
45+
* // The value `developer` will be translated to `développeur`.
46+
* developer: 'développeur'
47+
* }
48+
* }
3849
* }
50+
*
51+
* migrateSituation(situation, instructions) // { "âge": 25, "job": "'développeur'" }
3952
* ```
40-
* An example can be found in {@link https://github.com/incubateur-ademe/nosgestesclimat/blob/preprod/migration/migration.yaml | nosgestesclimat repository}.
41-
* @returns {Object} The migrated situation (and foldedSteps if specified).
53+
*
54+
* @note An example of instructions can be found {@link https://github.com/incubateur-ademe/nosgestesclimat/blob/preprod/migration/migration.yaml | here}.
4255
*/
43-
export function migrateSituation({
44-
situation,
45-
foldedSteps = [],
46-
migrationInstructions,
47-
}: {
48-
situation: Situation
49-
foldedSteps?: DottedName[]
50-
migrationInstructions: MigrationType
51-
}) {
52-
let situationMigrated = { ...situation }
53-
let foldedStepsMigrated = [...foldedSteps]
56+
export function migrateSituation(
57+
situation: Situation<RuleName>,
58+
instructions: Migration,
59+
): Situation<RuleName> {
60+
let newSituation = { ...situation }
61+
const currentRules = Object.keys(situation)
62+
const valueKeysToMigrate = Object.keys(instructions.valuesToMigrate)
5463

55-
Object.entries(situationMigrated).map(([ruleName, nodeValue]) => {
56-
situationMigrated = handleSpecialCases({
57-
ruleName,
58-
nodeValue,
59-
situation: situationMigrated,
60-
})
64+
Object.entries(situation).map(([rule, value]) => {
65+
handleSpecialCases(rule, value, newSituation)
6166

62-
// We check if the non supported ruleName is a key to migrate.
63-
// Ex: "logement . chauffage . bois . type . bûche . consommation": "xxx" which is now ""logement . chauffage . bois . type . bûches . consommation": "xxx"
64-
if (Object.keys(migrationInstructions.keysToMigrate).includes(ruleName)) {
65-
const result = handleSituationKeysMigration({
66-
ruleName,
67-
nodeValue,
68-
situation: situationMigrated,
69-
foldedSteps: foldedStepsMigrated,
70-
migrationInstructions,
71-
})
67+
if (currentRules.includes(rule)) {
68+
updateKey(rule, value, newSituation, instructions.keysToMigrate[rule])
69+
}
7270

73-
situationMigrated = result.situationMigrated
74-
foldedStepsMigrated = result.foldedStepsMigrated
71+
const formattedValue = getValueWithoutQuotes(value) ?? (value as string)
72+
const valuesMigration =
73+
instructions.valuesToMigrate[
74+
valueKeysToMigrate.find((key) => rule.includes(key))
75+
] ?? {}
76+
const oldValuesName = Object.keys(valuesMigration)
77+
78+
if (oldValuesName.includes(formattedValue)) {
79+
updateValue(rule, valuesMigration[formattedValue], newSituation)
7580
}
81+
})
82+
83+
return newSituation
84+
}
85+
86+
/**
87+
* Handle migration of old value format : an object { valeur: number, unité: string }.
88+
*
89+
* @example
90+
* ```json
91+
* { valeur: number, unité: string }
92+
* ```
93+
*/
94+
function handleSpecialCases(
95+
rule: RuleName,
96+
oldValue: Evaluation,
97+
situation: Situation<RuleName>,
98+
): void {
99+
// Special case, number store as a string, we have to convert it to a number
100+
if (
101+
oldValue &&
102+
typeof oldValue === 'string' &&
103+
!isNaN(parseFloat(oldValue))
104+
) {
105+
situation[rule] = parseFloat(oldValue)
106+
}
76107

77-
const matchingValueToMigrateObject =
78-
migrationInstructions.valuesToMigrate[
79-
Object.keys(migrationInstructions.valuesToMigrate).find((key) =>
80-
ruleName.includes(key),
81-
) as any
82-
]
108+
// Special case : wrong value format, legacy from previous publicodes version
109+
// handle the case where valeur is a string "2.33"
110+
if (oldValue && oldValue['valeur'] !== undefined) {
111+
situation[rule] =
112+
typeof oldValue['valeur'] === 'string' &&
113+
!isNaN(parseFloat(oldValue['valeur']))
114+
? parseFloat(oldValue['valeur'])
115+
: (oldValue['valeur'] as number)
116+
}
117+
// Special case : other wrong value format, legacy from previous publicodes version
118+
// handle the case where nodeValue is a string "2.33"
119+
if (oldValue && oldValue['nodeValue'] !== undefined) {
120+
situation[rule] =
121+
typeof oldValue['nodeValue'] === 'string' &&
122+
!isNaN(parseFloat(oldValue['nodeValue']))
123+
? parseFloat(oldValue['nodeValue'])
124+
: (oldValue['nodeValue'] as number)
125+
}
126+
}
83127

84-
const formattedNodeValue =
85-
getValueWithoutQuotes(nodeValue) || (nodeValue as string)
128+
function updateKey(
129+
rule: RuleName,
130+
oldValue: Evaluation,
131+
situation: Situation<RuleName>,
132+
ruleToMigrate: RuleName | undefined,
133+
): void {
134+
if (ruleToMigrate === undefined) {
135+
return
136+
}
86137

87-
if (
88-
// We check if the value of the non supported ruleName value is a value to migrate.
89-
// Ex: answer "logement . chauffage . bois . type": "bûche" changed to "bûches"
90-
// If a value is specified but empty, we consider it to be deleted (we need to ask the question again)
91-
// Ex: answer "transport . boulot . commun . type": "vélo"
92-
matchingValueToMigrateObject &&
93-
Object.keys(matchingValueToMigrateObject).includes(
94-
// If the string start with a ', we remove it along with the last character
95-
// Ex: "'bûche'" => "bûche"
96-
formattedNodeValue,
97-
)
98-
) {
99-
const result = handleSituationValuesMigration({
100-
ruleName,
101-
nodeValue: formattedNodeValue,
102-
situation: situationMigrated,
103-
foldedSteps: foldedStepsMigrated,
104-
migrationInstructions,
105-
})
138+
delete situation[rule]
106139

107-
situationMigrated = result.situationMigrated
108-
foldedStepsMigrated = result.foldedStepsMigrated
109-
}
110-
})
140+
if (ruleToMigrate !== '') {
141+
situation[ruleToMigrate] =
142+
typeof oldValue === 'object' ? (oldValue as any)?.valeur : oldValue
143+
}
144+
}
111145

112-
return { situationMigrated, foldedStepsMigrated }
146+
function updateValue(
147+
rule: RuleName,
148+
value: string,
149+
situation: Situation<RuleName>,
150+
): void {
151+
// The value is not a value to migrate and the key has to be deleted
152+
if (value === '') {
153+
delete situation[rule]
154+
} else {
155+
// The value is renamed and needs to be migrated
156+
situation[rule] =
157+
typeof value === 'string' && value !== 'oui' && value !== 'non'
158+
? `'${value}'`
159+
: value
160+
}
113161
}

0 commit comments

Comments
 (0)