Module: XeroKiwi::Query::Filter

Defined in:
lib/xero_kiwi/query/filter.rb

Overview

Compiles a Ruby-native filter into Xero’s ‘where` expression syntax.

XeroKiwi::Query::Filter.compile(
  { status: "AUTHORISED", date: Date.new(2026, 1, 1)..Date.new(2026, 4, 1) },
  fields: Invoice.query_fields
)
# => 'Status=="AUTHORISED"&&Date>=DateTime(2026,1,1)&&Date<=DateTime(2026,4,1)'

Supported input shapes:

nil                → nil
String             → passthrough (raw escape hatch)
Hash               → walked; each pair becomes `Path==Literal`
Array value        → IN-semantics `(Path==x || Path==y)`
Range value        → `Path>=lo && Path<=hi`
Hash value         → nested `Parent.Child==Literal`, using the child's
                     query_fields schema

Unknown field keys raise ArgumentError so typos surface immediately.

Class Method Summary collapse

Class Method Details

.compile(where, fields:) ⇒ Object

Raises:

  • (ArgumentError)


30
31
32
33
34
35
36
37
# File 'lib/xero_kiwi/query/filter.rb', line 30

def compile(where, fields:)
  return nil if where.nil?
  return where if where.is_a?(String)

  raise ArgumentError, "where must be a Hash or String, got #{where.class}" unless where.is_a?(Hash)

  compile_hash(where, fields, nil)
end

.compile_hash(hash, fields, prefix) ⇒ Object



39
40
41
# File 'lib/xero_kiwi/query/filter.rb', line 39

def compile_hash(hash, fields, prefix)
  hash.map { |key, value| compile_pair(key, value, fields, prefix) }.join("&&")
end

.compile_pair(key, value, fields, prefix) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/xero_kiwi/query/filter.rb', line 43

def compile_pair(key, value, fields, prefix) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
  spec = fields.fetch(key) do
    raise ArgumentError, "unknown filter field: #{key.inspect}"
  end

  path = prefix ? "#{prefix}.#{spec[:path]}" : spec[:path]

  case value
  when Array
    "(#{value.map { |v| "#{path}==#{literal(v, spec[:type])}" }.join("||")})"
  when Range
    "#{path}>=#{literal(value.begin, spec[:type])}&&#{path}<=#{literal(value.end, spec[:type])}"
  when Hash
    raise ArgumentError, "#{key.inspect} is not a nested filter field" unless spec[:type] == :nested

    compile_hash(value, spec[:fields], path)
  else
    "#{path}==#{literal(value, spec[:type])}"
  end
end

.date_literal(value) ⇒ Object

Date → render as-is (a Date IS a day, no timezone shifting). Time → convert to UTC before extracting Y/M/D so ‘Time.new(…, “+10:00”)`

doesn't render the local-timezone day.


83
84
85
86
87
88
89
90
# File 'lib/xero_kiwi/query/filter.rb', line 83

def date_literal(value)
  case value
  when Date then "DateTime(#{value.year},#{value.month},#{value.day})"
  when Time then t = value.utc
                 "DateTime(#{t.year},#{t.month},#{t.day})"
  else           raise ArgumentError, "cannot render #{value.class} as :date"
  end
end

.literal(value, type) ⇒ Object



64
65
66
67
68
69
70
71
72
73
# File 'lib/xero_kiwi/query/filter.rb', line 64

def literal(value, type)
  case type
  when :guid then %(Guid("#{value}"))
  when :string, :enum then quote_string(value)
  when :date         then date_literal(value)
  when :bool         then value ? "true" : "false"
  when :decimal      then value.to_s
  else                    raise ArgumentError, "cannot render #{value.inspect} as #{type.inspect}"
  end
end

.quote_string(value) ⇒ Object



75
76
77
78
# File 'lib/xero_kiwi/query/filter.rb', line 75

def quote_string(value)
  escaped = value.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
  %("#{escaped}")
end