From f6bd70c6166ba5a44df7754897dcb8f84f13e8f6 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 19:27:54 -0600 Subject: [PATCH 01/12] make Query API surface area smaller. --- lib/reso_transport/query.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index c46df8b..55864e3 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -90,6 +90,8 @@ def next_link=(link) @next_link = link end + private + def response use_next_link? ? resource.get_next_link_results(next_link) : resource.get(compile_params) rescue Faraday::ConnectionFailed @@ -144,16 +146,16 @@ def compile_filters filter_chunks = [] - filter_chunks << global[:criteria].join(" #{global[:context]} ") if global && global[:criteria]&.any? + filter_chunks << global.criteria.join(" #{global[:context]} ") if global && global.criteria.any? filter_chunks << filter_groups.map do |g| - "(#{g[:criteria].join(" #{g[:context]} ")})" + "(#{g.criteria.join(" #{g[:context]} ")})" end.join(' and ') filter_chunks.reject { |c| c == '' }.join(' and ') end - def compile_params + public def compile_params params = {} options.each_pair do |k, v| From 02b284a41ff027cb74b39d3a72ccf0f987de139c Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 19:28:18 -0600 Subject: [PATCH 02/12] extract SubQuery object. --- lib/reso_transport/query.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 55864e3..60935cb 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -124,7 +124,7 @@ def clear_query_context def current_query_context @current_query_context ||= nil - sub_queries[@current_query_context || :global][:criteria] + sub_queries[@current_query_context || :global].criteria end def options @@ -136,7 +136,13 @@ def query_parameters end def sub_queries - @sub_queries ||= Hash.new { |h, k| h[k] = { context: 'and', criteria: [] } } + @sub_queries ||= Hash.new { |h, k| h[k] = SubQuery.new("and") } + end + + SubQuery = Struct.new(:context) do + def criteria + @criteria ||= [] + end end def compile_filters From 472098e6d6540682a90f4e61f5bef31706647f0c Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 19:32:55 -0600 Subject: [PATCH 03/12] extract #to_s method and move it to SubQuery. --- lib/reso_transport/query.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 60935cb..189e9c1 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -114,7 +114,7 @@ def handle_response(response) def new_query_context(context) @last_query_context ||= 0 @current_query_context = @last_query_context + 1 - sub_queries[@current_query_context][:context] = context + sub_queries[@current_query_context].context = context end def clear_query_context @@ -139,10 +139,14 @@ def sub_queries @sub_queries ||= Hash.new { |h, k| h[k] = SubQuery.new("and") } end - SubQuery = Struct.new(:context) do + SubQuery = Struct.new(:context, :criteria) do def criteria @criteria ||= [] end + + def to_s + criteria.join(" #{context} ") + end end def compile_filters @@ -152,10 +156,10 @@ def compile_filters filter_chunks = [] - filter_chunks << global.criteria.join(" #{global[:context]} ") if global && global.criteria.any? + filter_chunks << global.to_s if global && global.criteria.any? filter_chunks << filter_groups.map do |g| - "(#{g.criteria.join(" #{g[:context]} ")})" + "(#{g})" end.join(' and ') filter_chunks.reject { |c| c == '' }.join(' and ') From 10870c1b47cbb819d97605f905bc6830b6bd46dc Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 20:50:12 -0600 Subject: [PATCH 04/12] use an array instead of a hash to build queries. --- lib/reso_transport/query.rb | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 189e9c1..28d9b93 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -114,7 +114,7 @@ def handle_response(response) def new_query_context(context) @last_query_context ||= 0 @current_query_context = @last_query_context + 1 - sub_queries[@current_query_context].context = context + sub_queries[@current_query_context] = SubQuery.new(context) end def clear_query_context @@ -124,7 +124,7 @@ def clear_query_context def current_query_context @current_query_context ||= nil - sub_queries[@current_query_context || :global].criteria + sub_queries[@current_query_context || 0].criteria end def options @@ -136,7 +136,7 @@ def query_parameters end def sub_queries - @sub_queries ||= Hash.new { |h, k| h[k] = SubQuery.new("and") } + @sub_queries ||= [SubQuery.new("and")] end SubQuery = Struct.new(:context, :criteria) do @@ -147,16 +147,19 @@ def criteria def to_s criteria.join(" #{context} ") end + + def present? + criteria.any? + end end def compile_filters - groups = sub_queries.dup - global = groups.delete(:global) - filter_groups = groups.values + filter_groups = sub_queries.dup + global = filter_groups.shift filter_chunks = [] - filter_chunks << global.to_s if global && global.criteria.any? + filter_chunks << global.to_s if global.present? filter_chunks << filter_groups.map do |g| "(#{g})" @@ -172,7 +175,7 @@ def compile_filters params["$#{k}"] = v end - params['$filter'] = compile_filters unless sub_queries.empty? + params['$filter'] = compile_filters if sub_queries.any?(&:present?) params.merge!(query_parameters) unless query_parameters.empty? params From bd1ca4b730734c646b5ec86a58dd999247f31f1b Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 20:55:08 -0600 Subject: [PATCH 05/12] move paren handling into SubQuery. --- lib/reso_transport/query.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 28d9b93..a138e4d 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -114,7 +114,7 @@ def handle_response(response) def new_query_context(context) @last_query_context ||= 0 @current_query_context = @last_query_context + 1 - sub_queries[@current_query_context] = SubQuery.new(context) + sub_queries[@current_query_context] = SubQuery.new(context, parens: true) end def clear_query_context @@ -139,13 +139,20 @@ def sub_queries @sub_queries ||= [SubQuery.new("and")] end - SubQuery = Struct.new(:context, :criteria) do - def criteria - @criteria ||= [] + class SubQuery + def initialize context, parens: false + @context = context + @parens = parens + @criteria = [] end + attr_reader :context, :parens, :criteria + alias_method :parens?, :parens + def to_s - criteria.join(" #{context} ") + out = criteria.join(" #{context} ") + out = "(#{out})" if parens? + out end def present? @@ -158,13 +165,8 @@ def compile_filters global = filter_groups.shift filter_chunks = [] - filter_chunks << global.to_s if global.present? - - filter_chunks << filter_groups.map do |g| - "(#{g})" - end.join(' and ') - + filter_chunks << filter_groups.map(&:to_s).join(' and ') filter_chunks.reject { |c| c == '' }.join(' and ') end From 5a061aa8f3199b2a4c6a4dd90f9289c5468de8ba Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:32:59 -0600 Subject: [PATCH 06/12] use SubQuery to build full query. --- lib/reso_transport/query.rb | 45 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index a138e4d..4824863 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -17,7 +17,7 @@ def any(&block) %i[eq ne gt ge lt le].each do |op| define_method(op) do |conditions| conditions.each_pair do |k, v| - current_query_context << "#{k} #{op} #{encode_value(k, v)}" + current_query_context.criteria << "#{k} #{op} #{encode_value(k, v)}" end return self end @@ -111,6 +111,18 @@ def handle_response(response) parsed end + def options + @options ||= {} + end + + def query_parameters + @query_parameters ||= {} + end + + def sub_queries + @sub_queries ||= [SubQuery.new("and")] + end + def new_query_context(context) @last_query_context ||= 0 @current_query_context = @last_query_context + 1 @@ -123,20 +135,7 @@ def clear_query_context end def current_query_context - @current_query_context ||= nil - sub_queries[@current_query_context || 0].criteria - end - - def options - @options ||= {} - end - - def query_parameters - @query_parameters ||= {} - end - - def sub_queries - @sub_queries ||= [SubQuery.new("and")] + sub_queries[@current_query_context || 0] end class SubQuery @@ -150,13 +149,17 @@ def initialize context, parens: false alias_method :parens?, :parens def to_s - out = criteria.join(" #{context} ") + out = criteria.select { |x| x.length > 0 }.map(&:to_s).join(" #{context} ") out = "(#{out})" if parens? out end + def length + criteria.length + end + def present? - criteria.any? + length > 0 end end @@ -164,10 +167,10 @@ def compile_filters filter_groups = sub_queries.dup global = filter_groups.shift - filter_chunks = [] - filter_chunks << global.to_s if global.present? - filter_chunks << filter_groups.map(&:to_s).join(' and ') - filter_chunks.reject { |c| c == '' }.join(' and ') + query = SubQuery.new("and") + query.criteria << global + query.criteria << filter_groups.map(&:to_s).join(' and ') + query.to_s end public def compile_params From 9b5323c124c0a11f64625de0f8dd2b97d17c8dd7 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:35:08 -0600 Subject: [PATCH 07/12] simpler deconstruction. --- lib/reso_transport/query.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 4824863..8bd1626 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -164,9 +164,7 @@ def present? end def compile_filters - filter_groups = sub_queries.dup - global = filter_groups.shift - + global, *filter_groups = sub_queries query = SubQuery.new("and") query.criteria << global query.criteria << filter_groups.map(&:to_s).join(' and ') From 4a23c6de6dc85bc639e097c0db152438a0b2b643 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:36:47 -0600 Subject: [PATCH 08/12] begin nesting sub_queries. --- lib/reso_transport/query.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 8bd1626..8e187a9 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -139,10 +139,10 @@ def current_query_context end class SubQuery - def initialize context, parens: false + def initialize context, criteria=[], parens: false @context = context @parens = parens - @criteria = [] + @criteria = criteria end attr_reader :context, :parens, :criteria @@ -165,10 +165,10 @@ def present? def compile_filters global, *filter_groups = sub_queries - query = SubQuery.new("and") - query.criteria << global - query.criteria << filter_groups.map(&:to_s).join(' and ') - query.to_s + SubQuery.new("and", [ + global, + SubQuery.new("and", filter_groups), + ]).to_s end public def compile_params From d82463ce9dd31b2d16c5ee09dcde78b69882598d Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:41:37 -0600 Subject: [PATCH 09/12] clarify. --- lib/reso_transport/query.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 8e187a9..e695f20 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -17,7 +17,7 @@ def any(&block) %i[eq ne gt ge lt le].each do |op| define_method(op) do |conditions| conditions.each_pair do |k, v| - current_query_context.criteria << "#{k} #{op} #{encode_value(k, v)}" + current_query_context.push "#{k} #{op} #{encode_value(k, v)}" end return self end @@ -124,18 +124,18 @@ def sub_queries end def new_query_context(context) - @last_query_context ||= 0 - @current_query_context = @last_query_context + 1 - sub_queries[@current_query_context] = SubQuery.new(context, parens: true) + @last_query_context_index ||= 0 + @current_query_context_index = @last_query_context_index + 1 + sub_queries[@current_query_context_index] = SubQuery.new(context, parens: true) end def clear_query_context - @last_query_context = @current_query_context - @current_query_context = nil + @last_query_context_index = @current_query_context_index + @current_query_context_index = nil end def current_query_context - sub_queries[@current_query_context || 0] + sub_queries[@current_query_context_index || 0] end class SubQuery @@ -154,6 +154,11 @@ def to_s out end + def push x + criteria << x + end + alias_method :<<, :push + def length criteria.length end From 08ae9b305c8bdde15599210d59d050c99e7c9dab Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:44:17 -0600 Subject: [PATCH 10/12] this is actually equivalent! --- lib/reso_transport/query.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index e695f20..80af2a7 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -169,11 +169,7 @@ def present? end def compile_filters - global, *filter_groups = sub_queries - SubQuery.new("and", [ - global, - SubQuery.new("and", filter_groups), - ]).to_s + SubQuery.new("and", sub_queries).to_s end public def compile_params From 6223c3213bb66f63c30066ca3927ed740ede4c79 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 8 Jan 2024 22:49:38 -0600 Subject: [PATCH 11/12] leverage new nesting capability to construct subquery tree. --- lib/reso_transport/query.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/reso_transport/query.rb b/lib/reso_transport/query.rb index 80af2a7..37c697c 100644 --- a/lib/reso_transport/query.rb +++ b/lib/reso_transport/query.rb @@ -124,18 +124,17 @@ def sub_queries end def new_query_context(context) - @last_query_context_index ||= 0 - @current_query_context_index = @last_query_context_index + 1 - sub_queries[@current_query_context_index] = SubQuery.new(context, parens: true) + sub_query = SubQuery.new(context, parens: true) + current_query_context.push sub_query + sub_queries.push sub_query end def clear_query_context - @last_query_context_index = @current_query_context_index - @current_query_context_index = nil + sub_queries.pop end def current_query_context - sub_queries[@current_query_context_index || 0] + sub_queries.last end class SubQuery @@ -169,7 +168,7 @@ def present? end def compile_filters - SubQuery.new("and", sub_queries).to_s + sub_queries.first.to_s end public def compile_params From 7f2ece84196858f1a800b02a4a28798301b1c67d Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 9 Jan 2024 14:01:42 -0600 Subject: [PATCH 12/12] test deeply tested queries. --- test/reso_transport/query_test.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/reso_transport/query_test.rb b/test/reso_transport/query_test.rb index 172dd26..602b224 100644 --- a/test/reso_transport/query_test.rb +++ b/test/reso_transport/query_test.rb @@ -107,4 +107,31 @@ def test_filters expected = { '$filter' => "ListPrice eq 1000 and (City eq 'Brea' or City eq 'Yorba Linda')" } assert_equal expected, sample end + + def test_nested_filters + expected = { '$filter' => <<~QUERY.gsub("\n", " ").strip } + (City eq 'Brea' or City eq 'Yorba Linda') and + ((MlsStatus eq 'Active' or MlsStatus eq 'Pending') or + (ListOfficeMlsId eq 'Heinz' and (MlsStatus eq 'Sold' or MlsStatus eq 'SoldNotListed'))) + QUERY + + sample = query.any { + eq(City: "Brea") + eq(City: "Yorba Linda") + }.any { + any { + eq(MlsStatus: "Active") + eq(MlsStatus: "Pending") + } + all { + eq(ListOfficeMlsId: "Heinz") + any { + eq(MlsStatus: "Sold") + eq(MlsStatus: "SoldNotListed") + } + } + }.compile_params + + assert_equal expected, sample + end end