Motor de Reglas SimpleRules (2) Implementando un DSL

Published on Author lopez

Anterior Post
Siguiente Post

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