Click here to Skip to main content
15,886,110 members
Articles / Web Development / HTML

Logician: A Table-based Rules Engine Suite in C++/.NET/JavaScript using XML

Rate me:
Please Sign up or sign in to vote.
4.90/5 (52 votes)
8 Mar 2017GPL314 min read 104.1K   6.8K   173  
Overview of a cross-platform spreadsheet-based rules enigne with algorithms implemented in C++ and JavaScript, and wrappers to C#/.NET/WinRT
// Copyright 2005, Google Inc.
// All Rights Reserved.
//
// Unit test for the XPath parser and engine.
//
// Author: Steffen Meschkat <mesch@google.com>
//         Junji Takagi <jtakagi@google.com>

//********************************************
// DGF BEWARE!  You MUST update this function if you add tests!
//********************************************
function exposeTestFunctionNames() {
    return ['testParse', 'testEval', 'testAxes', 'testAttributeAsterisk', 'testEvalDom', 'testEvalDomJapanese'];
}

var expr = [
    "@*",
    "@*|node()",
    "/descendant-or-self::div",
    "/div",
    "//div",
    "/descendant-or-self::node()/child::para",
    "substring('12345', 0, 3)",
    "//title | //link",
    "$x//title",
    // "$x/title",  // TODO(mesch): parsing of this expression is broken
    "id('a')//title",
    "//*[@about]",
    "count(descendant::*)",
    "count(descendant::*) + count(ancestor::*)",
    "concat(substring-before(@image,'marker'),'icon',substring-after(@image,'marker'))",
    "@*|text()",
    "*|/",
    "source|destination",
    "$page != 'to' and $page != 'from'",
    "substring-after(icon/@image, '/mapfiles/marker')",
    "substring-before($str, $c)",
    "$page = 'from'",
    "segments/@time",
    "child::para",
    "child::*",
    "child::text()",
    "child::node()",
    "attribute::name",
    "attribute::*",
    "descendant::para",
    "ancestor::div",
    "ancestor-or-self::div",
    "descendant-or-self::para",
    "self::para",
    "child::chapter/descendant::para",
    "child::*/child::para",
    "/",
    "/descendant::para",
    "/descendant::olist/child::item",
    "child::para[position()=1]",
    "child::para[position()=last()]",
    "child::para[position()=last()-1]",
    "child::para[position()>1]",
    "following-sibling::chapter[position()=1]",
    "preceding-sibling::chapter[position()=1]",
    "/descendant::figure[position()=42]",
    "/child::doc/child::chapter[position()=5]/child::section[position()=2]",
    "child::para[attribute::type='warning']",
    "child::para[attribute::type='warning'][position()=5]",
    "child::para[position()=5][attribute::type='warning']",
    "child::chapter[child::title='Introduction']",
    "child::chapter[child::title]",
    "child::*[self::chapter or self::appendix]",
    "child::*[self::chapter or self::appendix][position()=last()]",
    "count(//*[id='u1']|//*[id='u2'])",
    "count(//*[id='u1']|//*[class='u'])",
    "count(//*[class='u']|//*[class='u'])",
    "count(//*[class='u']|//*[id='u1'])",

    // Axis expressions
    "count(//*[@id='self']/ancestor-or-self::*)",
    "count(//*[@id='self']/ancestor::*)",
    "count(//*[@id='self']/attribute::*)",
    "count(//*[@id='self']/child::*)",
    "count(//*[@id='self']/descendant-or-self::*)",
    "count(//*[@id='self']/descendant::*)",
    "count(//*[@id='self']/following-sibling::*)",
    "count(//*[@id='self']/following::*)",
    "//*[@id='self']/parent::*/@id",
    "count(//*[@id='self']/preceding-sibling::*)",
    "count(//*[@id='self']/preceding::*)",
    "//*[@id='self']/self::*/@id",

    // (Japanese)
    "/descendant-or-self::\u90e8\u5206",
    "//\u90e8\u5206",
    "substring('\uff11\uff12\uff13\uff14\uff15', 0, 3)",
    "//\u30bf\u30a4\u30c8\u30eb | //\u30ea\u30f3\u30af",
    "$\u8b0e//\u30bf\u30a4\u30c8\u30eb",
    "//*[@\u30c7\u30b9\u30c6\u30a3\u30cd\u30a4\u30b7\u30e7\u30f3]",
    "concat(substring-before(@\u30a4\u30e1\u30fc\u30b8,'\u76ee\u5370'),'\u30a2\u30a4\u30b3\u30f3',substring-after(@\u30a4\u30e1\u30fc\u30b8,'\u76ee\u5370'))",
    "\u30bd\u30fc\u30b9|\u30c7\u30b9\u30c6\u30a3\u30cd\u30a4\u30b7\u30e7\u30f3",
    "$\u30da\u30fc\u30b8 != '\u307e\u3067' and $\u30da\u30fc\u30b8 != '\u304b\u3089'",
    "substring-after(\u30a2\u30a4\u30b3\u30f3/@\u30a4\u30e1\u30fc\u30b8, '/\u5730\u56f3\u30d5\u30a1\u30a4\u30eb/\u76ee\u5370')",
    "substring-before($\u6587\u5b57\u5217, $\u6587\u5b57)",
    "$\u30da\u30fc\u30b8 = '\u304b\u3089'",
    "\u30bb\u30b0\u30e1\u30f3\u30c8/@\u6642\u523b",
    "child::\u6bb5\u843d",
    "attribute::\u540d\u524d",
    "descendant::\u6bb5\u843d",
    "ancestor::\u90e8\u5206",
    "ancestor-or-self::\u90e8\u5206",
    "descendant-or-self::\u6bb5\u843d",
    "self::\u6bb5\u843d",
    "child::\u7ae0/descendant::\u6bb5\u843d",
    "child::*/child::\u6bb5\u843d",
    "/descendant::\u6bb5\u843d",
    "/descendant::\u9806\u5e8f\u30ea\u30b9\u30c8/child::\u9805\u76ee",
    "child::\u6bb5\u843d[position()=1]",
    "child::\u6bb5\u843d[position()=last()]",
    "child::\u6bb5\u843d[position()=last()-1]",
    "child::\u6bb5\u843d[position()>1]",
    "following-sibling::\u7ae0[position()=1]",
    "preceding-sibling::\u7ae0[position()=1]",
    "/descendant::\u56f3\u8868[position()=42]",
    "/child::\u6587\u66f8/child::\u7ae0[position()=5]/child::\u7bc0[position()=2]",
    "child::\u6bb5\u843d[attribute::\u30bf\u30a4\u30d7='\u8b66\u544a']",
    "child::\u6bb5\u843d[attribute::\u30bf\u30a4\u30d7='\u8b66\u544a'][position()=5]",
    "child::\u6bb5\u843d[position()=5][attribute::\u30bf\u30a4\u30d7='\u8b66\u544a']",
    "child::\u7ae0[child::\u30bf\u30a4\u30c8\u30eb='\u306f\u3058\u3081\u306b']",
    "child::\u7ae0[child::\u30bf\u30a4\u30c8\u30eb]",
    "child::*[self::\u7ae0 or self::\u4ed8\u9332]",
    "child::*[self::\u7ae0 or self::\u4ed8\u9332][position()=last()]",

    //Selenium bugs
    "id('nested1')/div[1]//input[2]",
    "id('foo')//div[contains(@id, 'useful')]//input",
    "(//table[@class='stylee'])//th[text()='theHeaderText']/../td",

    // The following are all expressions that used to occur in google
    // maps XSLT templates.
    "$address",
    "$address=string(/page/user/defaultlocation)",
    "$count-of-snippet-of-url = 0",
    "$daddr",
    "$form",
    "$form = 'from'",
    "$form = 'to'",
    "$form='near'",
    "$home",
    "$i",
    "$i > $page and $i < $page + $range",
    "$i < $page and $i >= $page - $range",
    "$i < @max",
    "$i <= $page",
    "$i + 1",
    "$i = $page",
    "$i = 1",
    "$info = position() or (not($info) and position() = 1)",
    "$is-first-order",
    "$is-first-order and $snippets-exist",
    "$more",
    "$more > 0",
    "$near-point",
    "$page",
    "$page != 'from'",
    "$page != 'to'",
    "$page != 'to' and $page != 'from'",
    "$page > 1",
    "$page = 'basics'",
    "$page = 'details'",
    "$page = 'from'",
    "$page = 'to'",
    "$page='from'",
    "$page='to'",
    "$r >= 0.5",
    "$r >= 1",
    "$r - 0",
    "$r - 1",
    "$r - 2",
    "$r - 3",
    "$r - 4",
    "$saddr",
    "$sources",
    "$sources[position() < $details]",
    "$src",
    "$str",
    "\"'\"",
    "(//location[string(info/references/reference[1]/url)=string($current-url)]/info/references/reference[1])[1]",
    "(not($count-of-snippet-of-url = 0) and (position() = 1) or not($current-url = //locations/location[position() = $last-pos]//reference[1]/url))",
    "(not($info) and position() = 1) or $info = position()",
    ".",
    "../@arg0",
    "../@filterpng",
    "/page/@filterpng",
    "4",
    "@attribution",
    "@id",
    "@max > @num",
    "@meters > 16093",
    "@name",
    "@start div @num + 1",
    "@url",
    "ad",
    "address/line",
    "adsmessage",
    "attr",
    "boolean(location[@id='near'][icon/@image])",
    "bubble/node()",
    "calltoaction/node()",
    "category",
    "contains($str, $c)",
    "count(//location[string(info/references/reference[1]/url)=string($current-url)]//snippet)",
    "count(//snippet)",
    "count(attr)",
    "count(location)",
    "count(structured/source) > 1",
    "description/node()",
    "destination",
    "destinationAddress",
    "domain",
    "false()",
    "icon/@class != 'noicon'",
    "icon/@image",
    "info",
    "info/address/line",
    "info/distance",
    "info/distance and $near-point",
    "info/distance and info/phone and $near-point",
    "info/distance or info/phone",
    "info/panel/node()",
    "info/phone",
    "info/references/reference[1]",
    "info/references/reference[1]/snippet",
    "info/references/reference[1]/url",
    "info/title",
    "info/title/node()",
    "line",
    "location",
    "location[@id!='near']",
    "location[@id='near'][icon/@image]",
    "location[position() > $numlocations div 2]",
    "location[position() <= $numlocations div 2]",
    "locations",
    "locations/location",
    "near",
    "node()",
    "not($count-of-snippets = 0)",
    "not($form = 'from')",
    "not($form = 'near')",
    "not($form = 'to')",
    "not(../@page)",
    "not(structured/source)",
    "notice",
    "number(../@info)",
    "number(../@items)",
    "number(/page/@linewidth)",
    "page/ads",
    "page/directions",
    "page/error",
    "page/overlay",
    "page/overlay/locations/location",
    "page/refinements",
    "page/request/canonicalnear",
    "page/request/near",
    "page/request/query",
    "page/spelling/suggestion",
    "page/user/defaultlocation",
    "phone",
    "position()",
    "position() != 1",
    "position() != last()",
    "position() > 1",
    "position() < $details",
    "position()-1",
    "query",
    "references/@total",
    "references/reference",
    "references/reference/domain",
    "references/reference/url",
    "reviews/@positive div (reviews/@positive + reviews/@negative) * 5",
    "reviews/@positive div (reviews/@positive + reviews/@negative) * (5)",
    "reviews/@total",
    "reviews/@total > 1",
    "reviews/@total > 5",
    "reviews/@total = 1",
    "segments/@distance",
    "segments/@time",
    "segments/segment",
    "shorttitle/node()",
    "snippet",
    "snippet/node()",
    "source",
    "sourceAddress",
    "sourceAddress and destinationAddress",
    "string(../@daddr)",
    "string(../@form)",
    "string(../@page)",
    "string(../@saddr)",
    "string(info/title)",
    "string(page/request/canonicalnear) != ''",
    "string(page/request/near) != ''",
    "string-length($address) > $linewidth",
    "structured/@total - $details",
    "structured/source",
    "structured/source[@name]",
    "substring($address, 1, $linewidth - 3)",
    "substring-after($str, $c)",
    "substring-after(icon/@image, '/mapfiles/marker')",
    "substring-before($str, $c)",
    "tagline/node()",
    "targetedlocation",
    "title",
    "title/node()",
    "true()",
    "url",
    "visibleurl"
];

function testParse() {
  for (var i = 0; i < expr.length; ++i) {
    assert(expr[i], xpathParse(expr[i]));
  }
}

var numExpr = [
    /* number expressions */
    [ "1+1", 2 ],
    [ "floor( -3.1415 )", -4 ],
    [ "-5 mod -2", -1 ],
    [ "-5 mod 2", -1 ],
    [ "5 mod -2", 1 ],
    [ "5 mod 2", 1 ],
    [ "ceiling( 3.1415 )", 4.0 ],
    [ "floor( 3.1415 )", 3.0 ],
    [ "ceiling( -3.1415 )", -3.0 ],
    /* string expressions */
    [ "substring('12345', -42, 1 div 0)", "12345" ],
    [ "normalize-space( '  qwerty ' )", "qwerty" ],
    [ "contains('1234567890','9')", true ],
    [ "contains('1234567890','1')", true ],
    [ "'Hello World!'", 'Hello World!' ],
    [ "substring('12345', 1.5, 2.6)", "234" ],
    [ "substring('12345', 0, 3)", "12" ],
    /* string expressions (Japanese) */
    [ "substring('\u3042\u3044\u3046\u3048\u304a', -42, 1 div 0)",
      "\u3042\u3044\u3046\u3048\u304a" ],
    [ "normalize-space( '  \u3044\u308d\u306f\u306b\u307b\u3078\u3068 ' )",
      "\u3044\u308d\u306f\u306b\u307b\u3078\u3068" ],
    [ "contains('\u5357\u7121\u5999\u6cd5\u9023\u83ef\u7d4c','\u7d4c')",
      true ],
    [ "contains('\u5357\u7121\u5999\u6cd5\u9023\u83ef\u7d4c','\u5357')",
      true ],
    [ "'\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c\uff01'",
      '\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c\uff01' ],
    [ "substring('\uff11\uff12\uff13\uff14\uff15', 1.5, 2.6)",
      "\uff12\uff13\uff14" ],
    [ "substring('\uff11\uff12\uff13\uff14\uff15', 0, 3)",
      "\uff11\uff12" ],
    /* selenium bug SEL-347, AJAXSLT issue 19 */
    [ "count(//a[@href=\"javascript:doFoo('a', 'b')\"])", 1 ],
    /* variables */
    [ "$foo", 'bar', { foo: 'bar' } ],
    [ "$foo", 100, { foo: 100 } ],
    [ "$foo", true, { foo: true } ],
    [ "$foo + 1", 101, { foo: 100 } ],
    /* variables (Japanese) */
    [ "$\u307b\u3052", '\u307b\u3048', { \u307b\u3052: '\u307b\u3048' } ],
    [ "$\u307b\u3052", 100, { \u307b\u3052: 100 } ],
    [ "$\u307b\u3052", true, { \u307b\u3052: true } ],
    [ "$\u307b\u3052 + 1", 101, { \u307b\u3052: 100 } ],
    /* functions */
    // function id() with string argument
    [ "count(id('test1'))", 1 ],
    // function id() with node-set argument
    [ "count(id(//*[@id='testid']))", 1 ],
    /* union expressions */
    [ "count(//*[@id='u1'])", 1 ],
    [ "count(//*[@class='u'])", 3 ],
    [ "count(//*[@id='u1']|//*[@id='u2'])", 2 ],
    [ "count(//*[@id='u1']|//*[@class='u'])", 3 ],
    [ "count(//*[@class='u']|//*[@class='u'])", 3 ],
    [ "count(//*[@class='u']|//*[@id='u1'])", 3 ]
];

function testEval() {
  for (var i = 0; i < numExpr.length; ++i) {
    var ctx = new ExprContext(document.body);
    var e = numExpr[i];
    if (e[2]) {
      for (var k in e[2]) {
        var v = e[2][k];
        if (typeof v == 'number') {
          ctx.setVariable(k, new NumberValue(v));
        } else if (typeof v == 'string') {
          ctx.setVariable(k, new StringValue(v));
        } else if (typeof v == 'boolean') {
          ctx.setVariable(k, new BooleanValue(v));
        }
      }
    }
    var result = xpathParse(e[0]).evaluate(ctx);
    if (typeof e[1] == 'number') {
      assertEquals(e[0], e[1], result.numberValue());
    } else if (typeof e[1] == 'string') {
      assertEquals(e[0], e[1], result.stringValue());
    } else if (typeof e[1] == 'boolean') {
      assertEquals(e[0], e[1], result.booleanValue());
    }
  }
}

// For the following axis expressions, we need full control over the
// entire document, so we cannot evaluate them against document.body,
// but use our own XML document here. We verify that they give the
// right results by counting the nodes in their result node sets. For
// the axes that contain only one node, we check that we found the
// right node using the id. For axes that contain elements, we only
// count the elements, so we don't have to worry about whitespace
// normalization for the text nodes.
var axisTests = [
    [ "count(//*[@id='self']/ancestor-or-self::*)", 3 ],
    [ "count(//*[@id='self']/ancestor::*)", 2 ],
    [ "count(//*[@id='self']/attribute::node())", 1 ],
    [ "count(//*[@id='self']/child::*)", 1 ],
    [ "count(//*[@id='self']/descendant-or-self::*)", 3 ],
    [ "count(//*[@id='self']/descendant::*)", 2 ],
    [ "count(//*[@id='self']/following-sibling::*)", 3 ],
    [ "count(//*[@id='self']/@*/following-sibling::*)", 0 ],
    [ "count(//*[@id='self']/following::*)", 4 ],
    [ "//*[@id='self']/parent::*/@id", "parent" ],
    [ "count(/parent::*)", 0 ],
    [ "count(//*[@id='self']/preceding-sibling::*)", 1 ],
    [ "count(//*[@id='self']/@*/preceding-sibling::*)", 0 ],
    [ "count(//*[@id='self']/preceding::*)", 2 ],
    [ "//*[@id='self']/self::*/@id", "self" ]
];

function testAxes() {
  var xml = [
      '<page>',
      ' <p></p>',
      ' <list id="parent">',
      '  <item></item>',
      '  <item id="self"><d><d></d></d></item>',
      '  <item></item>',
      '  <item></item>',
      '  <item></item>',
      ' </list>',
      ' <f></f>',
      '</page>'
  ].join("");
  var ctx = new ExprContext(xmlParse(xml));
  for (var i = 0; i < axisTests.length; ++i) {
    var e = axisTests[i];
    var result = xpathParse(e[0]).evaluate(ctx);
    if (typeof e[1] == 'number') {
      assertEquals(e[0], e[1], result.numberValue());
    } else if (typeof e[1] == 'string') {
      assertEquals(e[0], e[1], result.stringValue());
    } else if (typeof e[1] == 'boolean') {
      assertEquals(e[0], e[1], result.booleanValue());
    }
  }
}


function testAttributeAsterisk() {
  var ctx = new ExprContext(xmlParse('<x a="1" b="1"><y><z></z></y></x>'));
  var expr = xpathParse("count(/x/@*)");
  assertEquals(2, expr.evaluate(ctx).numberValue());
}


// eval an xpath expression to a single node
function evalNodeSet(expr, ctx) {
  var expr1 = xpathParse(expr);
  var e = expr1.evaluate(ctx);
  return e.nodeSetValue();
}

function testEvalDom() {
  var xml = [
      '<page>',
      '<request>',
      '<q>new york</q>',
      '</request>',
      '<location lat="100" lon="200"/>',
      '</page>'
  ].join('');

  doTestEvalDom(xml, 'page', 'location', 'lat', '100', 'lon', '200');
}

function testEvalDomJapanese() {
  var xml = [
      '<\u30da\u30fc\u30b8>',
      '<\u30ea\u30af\u30a8\u30b9\u30c8>',
      '<\u30af\u30a8\u30ea>\u6771\u4eac</\u30af\u30a8\u30ea>',
      '</\u30ea\u30af\u30a8\u30b9\u30c8>',
      '<\u4f4d\u7f6e \u7def\u5ea6="\u4e09\u5341\u4e94" ',
      "\u7d4c\u5ea6='\u767e\u56db\u5341'/>",
      '</\u30da\u30fc\u30b8>'
  ].join('');

  doTestEvalDom(xml, '\u30da\u30fc\u30b8', '\u4f4d\u7f6e',
                '\u7def\u5ea6', '\u4e09\u5341\u4e94',
                '\u7d4c\u5ea6', '\u767e\u56db\u5341');
}

function doTestEvalDom(xml, page, location, lat, latValue, lon, lonValue) {
  var slashPage = '/' + page;
  var slashPageLocationAtLat = '/' + page + '/' + location + '/@' + lat;
  var slashPageLocationAtLon = '/' + page + '/' + location + '/@' + lon;

  var ctx = new ExprContext(xmlParse(xml));
  // DGF if we have access to an official DOMParser, compare output with that also
  var ctx1;
  if (typeof(DOMParser) != 'undefined') {
    ctx1 = new ExprContext((new DOMParser).parseFromString(xml, 'text/xml'));
  } else {
    ctx1 = ctx;
  }

  var ns = evalNodeSet(page, ctx);
  assertEquals(page, ns.length, 1);

  ns = evalNodeSet(page, ctx1);
  assertEquals(page, ns.length, 1);

  ns = evalNodeSet(slashPage, ctx);
  assertEquals(slashPage, ns.length, 1);

  ns = evalNodeSet(slashPage, ctx1);
  assertEquals(slashPage, ns.length, 1);

  assertEquals('/', evalNodeSet('/', ctx).length, 1);
  assertEquals('/', evalNodeSet('/', ctx1).length, 1);

  assertEquals('/', evalNodeSet('/', ctx)[0].nodeName, '#document');
  assertEquals('/', evalNodeSet('/', ctx1)[0].nodeName, '#document');

  assertEquals(slashPage, evalNodeSet(slashPage, ctx)[0].nodeName, page);
  assertEquals(slashPage, evalNodeSet(slashPage, ctx1)[0].nodeName, page);

  var n = evalNodeSet(slashPageLocationAtLat, ctx)[0];
  assertEquals(slashPageLocationAtLat, n.nodeName, lat);
  assertEquals(slashPageLocationAtLat, n.nodeValue, latValue);

  n = evalNodeSet(slashPageLocationAtLat, ctx1)[0];
  assertEquals(slashPageLocationAtLat, n.nodeName, lat);
  assertEquals(slashPageLocationAtLat, n.nodeValue, latValue);

  var n = evalNodeSet(slashPageLocationAtLon, ctx)[0];
  assertEquals(slashPageLocationAtLon, n.nodeName, lon);
  assertEquals(slashPageLocationAtLon, n.nodeValue, lonValue);

  n = evalNodeSet(slashPageLocationAtLon, ctx1)[0];
  assertEquals(slashPageLocationAtLon, n.nodeName, lon);
  assertEquals(slashPageLocationAtLon, n.nodeValue, lonValue);
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Software Developer (Senior)
United States United States
I have extensive experience developing software on both Linux and Windows in C++ and Python. I have also done a lot of work in the C#/.NET ecosystem. I currently work in the fields of robotics and machine learning, and also have a strong background in business automation/rules engines.

Comments and Discussions