Skip to content

Commit

Permalink
refactor(optim): serialization implemented of all the basic API
Browse files Browse the repository at this point in the history
  • Loading branch information
EmileRolley committed Jan 17, 2024
1 parent 0b4974a commit fed1c1e
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 60 deletions.
143 changes: 83 additions & 60 deletions source/serializeParsedRules.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { ASTNode, ParsedRules, reduceAST, serializeUnit } from 'publicodes'
import { RawRule, RuleName } from './commons'
//
// type SourceMap = {
// mecanismName: string
// args: Record<string, ASTNode | Array<ASTNode>>
// }
//

type SerializedRule = RawRule | number | string

function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule {
Expand All @@ -18,7 +13,6 @@ function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule {
}

function serializeValue(node: ASTNode, needParens = false): SerializedRule {
// console.log('[SERIALIZE_VALUE]:', node.nodeKind, needParens)
switch (node.nodeKind) {
case 'reference': {
return node.name
Expand Down Expand Up @@ -96,11 +90,12 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule {
}

default: {
return `TODO: ${node.nodeKind}`
throw new Error(`[SERIALIZE_VALUE]: '${node.nodeKind}' not implemented`)
}
}
}

// TODO: this function might be refactored
function serializeSourceMap(node: ASTNode): SerializedRule {
const sourceMap = node.sourceMap

Expand All @@ -109,9 +104,6 @@ function serializeSourceMap(node: ASTNode): SerializedRule {
const value = sourceMap.args[key]
const isArray = Array.isArray(value)

console.log('[SOURCE_MAP]:', key)

// FIXME: bug with 'une de ses conditions' with 'applicable si'.
rawRule[sourceMap.mecanismName] = isArray
? value.map((v) => serializeASTNode(v))
: serializeASTNode(value)
Expand All @@ -121,14 +113,7 @@ function serializeSourceMap(node: ASTNode): SerializedRule {

function serializeASTNode(node: ASTNode): SerializedRule {
return reduceAST<SerializedRule>(
(rawRule, node: ASTNode) => {
// if (node?.nodeKind) {
console.log('[NODE_KIND]:', node.nodeKind)
console.log('[MECANISME_NAME]:', node?.sourceMap?.mecanismName)
// } else {
// console.log('[NODE_KIND]:', node)
// return rawRule
// }
(_, node: ASTNode) => {
switch (node?.nodeKind) {
case 'reference':
case 'constant':
Expand Down Expand Up @@ -169,9 +154,12 @@ function serializeASTNode(node: ASTNode): SerializedRule {
}

case 'arrondi': {
const serializedValeur = serializedRuleToRawRule(
serializeASTNode(node.explanation.valeur),
)
return {
...serializedValeur,
arrondi: serializeASTNode(node.explanation.arrondi),
valeur: serializeASTNode(node.explanation.valeur),
}
}

Expand Down Expand Up @@ -227,8 +215,8 @@ function serializeASTNode(node: ASTNode): SerializedRule {
serializeASTNode(node.explanation.node),
)
return {
contexte,
...serializedExplanationNode,
contexte,
}
}

Expand Down Expand Up @@ -257,10 +245,10 @@ function serializeASTNode(node: ASTNode): SerializedRule {
serializeASTNode(node.explanation.alors),
)
return {
...serializedExplanationNode,
[mecanismName]: serializeASTNode(
sourceMap.args[mecanismName] as ASTNode,
),
...serializedExplanationNode,
}
}

Expand All @@ -273,57 +261,66 @@ function serializeASTNode(node: ASTNode): SerializedRule {

case 'abattement':
case 'plancher':
case 'plafond': {
case 'plafond':
case 'par défaut': {
const serializedExplanationNode = serializedRuleToRawRule(
serializeASTNode(node.sourceMap.args.valeur as ASTNode),
)

return {
...serializedExplanationNode,
[mecanismName]: serializeASTNode(
node.sourceMap.args[mecanismName] as ASTNode,
),
...serializedExplanationNode,
}
}

default: {
throw new Error(
`[SERIALIZE_AST_NODE]: mecanism '${mecanismName}' found in a '${node.nodeKind}`,
)
}
}
}

case 'variable manquante': {
// Simple need to unwrap the explanation node
return serializeASTNode(node.explanation)
}

case 'texte': {
const serializedText = node.explanation.reduce(
(currText: string, node: ASTNode | string) => {
if (typeof node === 'string') {
return currText + node
}

const serializedNode = serializeASTNode(node)
if (typeof serializedNode !== 'string') {
throw new Error(`[SERIALIZE_AST_NODE > 'texte']: all childs of 'texte' expect to be serializable as string.
Got '${serializedNode}'`)
}
return currText + '{{ ' + serializedNode + ' }}'
},
'',
)
return { texte: serializedText }
}

case 'une possibilité': {
return {
'une possibilité': {
'choix obligatoire': node['choix obligatoire'],
possibilités: node.explanation.map(serializeASTNode),
},
}
}

default: {
// console.log('[DEFAULT]:', node.nodeKind)
// console.log('[KIND]:', node.nodeKind)
// console.log('[SOURCE_MAP]:', node.sourceMap)
// if (node?.sourceMap) {
// switch (node.sourceMap.mecanismName) {
// case 'dans la situation': {
// if (node.nodeKind === 'condition') {
// console.log(`\n----- serializing `, node.nodeKind)
// console.log(`----- node `, node)
// const serializedNode = serializeASTNode(
// node.explanation.alors,
// )
// if (typeof serializedNode !== 'object') {
// return {
// valeur: serializedNode,
// }
// }
// return {
// ...rawRule,
// ...serializeASTNode(node.explanation.alors),
// }
// } else {
// console.error(
// `'dans la situation' expect be resolved to a condition got ${node.nodeKind}`,
// )
// }
// }
// default: {
// return { ...rawRule, ...serializeMechanism(node) }
// }
// }
// } else {
// return { ...rawRule, ...serializeMechanism(node) }
// }
// return { ...rawRule, ...serializeSourceMap(node) }
throw new Error(
`[SERIALIZE_AST_NODE]: '${node.nodeKind}' not implemented.
Node:\n${JSON.stringify(node, null, 2)}`,
)
}
}
},
Expand All @@ -335,14 +332,40 @@ function serializeASTNode(node: ASTNode): SerializedRule {
export function serializeParsedRules(
parsedRules: ParsedRules<RuleName>,
): Record<RuleName, RawRule> {
/**
* This mecanisms are syntaxique sugars that are inlined with simplier ones.
* Consequently, we need to remove them from the rawNode in order to avoid
* duplicate mecanisms.
*
*
* NOTE: for now, the [avec] mecanism is unfolded as full rules. Therefore,
* we need to remove the [avec] mecanism from the rawNode in order to
* avoid duplicate rule definitions.
*
* TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but
* for now it's not a priority.
*/
const syntaxiqueSugars = ['avec', 'formule']
const rawRules = {}

for (const [rule, node] of Object.entries(parsedRules)) {
console.log(`serializing ${rule}`)
if (Object.keys(node.rawNode).length === 0) {
console.log(`[SERIALIZE_PARSED_RULES]: empty rule '${rule}' found`)
// Empty rule should be null not {}
rawRules[rule] = null
continue
}

const serializedNode = serializedRuleToRawRule(
serializeASTNode(node.explanation.valeur),
)

syntaxiqueSugars.forEach((attr) => {
if (attr in node.rawNode) {
delete node.rawNode[attr]
}
})

rawRules[rule] = {
...node.rawNode,
...serializedNode,
Expand Down
147 changes: 147 additions & 0 deletions test/serializeParsedRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,151 @@ describe('API > mecanisms list', () => {
)
expect(serializedRules).toStrictEqual(rules)
})

it('should serialize rule with [par défaut]', () => {
const rules = {
'prix HT': {
valeur: '50 €',
},
'prix TTC': {
assiette: 'prix HT * (100 % + TVA)',
},
TVA: {
valeur: '50 %',
'par défaut': '20 %',
},
}
const serializedRules = serializeParsedRules(
new Engine(rules).getParsedRules(),
)
expect(serializedRules).toStrictEqual(rules)
})

it('should serialize rule with [avec]', () => {
const rules = {
'prix final': {
valeur: 'prix de base * (100% - réduction)',
avec: {
'prix de base': '157 €',
réduction: '20 %',
},
},
}
const serializedRules = serializeParsedRules(
new Engine(rules).getParsedRules(),
)
expect(serializedRules).toStrictEqual({
'prix final': {
valeur: 'prix de base * (100 % - réduction)',
},
'prix final . prix de base': {
valeur: '157 €',
},
'prix final . réduction': {
valeur: '20 %',
},
})
})

it('should serialize rule with [texte]', () => {
const rules = {
'aide vélo': {
texte: `La région subventionne l’achat d’un vélo à hauteur de
{{ prise en charge }} et jusqu’à un plafond de {{ 500 € }}.
Les éventuelles aides locales déjà perçues sont déduites de
ce montant.
Par exemple, pour un vélo de {{ exemple }}, la région {{ 'Nouvelle Aquitaine' }}
vous versera {{ exemple * prise en charge }}`,
},
'aide vélo . prise en charge': {
valeur: '50 %',
},
'aide vélo . exemple': {
valeur: '250 €',
},
}
const serializedRules = serializeParsedRules(
new Engine(rules).getParsedRules(),
)
expect(serializedRules).toStrictEqual(rules)
})

it('should serialize rule with multiple [mécanismes chaînés]', () => {
const rules = {
'nombre de repas': {
valeur: '12 repas',
},
'remboursement repas': {
valeur: 'nombre de repas * 4.21 €/repas',
plafond: '500 €',
abattement: '25 €',
arrondi: 'oui',
},
}
const serializedRules = serializeParsedRules(
new Engine(rules).getParsedRules(),
)
expect(serializedRules).toStrictEqual(rules)
})

it('should serialize rule with multiple [une possibilité]', () => {
const rules = {
'chauffage collectif': {
avec: {
collectif: null,
individuel: null,
},
question: 'Votre chauffage est-il collectif ou individuel ?',
'par défaut': "'collectif'",
formule: {
'une possibilité': {
'choix obligatoire': 'oui',
possibilités: ['collectif', 'individuel'],
},
},
},
}
const serializedRules = serializeParsedRules(
new Engine(rules).getParsedRules(),
)
expect(serializedRules).toStrictEqual({
'chauffage collectif': {
question: 'Votre chauffage est-il collectif ou individuel ?',
'par défaut': "'collectif'",
'une possibilité': {
'choix obligatoire': 'oui',
possibilités: ['collectif', 'individuel'],
},
},
'chauffage collectif . collectif': null,
'chauffage collectif . individuel': null,
})
})

// TODO
// it('should serialize rule with [private rule]', () => {
// const rules = {
// 'aide vélo': {
// texte: `La région subventionne l’achat d’un vélo à hauteur de
// {{ prise en charge }} et jusqu’à un plafond de {{ 500 € }}.
// Les éventuelles aides locales déjà perçues sont déduites de
// ce montant.
//
// Par exemple, pour un vélo de {{ exemple }}, la région {{ 'Nouvelle Aquitaine' }}
// vous versera {{ exemple * prise en charge }}`,
// },
// 'aide vélo . prise en charge': {
// valeur: '50 %',
// },
// 'aide vélo . exemple': {
// valeur: '250 €',
// },
// }
// const serializedRules = serializeParsedRules(
// new Engine(rules).getParsedRules(),
// )
// console.log(JSON.stringify(serializedRules, null, 2))
// expect(serializedRules).toStrictEqual(rules)
// })
})

0 comments on commit fed1c1e

Please sign in to comment.