Skip to content

Commit 7eaa377

Browse files
committed
Use custom class to handle autocomplete loading
1 parent 8392e76 commit 7eaa377

11 files changed

+268
-8
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
/spec/reports/
1111
/tmp/
1212
vendor
13+
.byebug_history

Guardfile

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
guard :rspec, cmd: "bundle exec rspec --color --format documentation" do
2+
require "guard/rspec/dsl"
3+
dsl = Guard::RSpec::Dsl.new(self)
4+
watch(%r{^spec/(.*)\/?(.*)_spec\.rb$})
5+
6+
# Feel free to open issues for suggestions and improvements
7+
8+
# RSpec files
9+
rspec = dsl.rspec
10+
watch(rspec.spec_helper) { rspec.spec_dir }
11+
watch(rspec.spec_support) { rspec.spec_dir }
12+
watch(rspec.spec_files)
13+
14+
# Ruby files
15+
ruby = dsl.ruby
16+
dsl.watch_spec_files_for(ruby.lib_files)
17+
end

lib/trample.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
require "trample/version"
44
require "trample/aggregation"
55
require "trample/serializable"
6+
require "trample/lookup_not_found"
67
require "trample/condition"
78
require "trample/condition_proxy"
89
require "trample/metadata"
10+
require "trample/text_lookup"
911
require "trample/search"
1012
require "trample/backend/searchkick"
1113
require "trample/results"

lib/trample/condition.rb

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
module Trample
22
class Condition
33
include Virtus.model
4-
54
attribute :name, Symbol
65
attribute :query_name, Symbol, default: :name
76
attribute :values, Array
@@ -20,12 +19,32 @@ class Condition
2019
attribute :fields, Array
2120
attribute :user_query, Hash
2221
attribute :transform, Proc, default: ->(_,_) { ->(val) { val } }
22+
attribute :search_klass
23+
attribute :lookup, Hash
2324

2425
def initialize(attrs)
2526
attrs.merge!(single: true) if attrs[:name] == :keywords
2627
super(attrs)
2728
end
2829

30+
def lookup_autocomplete
31+
if require_autocomplete_lookup?
32+
options = (lookup || {}).dup
33+
klass = options.delete(:klass) || '::Trample::TextLookup'
34+
35+
options.assert_valid_keys(:key, :label)
36+
37+
options = options.merge({
38+
search_klass: search_klass,
39+
condition_name: name
40+
})
41+
42+
lookup_instance = klass.to_s.constantize.new(options)
43+
44+
self.values = lookup_instance.load(values)
45+
end
46+
end
47+
2948
def blank?
3049
values.reject { |v| v == "" || v.nil? }.empty? && !range?
3150
end
@@ -204,5 +223,10 @@ def to_range_query
204223

205224
{ runtime_query_name => hash }
206225
end
226+
227+
def require_autocomplete_lookup?
228+
(values.present? && values.first.is_a?(Hash)) &&
229+
values.any? { |v| v[:text].blank? }
230+
end
207231
end
208232
end

lib/trample/lookup_not_found.rb

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module Trample
2+
class LookupNotFoundError < StandardError
3+
4+
attr_reader :condition, :lookup_results, :corresponding_lookup
5+
6+
def initialize(condition, lookup_results, corresponding_lookup)
7+
@condition = condition
8+
@lookup_results = lookup_results
9+
@corresponding_lookup = corresponding_lookup
10+
super(message)
11+
end
12+
13+
def message
14+
msg = <<-MSG.strip_heredoc
15+
No corresponding lookup found for condition #{condition.name}
16+
17+
Corresponding Lookup Result: #{ corresponding_lookup }
18+
19+
Criteria Keys: #{ criteria_keys.join(", ") }
20+
Lookup Result Keys: #{ lookup_keys.join(", ") }
21+
Missing: #{ diff.join(", ") }
22+
MSG
23+
end
24+
25+
def criteria_keys
26+
@criteria_keys ||= Array(condition.values).map { |l| l[:key] }.sort
27+
end
28+
29+
def lookup_keys
30+
@lookup_keys ||= Array(lookup_results).map {|l| l[:key] }.sort
31+
end
32+
33+
def diff
34+
criteria_keys - lookup_keys
35+
end
36+
end
37+
end

lib/trample/search.rb

+9-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def self.inherited(klass)
2323
end
2424

2525
def self.condition(name, attrs = {})
26-
attrs.merge!(name: name)
26+
attrs.merge!(name: name, search_klass: self)
27+
2728
@_conditions[name] = Condition.new(attrs)
2829
end
2930

@@ -133,9 +134,12 @@ def backend
133134
@backend ||= Backend::Searchkick.new(metadata, self.class._models)
134135
end
135136

136-
def query!
137+
def query!(options = { lookup: true })
137138
@records = nil
138139
hash = backend.query!(conditions, aggregations)
140+
141+
load_autocompletes if options[:lookup]
142+
139143
self.metadata.took = hash[:took]
140144
self.metadata.scroll_id = hash[:scroll_id]
141145
self.metadata.pagination.total = hash[:total]
@@ -191,5 +195,8 @@ def deep_dup(o)
191195
Marshal.load(Marshal.dump(o))
192196
end
193197

198+
def load_autocompletes
199+
self.conditions.values.each(&:lookup_autocomplete)
200+
end
194201
end
195202
end

lib/trample/text_lookup.rb

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Trample
2+
class TextLookup
3+
include Virtus.model
4+
attribute :search_klass
5+
attribute :condition_name, Symbol
6+
attribute :key, Symbol, default: :id
7+
attribute :label, Symbol, default: :text
8+
9+
def load(values)
10+
search = search_klass.new
11+
search.condition(condition_name).in(values)
12+
search.query!(lookup: false)
13+
14+
values.map do | value |
15+
result = find_corresponding_value(value, search.results)
16+
value[:text] = result[label]
17+
value
18+
end
19+
end
20+
21+
private
22+
23+
def find_corresponding_value(value, results)
24+
results.find { |result| value[:key].to_s == result[key].to_s }
25+
end
26+
end
27+
end

spec/condition_spec.rb

+75-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,81 @@
55
let(:attrs) { {} }
66
let(:instance) { described_class.new(attrs) }
77

8+
class Myklass
9+
include Virtus.model
10+
def load(values)
11+
[]
12+
end
13+
end
14+
15+
describe "#lookup_autocomplete" do
16+
context "with custom default lookup klass" do
17+
before do
18+
attrs[:lookup] = { key: :test_id, label: :test_label, klass: :Myklass }
19+
end
20+
it "should load autocomplete labels using custom klass" do
21+
results = [{key: 2, text: "two"}, {key: 4, text: "four"}]
22+
expect(Myklass).to receive(:new).and_return(double(load: results))
23+
instance.values = [{key: 2}, {key: 4}]
24+
instance.lookup_autocomplete
25+
26+
expect(instance.values).to eql(results)
27+
end
28+
end
29+
30+
context "with default lookup klass" do
31+
before do
32+
attrs[:lookup] = { key: :test_id, label: :test_label }
33+
end
34+
context "when all values has missing text key" do
35+
context "when all labels are missing" do
36+
it "should load autocomplete labels" do
37+
results = [{key: 2, text: "two"}, {key: 4, text: "four"}]
38+
expect(Trample::TextLookup).to receive(:new).and_return(double(load: results))
39+
instance.values = [{key: 2}, {key: 4}]
40+
instance.lookup_autocomplete
41+
42+
expect(instance.values).to eql(results)
43+
end
44+
end
45+
46+
context "when any labels are missing" do
47+
it "should load autocomplete labels" do
48+
results = [{key: 2, text: "two"}, {key: 4, text: "four"}]
49+
expect(Trample::TextLookup).to receive(:new).and_return(double(load: results))
50+
instance.values = [{key: 2, text: "two"}, {key: 4}]
51+
instance.lookup_autocomplete
52+
53+
expect(instance.values).to eql(results)
54+
end
55+
end
56+
57+
context "when all labels are present" do
58+
it "should not load autocomplete labels" do
59+
expect(Trample::TextLookup).not_to receive(:new)
60+
instance.values = [{key: 2, text: "two"}, {key: 4, text: "four"}]
61+
instance.lookup_autocomplete
62+
end
63+
end
64+
65+
context "when condition is not autocomplete type" do
66+
it "should not load autocomplete labels" do
67+
expect(Trample::TextLookup).not_to receive(:new)
68+
instance.values = ['test']
69+
instance.lookup_autocomplete
70+
end
71+
end
72+
73+
context "when condition value is blank" do
74+
it "should not load autocomplete labels" do
75+
expect(Trample::TextLookup).not_to receive(:new)
76+
instance.lookup_autocomplete
77+
end
78+
end
79+
end
80+
end
81+
end
82+
883
describe "#as_json" do
984
context "when single-value" do
1085
before do
@@ -45,5 +120,4 @@
45120
end
46121
end
47122
end
48-
49123
end

spec/integration/search_spec.rb

+53-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
require 'spec_helper'
22

33
RSpec.describe "searching", elasticsearch: true do
4+
class MyLookup
5+
include Virtus.model
6+
7+
def load(values)
8+
values.map do | value |
9+
value[:text] = "#{value[:key]} - test"
10+
value
11+
end
12+
end
13+
end
414

515
let(:klass) do
616
Class.new(Trample::Search) do
@@ -14,20 +24,59 @@
1424
condition :_name_prefix, query_name: :name, prefix: true
1525

1626
condition :simple_name, query_name: 'name', single: true
27+
condition :company_ids, query_name: 'company_id',
28+
lookup: {
29+
key: :company_id,
30+
label: :company_name
31+
}
32+
condition :company_custom_ids, query_name: 'company_id',
33+
lookup: {
34+
key: :company_id,
35+
label: :company_name,
36+
klass: 'MyLookup'
37+
}
1738
end
1839
end
1940

2041
before do
2142
Searchkick.client.indices.delete index: '_all'
22-
Person.create!(name: 'Homer', tags: ['funny', 'stupid', 'bald'], age: 38)
23-
Person.create!(name: 'Lisa', tags: ['funny', 'smart', 'kid'], age: 8)
24-
Person.create!(name: 'Marge', tags: ['motherly'], age: 34)
25-
Person.create!(name: 'Bart', tags: ['funny', 'stupid', 'kid'], age: 10)
43+
foo = Company.create!(name: 'Foo Inc')
44+
bar = Company.create!(name: 'Bar Inc')
45+
apex = Company.create!(name: 'Apex Inc')
46+
Person.create!(name: 'Homer', tags: ['funny', 'stupid', 'bald'], age: 38, company: foo)
47+
Person.create!(name: 'Lisa', tags: ['funny', 'smart', 'kid'], age: 8, company: bar)
48+
Person.create!(name: 'Marge', tags: ['motherly'], age: 34, company: foo)
49+
Person.create!(name: 'Bart', tags: ['funny', 'stupid', 'kid'], age: 10, company: apex)
2650
Person.reindex
2751

2852
klass.model(Person)
2953
end
3054

55+
it "should fetch autocomplete labels by default" do
56+
search = klass.new
57+
search.condition(:company_ids).in([{key: Company.first.id.to_s}, { key: Company.last.id.to_s, text: Company.last.name }])
58+
search.query!
59+
60+
expect(search.conditions.as_json['company_ids'][:values]).to eq([{:key=>"1", :text=>"Foo Inc"}, {:key=>"3", :text=>"Apex Inc"}])
61+
end
62+
63+
it "should not fetch autocomplete labels when lookup: false" do
64+
search = klass.new
65+
values = [{key: Company.first.id.to_s}, { key: Company.last.id.to_s, text: Company.last.name }]
66+
search.condition(:company_ids).in(values)
67+
search.query!(lookup: false)
68+
69+
expect(search.conditions.as_json['company_ids'][:values]).to eq([{:key=>"1"}, {:key=>"3", :text=>"Apex Inc"}])
70+
end
71+
72+
it "should fetch autocomplete labels using custom lookup_class" do
73+
search = klass.new
74+
search.condition(:company_custom_ids).in([{key: Company.first.id.to_s}, { key: Company.last.id.to_s, text: Company.last.name }])
75+
search.query!
76+
77+
expect(search.conditions.as_json['company_custom_ids'][:values]).to eq([{:key=>"1", :text=>"#{Company.first.id} - test"}, {:key=>"3", :text=> "#{Company.last.id} - test"}])
78+
end
79+
3180
it "records time the search took" do
3281
search = klass.new
3382
search.query!

0 commit comments

Comments
 (0)