/** * Note: all here is strictly based on duck typing. * We don't exepect the parent rule to explain the type of the contained formula, for example. */ import R from 'ramda' import { ParsedRule, ParsedRules } from './types' type OnOff = 'oui' | 'non' export function isOnOff(a: string): a is OnOff { return a === 'oui' || a === 'non' } type WannabeDottedName = string export function isWannabeDottedName(a: string): a is WannabeDottedName { return typeof a === 'string' } type ASTNode = { [_: string]: {} | undefined } // [XXX] - Vaudrait-il mieux utiliser les DottedNames directement ici? // A priori non car on peut imaginer cette lib comme étant indépendante des règles existantes et // fonctionnant par ex en "mode serveur". type RuleNode = /* ASTNode & */ ParsedRule type Value = ASTNode & { nodeValue: number | string constant?: boolean } export function isValue(node: ASTNode): node is Value { const value = node as Value return ( (typeof value.nodeValue === 'string' || typeof value.nodeValue === 'number') && (value.constant === undefined || typeof value.constant === 'boolean') ) } type Operation = ASTNode & { operationType: 'comparison' | 'calculation' explanation: Array } export function isOperation(node: ASTNode): node is Operation { return R.includes((node as Operation).operationType, [ 'comparison', 'calculation' ]) } type Possibilities = ASTNode & { possibilités: Array 'choix obligatoire'?: OnOff 'une possibilité': OnOff } export function isPossibilities(node: ASTNode): node is Possibilities { const possibilities = node as Possibilities return ( possibilities.possibilités instanceof Array && possibilities.possibilités.every(it => typeof it === 'string') && (possibilities['choix obligatoire'] === undefined || isOnOff(possibilities['choix obligatoire'])) && isOnOff(possibilities['une possibilité']) ) } type Reference = ASTNode & { category: 'reference' name: Name partialReference: Name dottedName: Name unit: {} | undefined } export function isReference( node: ASTNode ): node is Reference { const reference = node as Reference return ( reference.category === 'reference' && isWannabeDottedName(reference.name) && isWannabeDottedName(reference.partialReference) && isWannabeDottedName(reference.dottedName) ) } type Recalcul = ASTNode & { explanation: { recalcul: Reference amendedSituation: Record> } } export function isRecalcul( node: ASTNode ): node is Recalcul { const recalcul = node as Recalcul return ( typeof recalcul.explanation === 'object' && typeof recalcul.explanation.recalcul === 'object' && isReference(recalcul.explanation.recalcul as ASTNode) && typeof recalcul.explanation.amendedSituation === 'object' // [XXX] - We would like to do // && R.all(isDottedName, R.keys(recalcul.explanation.amendedSituation)) // but it seems there is no simple way to get a type's guard in Typescript // apart if it's built as a class. Or we could rebuild everything here with // passing this guard ƒ as a context everywhere along with the ASTNodes, // with a context monad for example. Overkill. ) } type AbstractMechanism = ASTNode & { category: 'mecanism' name: string } export function isAbstractMechanism(node: ASTNode): node is AbstractMechanism { return ( (node as AbstractMechanism).category === 'mecanism' && typeof (node as AbstractMechanism).name === 'string' ) } type EncadrementMech = AbstractMechanism & { name: 'encadrement' valeur: { explanation: Array [_: string]: {} | undefined } } export function isEncadrementMech(node: ASTNode): node is EncadrementMech { const encadrementMech = node as EncadrementMech return ( isAbstractMechanism(encadrementMech) && typeof encadrementMech.valeur === 'object' && encadrementMech.valeur.explanation instanceof Array && encadrementMech.name === 'encadrement' ) } type SommeMech = AbstractMechanism & { name: 'somme' explanation: Array } export function isSommeMech(node: ASTNode): node is SommeMech { const sommeMech = node as SommeMech return ( isAbstractMechanism(sommeMech) && sommeMech.name === 'somme' && typeof sommeMech.explanation === 'object' ) } type ProduitMech = AbstractMechanism & { name: 'produit' type: 'numeric' unit: {} explanation: { assiette: ASTNode plafond: ASTNode facteur: ASTNode taux: ASTNode } } export function isProduitMech(node: ASTNode): node is ProduitMech { const produitMech = node as ProduitMech return ( produitMech.name === 'produit' && produitMech.type === 'numeric' && produitMech.unit !== undefined && typeof produitMech.explanation === 'object' && typeof produitMech.explanation.assiette === 'object' && typeof produitMech.explanation.plafond === 'object' && typeof produitMech.explanation.facteur === 'object' && typeof produitMech.explanation.taux === 'object' ) } type AnyMechanism = EncadrementMech | SommeMech export function isAnyMechanism(node: ASTNode): node is AnyMechanism { return isEncadrementMech(node) || isSommeMech(node) || isProduitMech(node) } type FormuleNode = | Value | Operation | Possibilities | Reference | Recalcul | AnyMechanism export function isFormuleNode( node: ASTNode ): node is FormuleNode { return ( isValue(node) || isOperation(node) || isReference(node) || isPossibilities(node) || isRecalcul(node) || isAnyMechanism(node) ) } function logVisit( depth: number, typeName: string, obj: string | number | object ): void { let cleanRepr = obj if (typeof obj === 'object') { cleanRepr = JSON.stringify(obj, null) } console.log(' '.repeat(depth) + `visiting ${typeName} node ${cleanRepr}`) } export function ruleDependenciesOfNode( depth: number, node: ASTNode ): Array { function ruleDependenciesOfValue(depth: number, value: Value): Array { logVisit(depth, 'value', value.nodeValue) return [] } function ruleDependenciesOfOperation( depth: number, operation: Operation ): Array { logVisit(depth, 'operation', operation.operationType) return R.chain( R.partial>(ruleDependenciesOfNode, [ depth + 1 ]), operation.explanation ) } function ruleDependenciesOfPossibilities( depth: number, possibilities: Possibilities ): Array { logVisit(depth, 'possibilities', possibilities.possibilités.join(', ')) return [] } function ruleDependenciesOfReference( depth: number, reference: Reference ): Array { logVisit(depth, 'reference', reference.dottedName) return [reference.dottedName] } function ruleDependenciesOfRecalcul( depth: number, recalcul: Recalcul ): Array { logVisit( depth, 'recalcul', recalcul.explanation.recalcul.partialReference as string ) return [recalcul.explanation.recalcul.partialReference] } function ruleDependenciesOfEncadrementMech( depth: number, encadrementMech: EncadrementMech ): Array { debugger // [XXX] - correct the type, guard and visit logger logVisit(depth, 'encadrement mechanism', '??') // [XXX] return R.chain( R.partial>(ruleDependenciesOfNode, [ depth + 1 ]), encadrementMech.valeur.explanation ) } function ruleDependenciesOfSommeMech( depth: number, sommeMech: SommeMech ): Array { debugger // [XXX] - correct the type, guard and visit logger logVisit(depth, 'somme mech', '??') // [XXX] return R.chain( R.partial>(ruleDependenciesOfNode, [ depth + 1 ]), sommeMech.explanation ) } function ruleDependenciesOfProduitMech( depth: number, produitMech: ProduitMech ): Array { logVisit(depth, 'produit mech', '') const result = R.chain, Name>(R.identity, [ ruleDependenciesOfNode(depth + 1, produitMech.explanation.assiette), ruleDependenciesOfNode(depth + 1, produitMech.explanation.plafond), ruleDependenciesOfNode(depth + 1, produitMech.explanation.facteur), ruleDependenciesOfNode(depth + 1, produitMech.explanation.taux) ]) return result } if (isValue(node)) { return ruleDependenciesOfValue(depth, node as Value) } else if (isOperation(node)) { return ruleDependenciesOfOperation(depth, node as Operation) } else if (isReference(node)) { return ruleDependenciesOfReference(depth, node as Reference) } else if (isPossibilities(node)) { return ruleDependenciesOfPossibilities(depth, node as Possibilities) } else if (isRecalcul(node)) { return ruleDependenciesOfRecalcul(depth, node as Recalcul) } else if (isEncadrementMech(node)) { return ruleDependenciesOfEncadrementMech(depth, node as EncadrementMech) } else if (isSommeMech(node)) { return ruleDependenciesOfSommeMech(depth, node as SommeMech) } else if (isProduitMech(node)) { return ruleDependenciesOfProduitMech(depth, node) } throw new Error( `This node doesn't have a visitor method defined: ${JSON.stringify( node, null, 4 )}` ) } function ruleDependenciesOfRule( depth: number, rule: RuleNode ): Array { logVisit(depth, 'rule', rule.dottedName as string) if (rule.formule) { const formuleNode: FormuleNode = rule.formule.explanation // This is for comfort, as the responsibility over structure isn't owned by this piece of code: if (!isFormuleNode(formuleNode)) { debugger // throw Error( // `This rule's formule is not of a known type: ${rule.dottedName}` // ) } return ruleDependenciesOfNode(depth + 1, formuleNode) } else return [rule.dottedName] } export function buildRulesDependencies( parsedRules: ParsedRules ): Array<[Name, Array]> { // This stringPairs thing is necessary because `toPairs` is strictly considering that // object keys are strings (same for `Object.entries`). Maybe we should build our own // `toPairs`? const stringPairs: Array<[string, RuleNode]> = R.toPairs(parsedRules) const pairs: Array<[Name, RuleNode]> = stringPairs as Array< [Name, RuleNode] > const pairsResults: Array> = R.map( ([_, ruleNode]: [Name, RuleNode]): Array => ruleDependenciesOfRule(0, ruleNode), pairs ) console.log(pairsResults) return R.map( ([dottedName, ruleNode]: [Name, RuleNode]): [Name, Array] => [ dottedName, ruleDependenciesOfRule(0, ruleNode) ], pairs ) }