From 61dabdb44a334c87bb528cd91d619f3a98c3e410 Mon Sep 17 00:00:00 2001 From: James Mason Date: Tue, 10 May 2022 17:26:57 -0700 Subject: [PATCH 1/2] Prevent unknown operations from calling arbitrary methods --- lib/json_logic.rb | 4 +- lib/json_logic/operation.rb | 13 +- test/json_logic_test.rb | 16 +- test/tests.json | 523 ++++++++++++++++++++++++++++++++++++ 4 files changed, 545 insertions(+), 11 deletions(-) create mode 100644 test/tests.json diff --git a/lib/json_logic.rb b/lib/json_logic.rb index 3bb256b..762de38 100644 --- a/lib/json_logic.rb +++ b/lib/json_logic.rb @@ -61,9 +61,7 @@ def self.filter(logic, data) end def self.add_operation(operator, function) - Operation.class.send(:define_method, operator) do |v, d| - function.call(v, d) - end + Operation.add_operation(operator, function) end end diff --git a/lib/json_logic/operation.rb b/lib/json_logic/operation.rb index 5cec5b6..079a223 100644 --- a/lib/json_logic/operation.rb +++ b/lib/json_logic/operation.rb @@ -114,6 +114,8 @@ class Operation 'log' => ->(v, d) { puts v } } + CUSTOM_LAMBDAS = {} + def self.interpolated_block(block, data) # Make sure the empty var is there to be used in iterator JSONLogic.apply(block, data.is_a?(Hash) ? data.merge({"": data}) : { "": data }) @@ -132,13 +134,18 @@ def self.perform(operator, values, data) interpolated.flatten!(1) if interpolated.size == 1 # [['A']] => ['A'] return LAMBDAS[operator.to_s].call(interpolated, data) if is_standard?(operator) - send(operator, interpolated, data) + return CUSTOM_LAMBDAS[operator.to_s].call(interpolated, data) if is_custom?(operator) + raise ArgumentError, "Unknown operator #{operator}" end def self.is_standard?(operator) LAMBDAS.key?(operator.to_s) end + def self.is_custom?(operator) + CUSTOM_LAMBDAS.key?(operator.to_s) + end + # Determine if values associated with operator need to be re-interpreted for each iteration(ie some kind of iterator) # or if values can just be evaluated before passing in. def self.is_iterable?(operator) @@ -146,9 +153,7 @@ def self.is_iterable?(operator) end def self.add_operation(operator, function) - self.class.send(:define_method, operator) do |v, d| - function.call(v, d) - end + CUSTOM_LAMBDAS[operator.to_s] = function end end end diff --git a/test/json_logic_test.rb b/test/json_logic_test.rb index 2f76271..1df09be 100644 --- a/test/json_logic_test.rb +++ b/test/json_logic_test.rb @@ -7,8 +7,13 @@ require 'json_logic' class JSONLogicTest < Minitest::Test - test_suite_url = 'http://jsonlogic.com/tests.json' - tests = JSON.parse(open(test_suite_url).read) + test_suite_url = 'https://jsonlogic.com/tests.json' + tests = begin + JSON.parse(open(test_suite_url).read) + rescue Errno::ENOENT + # Run a cached copy of the test suite if we can't reach the canonical version + JSON.parse(File.read(File.join(File.dirname(__FILE__), 'tests.json'))) + end count = 1 tests.each do |pattern| next unless pattern.is_a?(Array) @@ -44,10 +49,13 @@ def test_false_value end def test_add_operation - new_operation = ->(v, d) { v.map { |x| x + 5 } } - JSONLogic.add_operation('fives', new_operation) rules = JSON.parse(%Q|{"fives": {"var": "num"}}|) data = JSON.parse(%Q|{"num": 1}|) + assert_raises(ArgumentError, "Unknown operator fives") do + JSONLogic.apply(rules, data) + end + new_operation = ->(v, d) { v.map { |x| x + 5 } } + JSONLogic.add_operation('fives', new_operation) assert_equal([6], JSONLogic.apply(rules, data)) end diff --git a/test/tests.json b/test/tests.json new file mode 100644 index 0000000..0bbe1c7 --- /dev/null +++ b/test/tests.json @@ -0,0 +1,523 @@ +[ + "# Non-rules get passed through", + [ true, {}, true ], + [ false, {}, false ], + [ 17, {}, 17 ], + [ 3.14, {}, 3.14 ], + [ "apple", {}, "apple" ], + [ null, {}, null ], + [ ["a","b"], {}, ["a","b"] ], + + "# Single operator tests", + [ {"==":[1,1]}, {}, true ], + [ {"==":[1,"1"]}, {}, true ], + [ {"==":[1,2]}, {}, false ], + [ {"===":[1,1]}, {}, true ], + [ {"===":[1,"1"]}, {}, false ], + [ {"===":[1,2]}, {}, false ], + [ {"!=":[1,2]}, {}, true ], + [ {"!=":[1,1]}, {}, false ], + [ {"!=":[1,"1"]}, {}, false ], + [ {"!==":[1,2]}, {}, true ], + [ {"!==":[1,1]}, {}, false ], + [ {"!==":[1,"1"]}, {}, true ], + [ {">":[2,1]}, {}, true ], + [ {">":[1,1]}, {}, false ], + [ {">":[1,2]}, {}, false ], + [ {">":["2",1]}, {}, true ], + [ {">=":[2,1]}, {}, true ], + [ {">=":[1,1]}, {}, true ], + [ {">=":[1,2]}, {}, false ], + [ {">=":["2",1]}, {}, true ], + [ {"<":[2,1]}, {}, false ], + [ {"<":[1,1]}, {}, false ], + [ {"<":[1,2]}, {}, true ], + [ {"<":["1",2]}, {}, true ], + [ {"<":[1,2,3]}, {}, true ], + [ {"<":[1,1,3]}, {}, false ], + [ {"<":[1,4,3]}, {}, false ], + [ {"<=":[2,1]}, {}, false ], + [ {"<=":[1,1]}, {}, true ], + [ {"<=":[1,2]}, {}, true ], + [ {"<=":["1",2]}, {}, true ], + [ {"<=":[1,2,3]}, {}, true ], + [ {"<=":[1,4,3]}, {}, false ], + [ {"!":[false]}, {}, true ], + [ {"!":false}, {}, true ], + [ {"!":[true]}, {}, false ], + [ {"!":true}, {}, false ], + [ {"!":0}, {}, true ], + [ {"!":1}, {}, false ], + [ {"or":[true,true]}, {}, true ], + [ {"or":[false,true]}, {}, true ], + [ {"or":[true,false]}, {}, true ], + [ {"or":[false,false]}, {}, false ], + [ {"or":[false,false,true]}, {}, true ], + [ {"or":[false,false,false]}, {}, false ], + [ {"or":[false]}, {}, false ], + [ {"or":[true]}, {}, true ], + [ {"or":[1,3]}, {}, 1 ], + [ {"or":[3,false]}, {}, 3 ], + [ {"or":[false,3]}, {}, 3 ], + [ {"and":[true,true]}, {}, true ], + [ {"and":[false,true]}, {}, false ], + [ {"and":[true,false]}, {}, false ], + [ {"and":[false,false]}, {}, false ], + [ {"and":[true,true,true]}, {}, true ], + [ {"and":[true,true,false]}, {}, false ], + [ {"and":[false]}, {}, false ], + [ {"and":[true]}, {}, true ], + [ {"and":[1,3]}, {}, 3 ], + [ {"and":[3,false]}, {}, false ], + [ {"and":[false,3]}, {}, false ], + [ {"?:":[true,1,2]}, {}, 1 ], + [ {"?:":[false,1,2]}, {}, 2 ], + [ {"in":["Bart",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, true ], + [ {"in":["Milhouse",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, false ], + [ {"in":["Spring","Springfield"]}, {}, true ], + [ {"in":["i","team"]}, {}, false ], + [ {"cat":"ice"}, {}, "ice" ], + [ {"cat":["ice"]}, {}, "ice" ], + [ {"cat":["ice","cream"]}, {}, "icecream" ], + [ {"cat":[1,2]}, {}, "12" ], + [ {"cat":["Robocop",2]}, {}, "Robocop2" ], + [ {"cat":["we all scream for ","ice","cream"]}, {}, "we all scream for icecream" ], + [ {"%":[1,2]}, {}, 1 ], + [ {"%":[2,2]}, {}, 0 ], + [ {"%":[3,2]}, {}, 1 ], + [ {"max":[1,2,3]}, {}, 3 ], + [ {"max":[1,3,3]}, {}, 3 ], + [ {"max":[3,2,1]}, {}, 3 ], + [ {"max":[1]}, {}, 1 ], + [ {"min":[1,2,3]}, {}, 1 ], + [ {"min":[1,1,3]}, {}, 1 ], + [ {"min":[3,2,1]}, {}, 1 ], + [ {"min":[1]}, {}, 1 ], + + [ {"+":[1,2]}, {}, 3 ], + [ {"+":[2,2,2]}, {}, 6 ], + [ {"+":[1]}, {}, 1 ], + [ {"+":["1",1]}, {}, 2 ], + [ {"*":[3,2]}, {}, 6 ], + [ {"*":[2,2,2]}, {}, 8 ], + [ {"*":[1]}, {}, 1 ], + [ {"*":["1",1]}, {}, 1 ], + [ {"-":[2,3]}, {}, -1 ], + [ {"-":[3,2]}, {}, 1 ], + [ {"-":[3]}, {}, -3 ], + [ {"-":["1",1]}, {}, 0 ], + [ {"/":[4,2]}, {}, 2 ], + [ {"/":[2,4]}, {}, 0.5 ], + [ {"/":["1",1]}, {}, 1 ], + + "Substring", + [{"substr":["jsonlogic", 4]}, null, "logic"], + [{"substr":["jsonlogic", -5]}, null, "logic"], + [{"substr":["jsonlogic", 0, 1]}, null, "j"], + [{"substr":["jsonlogic", -1, 1]}, null, "c"], + [{"substr":["jsonlogic", 4, 5]}, null, "logic"], + [{"substr":["jsonlogic", -5, 5]}, null, "logic"], + [{"substr":["jsonlogic", -5, -2]}, null, "log"], + [{"substr":["jsonlogic", 1, -5]}, null, "son"], + + "Merge arrays", + [{"merge":[]}, null, []], + [{"merge":[[1]]}, null, [1]], + [{"merge":[[1],[]]}, null, [1]], + [{"merge":[[1], [2]]}, null, [1,2]], + [{"merge":[[1], [2], [3]]}, null, [1,2,3]], + [{"merge":[[1, 2], [3]]}, null, [1,2,3]], + [{"merge":[[1], [2, 3]]}, null, [1,2,3]], + "Given non-array arguments, merge converts them to arrays", + [{"merge":1}, null, [1]], + [{"merge":[1,2]}, null, [1,2]], + [{"merge":[1,[2]]}, null, [1,2]], + + "Too few args", + [{"if":[]}, null, null], + [{"if":[true]}, null, true], + [{"if":[false]}, null, false], + [{"if":["apple"]}, null, "apple"], + + "Simple if/then/else cases", + [{"if":[true, "apple"]}, null, "apple"], + [{"if":[false, "apple"]}, null, null], + [{"if":[true, "apple", "banana"]}, null, "apple"], + [{"if":[false, "apple", "banana"]}, null, "banana"], + + "Empty arrays are falsey", + [{"if":[ [], "apple", "banana"]}, null, "banana"], + [{"if":[ [1], "apple", "banana"]}, null, "apple"], + [{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"], + + "Empty strings are falsey, all other strings are truthy", + [{"if":[ "", "apple", "banana"]}, null, "banana"], + [{"if":[ "zucchini", "apple", "banana"]}, null, "apple"], + [{"if":[ "0", "apple", "banana"]}, null, "apple"], + + "You can cast a string to numeric with a unary + ", + [{"===":[0,"0"]}, null, false], + [{"===":[0,{"+":"0"}]}, null, true], + [{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"], + [{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"], + + "Zero is falsy, all other numbers are truthy", + [{"if":[ 0, "apple", "banana"]}, null, "banana"], + [{"if":[ 1, "apple", "banana"]}, null, "apple"], + [{"if":[ 3.1416, "apple", "banana"]}, null, "apple"], + [{"if":[ -1, "apple", "banana"]}, null, "apple"], + + "Truthy and falsy definitions matter in Boolean operations", + [{"!" : [ [] ]}, {}, true], + [{"!!" : [ [] ]}, {}, false], + [{"and" : [ [], true ]}, {}, [] ], + [{"or" : [ [], true ]}, {}, true ], + + [{"!" : [ 0 ]}, {}, true], + [{"!!" : [ 0 ]}, {}, false], + [{"and" : [ 0, true ]}, {}, 0 ], + [{"or" : [ 0, true ]}, {}, true ], + + [{"!" : [ "" ]}, {}, true], + [{"!!" : [ "" ]}, {}, false], + [{"and" : [ "", true ]}, {}, "" ], + [{"or" : [ "", true ]}, {}, true ], + + [{"!" : [ "0" ]}, {}, false], + [{"!!" : [ "0" ]}, {}, true], + [{"and" : [ "0", true ]}, {}, true ], + [{"or" : [ "0", true ]}, {}, "0" ], + + "If the conditional is logic, it gets evaluated", + [{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"], + [{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"], + + "If the consequents are logic, they get evaluated", + [{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"], + [{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"], + + "If/then/elseif/then cases", + [{"if":[true, "apple", true, "banana"]}, null, "apple"], + [{"if":[true, "apple", false, "banana"]}, null, "apple"], + [{"if":[false, "apple", true, "banana"]}, null, "banana"], + [{"if":[false, "apple", false, "banana"]}, null, null], + + [{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"], + [{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"], + [{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"], + [{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"], + + [{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null], + [{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"], + [{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"], + [{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"], + [{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"], + [{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"], + [{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"], + [{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"], + [{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"], + + "Arrays with logic", + [[1, {"var": "x"}, 3], {"x": 2}, [1, 2, 3]], + [{"if": [{"var": "x"}, [{"var": "y"}], 99]}, {"x": true, "y": 42}, [42]], + + "# Compound Tests", + [ {"and":[{">":[3,1]},true]}, {}, true ], + [ {"and":[{">":[3,1]},false]}, {}, false ], + [ {"and":[{">":[3,1]},{"!":true}]}, {}, false ], + [ {"and":[{">":[3,1]},{"<":[1,3]}]}, {}, true ], + [ {"?:":[{">":[3,1]},"visible","hidden"]}, {}, "visible" ], + + "# Data-Driven", + [ {"var":["a"]},{"a":1},1 ], + [ {"var":["b"]},{"a":1},null ], + [ {"var":["a"]},null,null ], + [ {"var":"a"},{"a":1},1 ], + [ {"var":"b"},{"a":1},null ], + [ {"var":"a"},null,null ], + [ {"var":["a", 1]},null,1 ], + [ {"var":["b", 2]},{"a":1},2 ], + [ {"var":"a.b"},{"a":{"b":"c"}},"c" ], + [ {"var":"a.q"},{"a":{"b":"c"}},null ], + [ {"var":["a.q", 9]},{"a":{"b":"c"}},9 ], + [ {"var":1}, ["apple","banana"], "banana" ], + [ {"var":"1"}, ["apple","banana"], "banana" ], + [ {"var":"1.1"}, ["apple",["banana","beer"]], "beer" ], + [ {"and":[{"<":[{"var":"temp"},110]},{"==":[{"var":"pie.filling"},"apple"]}]},{"temp":100,"pie":{"filling":"apple"}},true ], + [ {"var":[{"?:":[{"<":[{"var":"temp"},110]},"pie.filling","pie.eta"]}]},{"temp":100,"pie":{"filling":"apple","eta":"60s"}},"apple" ], + [ {"in":[{"var":"filling"},["apple","cherry"]]},{"filling":"apple"},true ], + [ {"var":"a.b.c"}, null, null ], + [ {"var":"a.b.c"}, {"a":null}, null ], + [ {"var":"a.b.c"}, {"a":{"b":null}}, null ], + [ {"var":""}, 1, 1 ], + [ {"var":null}, 1, 1 ], + [ {"var":[]}, 1, 1 ], + + "Missing", + [{"missing":[]}, null, []], + [{"missing":["a"]}, null, ["a"]], + [{"missing":"a"}, null, ["a"]], + [{"missing":"a"}, {"a":"apple"}, []], + [{"missing":["a"]}, {"a":"apple"}, []], + [{"missing":["a","b"]}, {"a":"apple"}, ["b"]], + [{"missing":["a","b"]}, {"b":"banana"}, ["a"]], + [{"missing":["a","b"]}, {"a":"apple", "b":"banana"}, []], + [{"missing":["a","b"]}, {}, ["a","b"]], + [{"missing":["a","b"]}, null, ["a","b"]], + + [{"missing":["a.b"]}, null, ["a.b"]], + [{"missing":["a.b"]}, {"a":"apple"}, ["a.b"]], + [{"missing":["a.b"]}, {"a":{"c":"apple cake"}}, ["a.b"]], + [{"missing":["a.b"]}, {"a":{"b":"apple brownie"}}, []], + [{"missing":["a.b", "a.c"]}, {"a":{"b":"apple brownie"}}, ["a.c"]], + + + "Missing some", + [{"missing_some":[1, ["a", "b"]]}, {"a":"apple"}, [] ], + [{"missing_some":[1, ["a", "b"]]}, {"b":"banana"}, [] ], + [{"missing_some":[1, ["a", "b"]]}, {"a":"apple", "b":"banana"}, [] ], + [{"missing_some":[1, ["a", "b"]]}, {"c":"carrot"}, ["a", "b"]], + + [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana"}, [] ], + [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "c":"carrot"}, [] ], + [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana", "c":"carrot"}, [] ], + [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "d":"durian"}, ["b", "c"] ], + [{"missing_some":[2, ["a", "b", "c"]]}, {"d":"durian", "e":"eggplant"}, ["a", "b", "c"] ], + + + "Missing and If are friends, because empty arrays are falsey in JsonLogic", + [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"a":"apple"}, "found it"], + [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"b":"banana"}, "missed it"], + + "Missing, Merge, and If are friends. VIN is always required, APR is only required if financing is true.", + [ + {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, + {"financing":true}, + ["vin","apr"] + ], + + [ + {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, + {"financing":false}, + ["vin"] + ], + + "Filter, map, all, none, and some", + [ + {"filter":[{"var":"integers"}, true]}, + {"integers":[1,2,3]}, + [1,2,3] + ], + [ + {"filter":[{"var":"integers"}, false]}, + {"integers":[1,2,3]}, + [] + ], + [ + {"filter":[{"var":"integers"}, {">=":[{"var":""},2]}]}, + {"integers":[1,2,3]}, + [2,3] + ], + [ + {"filter":[{"var":"integers"}, {"%":[{"var":""},2]}]}, + {"integers":[1,2,3]}, + [1,3] + ], + + [ + {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, + {"integers":[1,2,3]}, + [2,4,6] + ], + [ + {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, + null, + [] + ], + [ + {"map":[{"var":"desserts"}, {"var":"qty"}]}, + {"desserts":[ + {"name":"apple","qty":1}, + {"name":"brownie","qty":2}, + {"name":"cupcake","qty":3} + ]}, + [1,2,3] + ], + + [ + {"reduce":[ + {"var":"integers"}, + {"+":[{"var":"current"}, {"var":"accumulator"}]}, + 0 + ]}, + {"integers":[1,2,3,4]}, + 10 + ], + [ + {"reduce":[ + {"var":"integers"}, + {"+":[{"var":"current"}, {"var":"accumulator"}]}, + 0 + ]}, + null, + 0 + ], + [ + {"reduce":[ + {"var":"integers"}, + {"*":[{"var":"current"}, {"var":"accumulator"}]}, + 1 + ]}, + {"integers":[1,2,3,4]}, + 24 + ], + [ + {"reduce":[ + {"var":"integers"}, + {"*":[{"var":"current"}, {"var":"accumulator"}]}, + 0 + ]}, + {"integers":[1,2,3,4]}, + 0 + ], + [ + {"reduce": [ + {"var":"desserts"}, + {"+":[ {"var":"accumulator"}, {"var":"current.qty"}]}, + 0 + ]}, + {"desserts":[ + {"name":"apple","qty":1}, + {"name":"brownie","qty":2}, + {"name":"cupcake","qty":3} + ]}, + 6 + ], + + + [ + {"all":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + true + ], + [ + {"all":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + false + ], + [ + {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + false + ], + [ + {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[]}, + false + ], + [ + {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + true + ], + [ + {"all":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + false + ], + [ + {"all":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + false + ], + [ + {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[]}, + false + ], + + + [ + {"none":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + false + ], + [ + {"none":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + false + ], + [ + {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + true + ], + [ + {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[]}, + true + ], + [ + {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + false + ], + [ + {"none":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + false + ], + [ + {"none":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + true + ], + [ + {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[]}, + true + ], + + [ + {"some":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + true + ], + [ + {"some":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + true + ], + [ + {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[1,2,3]}, + false + ], + [ + {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, + {"integers":[]}, + false + ], + [ + {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + true + ], + [ + {"some":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + true + ], + [ + {"some":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, + {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, + false + ], + [ + {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, + {"items":[]}, + false + ], + + "EOF" +] From f91ad059738dd2582396f4aa6d79e3791f0c3860 Mon Sep 17 00:00:00 2001 From: James Mason Date: Thu, 30 Jun 2022 11:20:41 -0700 Subject: [PATCH 2/2] Handle "in" operations on nil fields --- lib/json_logic/operation.rb | 2 +- test/json_logic_test.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/json_logic/operation.rb b/lib/json_logic/operation.rb index 079a223..0c025e0 100644 --- a/lib/json_logic/operation.rb +++ b/lib/json_logic/operation.rb @@ -109,7 +109,7 @@ class Operation '%' => ->(v, d) { v.map(&:to_i).reduce(:%) }, '^' => ->(v, d) { v.map(&:to_f).reduce(:**) }, 'merge' => ->(v, d) { v.flatten }, - 'in' => ->(v, d) { interpolated_block(v[1], d).include? v[0] }, + 'in' => ->(v, d) { !interpolated_block(v[1], d).nil? && interpolated_block(v[1], d).include?(v[0]) }, 'cat' => ->(v, d) { v.map(&:to_s).join }, 'log' => ->(v, d) { puts v } } diff --git a/test/json_logic_test.rb b/test/json_logic_test.rb index 1df09be..2c0655f 100644 --- a/test/json_logic_test.rb +++ b/test/json_logic_test.rb @@ -107,6 +107,18 @@ def test_in_with_variable ) end + def test_in_with_null + assert_equal false, JSONLogic.apply( + { + "in" => [ + {"var" => "x"}, + {"var" => "y"}, + ] + }, + { "x" => "foo", "y" => nil } + ) + end + def test_filter_with_non_array assert_equal [], JSONLogic.apply( {