Otro de los casos de uso que quiero implementar para usar mi proyecto de motor de reglas:
https://github.com/ajlopez/SimpleRules
Es poder definir las reglas en un lenguaje textual simple, podría decir en un DSL (Domain Specific Language). Ejemplo de lo que tenía en mente:
rule name FeverRule when model.temperature >= 38 then model.fever = true rule name CriticalFever when model.temperature >= 40 model.age >= 12 then model.critical = true
Quiero implementarlo usando indentación como en Python, para evitar sobrecargarlo con palabras o caracteres delimitadores de comienzo y fin de cada parte, como begin y end o las clásicas llaves { y }
Un opción sería usar un parser, tomar una librería ya existente, usar mi propio proyecto SimpleGrammar, o definir y consumir una gramática usando Backus-Naur Format o similares. Pero pensé que debía haber una forma más simple. Y al poco de meditar encontré un camino.
La primer idea es leer un texto, partirlo en líneas, y en cada línea proceder a calcular el nivel de indentación que tiene. Ejemplo de test:
exports['parse text'] = function (test) { var result = lines("rule"); test.deepEqual(result, [ { indent: 0, text: "rule" } ]); }; exports['parse text with indent and spaces'] = function (test) { var result = lines(" rule "); test.deepEqual(result, [ { indent: 2, text: "rule" } ]); }; exports['parse text with two lines'] = function (test) { var result = lines("rule\n when"); test.deepEqual(result, [ { indent: 0, text: "rule" }, { indent: 2, text: "when" } ]); }; exports['parse text with two lines and carriage return'] = function (test) { var result = lines("rule\r\n when\r\n"); test.deepEqual(result, [ { indent: 0, text: "rule" }, { indent: 2, text: "when" } ]); };
Luego, fui armando una librería auxiliar a usar por el módulo principal, que tomara la salida de las líneas analizadas, y forme un árbol de textos, donde ya no importa la indentación. Ejemplos de tests:
var parse = require('../lib/parse'); exports['parse line'] = function (test) { var result = parse("rule"); test.deepEqual(result, [{ text: "rule" }]); }; exports['parse line and indented line'] = function (test) { var result = parse("rule\n when"); test.deepEqual(result, [ { text: "rule", elements: [ { text: "when" } ]} ]); }; exports['parse rule'] = function (test) { var result = parse("rule\n when\n model.temperature >= 37\n then\n facts.fever = true"); test.deepEqual(result, [ { text: "rule", elements: [ { text: "when", elements: [ { text: "model.temperature >= 37" } ] }, { text: "then", elements: [ { text: "facts.fever = true" } ] } ]} ]); };
Y luego de estos baby steps, implementados de la forma más simple, llegué a compilar reglas, ejemplo de tests:
var simplerules = require('..'); exports['compile and run simple rule'] = function (test) { var text = [ "rule", " when", " model.temperature >= 37", " then", " model.fever = true" ].join('\n'); var engine = simplerules.compile(text); test.ok(engine.rules); test.equal(engine.rules.length, 1); var model = { temperature: 38 }; engine.run(model); test.strictEqual(model.fever, true); }
Y así, en apenas una hora y monedas, tengo una primera implementación de un DSL de definición de reglas. Gracias a seguir el principio de simplicidad, baby steps y el flujo de trabajo de TDD. En próximo post, espero mostrar algún refactor de implementación y nuevos casos de uso, y explicar el algoritmo de disparo de reglas.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez