|
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' |
6 | 3 |
|
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> |
14 | 8 |
|
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> |
18 | 15 | }
|
19 | 16 |
|
20 | 17 | /**
|
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. |
22 | 23 | *
|
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). |
27 | 25 | *
|
28 | 26 | * @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" |
37 | 34 | * }
|
| 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 | + * } |
38 | 49 | * }
|
| 50 | + * |
| 51 | + * migrateSituation(situation, instructions) // { "âge": 25, "job": "'développeur'" } |
39 | 52 | * ```
|
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}. |
42 | 55 | */
|
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) |
54 | 63 |
|
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) |
61 | 66 |
|
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 | + } |
72 | 70 |
|
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) |
75 | 80 | }
|
| 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 | + } |
76 | 107 |
|
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 | +} |
83 | 127 |
|
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 | + } |
86 | 137 |
|
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] |
106 | 139 |
|
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 | +} |
111 | 145 |
|
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 | + } |
113 | 161 | }
|
0 commit comments