Module: Glib::JsonUi::JsonLogicLint

Defined in:
app/helpers/glib/json_ui/json_logic_lint.rb

Constant Summary collapse

EQUALITY_OPS =
%w[== != === !== setEq in].freeze

Class Method Summary collapse

Class Method Details

.check_comparison(op, operands) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 39

def self.check_comparison(op, operands)
  return unless operands.any? { |operand| form_field_var?(operand) }

  operands.each do |operand|
    next unless non_string_constant?(operand)

    field = operands.find { |o| form_field_var?(o) }
    raise "JsonLogic rule compares form field #{var_name(field).inspect} against " \
          "non-string constant #{operand.inspect} (`#{op}`).\n" \
          'Why strings: rules never read the field spec (which may legitimately carry ' \
          'integers); a `var` reads the LIVE form data, which the client collects from ' \
          'DOM inputs -- and DOM input values are always strings (arrays of strings for ' \
          "multi-value fields). The comparison would rely on JS coercion or fail outright.\n" \
          'Fix: serialize the constant as a string, e.g. `record.id.to_s`, arrays via ' \
          '`.map(&:to_s)`. For array comparisons, prefer the `setEq` operator, which ' \
          'string-normalizes both sides.'
  end
end

.form_field_var?(operand) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 58

def self.form_field_var?(operand)
  operand.is_a?(Hash) && (name = var_name(operand)) && name.include?('[')
end

.non_string_constant?(operand) ⇒ Boolean

What matters is the JSON wire type, not the Ruby type: symbols and dates serialize to JSON strings and compare fine, but numerics and booleans stay typed in JSON while form values are always strings.

Returns:

  • (Boolean)


70
71
72
73
74
75
76
77
78
79
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 70

def self.non_string_constant?(operand)
  case operand
  when Numeric, TrueClass, FalseClass
    true
  when Array
    operand.flatten.any? { |element| non_string_constant?(element) }
  else
    false
  end
end

.validate!(rule) ⇒ Object



23
24
25
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 23

def self.validate!(rule)
  walk(rule)
end

.var_name(operand) ⇒ Object



62
63
64
65
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 62

def self.var_name(operand)
  value = operand['var'] || operand[:var]
  value&.to_s
end

.walk(node) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
# File 'app/helpers/glib/json_ui/json_logic_lint.rb', line 27

def self.walk(node)
  case node
  when Array
    node.each { |child| walk(child) }
  when Hash
    node.each do |op, operands|
      check_comparison(op.to_s, operands) if EQUALITY_OPS.include?(op.to_s) && operands.is_a?(Array)
      walk(operands)
    end
  end
end