tire-erez 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +14 -0
- data/.travis.yml +32 -0
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +775 -0
- data/Rakefile +51 -0
- data/examples/rails-application-template.rb +263 -0
- data/examples/tire-dsl.rb +932 -0
- data/lib/tire.rb +59 -0
- data/lib/tire/alias.rb +296 -0
- data/lib/tire/configuration.rb +38 -0
- data/lib/tire/count.rb +85 -0
- data/lib/tire/dsl.rb +114 -0
- data/lib/tire/http/client.rb +62 -0
- data/lib/tire/http/clients/curb.rb +61 -0
- data/lib/tire/http/clients/faraday.rb +71 -0
- data/lib/tire/http/response.rb +27 -0
- data/lib/tire/index.rb +443 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +27 -0
- data/lib/tire/model/indexing.rb +134 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +72 -0
- data/lib/tire/model/persistence/attributes.rb +148 -0
- data/lib/tire/model/persistence/finders.rb +54 -0
- data/lib/tire/model/persistence/storage.rb +77 -0
- data/lib/tire/model/search.rb +322 -0
- data/lib/tire/multi_search.rb +263 -0
- data/lib/tire/results/collection.rb +156 -0
- data/lib/tire/results/item.rb +94 -0
- data/lib/tire/results/pagination.rb +68 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +1 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/rubyext/uri_escape.rb +74 -0
- data/lib/tire/search.rb +211 -0
- data/lib/tire/search/facet.rb +81 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/queries/match.rb +40 -0
- data/lib/tire/search/query.rb +250 -0
- data/lib/tire/search/scan.rb +114 -0
- data/lib/tire/search/script_field.rb +23 -0
- data/lib/tire/search/sort.rb +25 -0
- data/lib/tire/tasks.rb +138 -0
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +18 -0
- data/test/fixtures/articles/1.json +1 -0
- data/test/fixtures/articles/2.json +1 -0
- data/test/fixtures/articles/3.json +1 -0
- data/test/fixtures/articles/4.json +1 -0
- data/test/fixtures/articles/5.json +1 -0
- data/test/integration/active_model_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +620 -0
- data/test/integration/boolean_queries_test.rb +43 -0
- data/test/integration/boosting_queries_test.rb +32 -0
- data/test/integration/bulk_test.rb +86 -0
- data/test/integration/count_test.rb +64 -0
- data/test/integration/custom_score_queries_test.rb +89 -0
- data/test/integration/dis_max_queries_test.rb +68 -0
- data/test/integration/dsl_search_test.rb +30 -0
- data/test/integration/explanation_test.rb +44 -0
- data/test/integration/facets_test.rb +311 -0
- data/test/integration/filtered_queries_test.rb +66 -0
- data/test/integration/filters_test.rb +75 -0
- data/test/integration/fuzzy_queries_test.rb +20 -0
- data/test/integration/highlight_test.rb +64 -0
- data/test/integration/index_aliases_test.rb +122 -0
- data/test/integration/index_mapping_test.rb +43 -0
- data/test/integration/index_store_test.rb +112 -0
- data/test/integration/index_update_document_test.rb +121 -0
- data/test/integration/match_query_test.rb +79 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/multi_search_test.rb +114 -0
- data/test/integration/nested_query_test.rb +135 -0
- data/test/integration/percolator_test.rb +111 -0
- data/test/integration/persistent_model_test.rb +205 -0
- data/test/integration/prefix_query_test.rb +43 -0
- data/test/integration/query_return_version_test.rb +70 -0
- data/test/integration/query_string_test.rb +52 -0
- data/test/integration/range_queries_test.rb +36 -0
- data/test/integration/reindex_test.rb +56 -0
- data/test/integration/results_test.rb +58 -0
- data/test/integration/scan_test.rb +56 -0
- data/test/integration/script_fields_test.rb +38 -0
- data/test/integration/sort_test.rb +52 -0
- data/test/integration/text_query_test.rb +39 -0
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +131 -0
- data/test/models/article.rb +15 -0
- data/test/models/mongoid_models.rb +85 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_index.rb +16 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +12 -0
- data/test/models/persistent_article_with_percolation.rb +5 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +118 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/configuration_test.rb +84 -0
- data/test/unit/count_test.rb +67 -0
- data/test/unit/http_client_test.rb +79 -0
- data/test/unit/http_response_test.rb +49 -0
- data/test/unit/index_alias_test.rb +335 -0
- data/test/unit/index_test.rb +1098 -0
- data/test/unit/logger_test.rb +125 -0
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +75 -0
- data/test/unit/model_initialization_test.rb +31 -0
- data/test/unit/model_persistence_test.rb +548 -0
- data/test/unit/model_search_test.rb +964 -0
- data/test/unit/multi_search_test.rb +304 -0
- data/test/unit/results_collection_test.rb +372 -0
- data/test/unit/results_item_test.rb +173 -0
- data/test/unit/rubyext_test.rb +66 -0
- data/test/unit/search_facet_test.rb +186 -0
- data/test/unit/search_filter_test.rb +42 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +419 -0
- data/test/unit/search_scan_test.rb +113 -0
- data/test/unit/search_script_field_test.rb +26 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +556 -0
- data/test/unit/tire_test.rb +144 -0
- data/tire.gemspec +83 -0
- metadata +586 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
module Tire
|
2
|
+
module Search
|
3
|
+
|
4
|
+
module Multi
|
5
|
+
|
6
|
+
# Wraps the search definitions for Tire::Multi::Search
|
7
|
+
#
|
8
|
+
class SearchDefinitions
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
attr_reader :names
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@names = []
|
15
|
+
@searches = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def << value
|
19
|
+
@names << value[:name]
|
20
|
+
@searches << value[:search]
|
21
|
+
end
|
22
|
+
|
23
|
+
def [] name
|
24
|
+
@searches[ @names.index(name) ]
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
@searches.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
@searches.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
@searches
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Wraps the search result sets for Tire::Multi::Search
|
41
|
+
#
|
42
|
+
class Results
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
def initialize(searches, results)
|
46
|
+
@searches = searches
|
47
|
+
@results = results
|
48
|
+
@collection = @results.zip(@searches.to_a).map do |results, search|
|
49
|
+
Tire::Results::Collection.new(results, search.options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a specific result sets
|
54
|
+
def [] name
|
55
|
+
if index = @searches.names.index(name)
|
56
|
+
@collection[ index ]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def each(&block)
|
61
|
+
@collection.each(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def each_pair(&block)
|
65
|
+
@searches.names.zip(@collection).each(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def size
|
69
|
+
@results.size
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the multi-search result sets as a Hash with the search name
|
73
|
+
# as key and the results as value.
|
74
|
+
#
|
75
|
+
def to_hash
|
76
|
+
result = {}
|
77
|
+
each_pair { |name,results| result[name] = results }
|
78
|
+
result
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html)
|
83
|
+
# request.
|
84
|
+
#
|
85
|
+
# s = Tire::Search::Multi::Search.new 'my-index' do
|
86
|
+
# search :names do
|
87
|
+
# query { match :name, 'john' }
|
88
|
+
# end
|
89
|
+
# search :counts, search_type: 'count' do
|
90
|
+
# query { match :_all, 'john' }
|
91
|
+
# end
|
92
|
+
# search :other, index: 'other-index' do
|
93
|
+
# query { string "first_name:john" }
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# You can optionally pass an index and type to the constructor, using them as defaults
|
98
|
+
# for searches which don't define them.
|
99
|
+
#
|
100
|
+
# Use the {#search} method to add a search definition to the request, passing it search options
|
101
|
+
# as a Hash and the search definition itself using Tire's DSL.
|
102
|
+
#
|
103
|
+
class Search
|
104
|
+
|
105
|
+
attr_reader :indices, :types, :path
|
106
|
+
|
107
|
+
def initialize(indices=nil, options={}, &block)
|
108
|
+
@indices = Array(indices)
|
109
|
+
@types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
|
110
|
+
@options = options
|
111
|
+
@path = ['/', @indices.join(','), @types.join(','), '_msearch'].compact.join('/').squeeze('/')
|
112
|
+
@searches = Tire::Search::Multi::SearchDefinitions.new
|
113
|
+
|
114
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add a search definition to the multi-search request.
|
118
|
+
#
|
119
|
+
# The usual search options such as `search_type`, `routing`, etc. can be passed as a Hash,
|
120
|
+
# and the search definition itself should be passed as a block using the Tire DSL.
|
121
|
+
#
|
122
|
+
def search(*args, &block)
|
123
|
+
name_or_options = args.pop
|
124
|
+
|
125
|
+
if name_or_options.is_a?(Hash)
|
126
|
+
options = name_or_options
|
127
|
+
name = args.pop
|
128
|
+
else
|
129
|
+
name = name_or_options
|
130
|
+
end
|
131
|
+
|
132
|
+
name ||= @searches.size
|
133
|
+
options ||= {}
|
134
|
+
indices = options.delete(:index) || options.delete(:indices)
|
135
|
+
|
136
|
+
@searches << { :name => name, :search => Tire::Search::Search.new(indices, options, &block) }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Without argument, returns the collection of search definitions.
|
140
|
+
# With argument, returns a search definition by name or order.
|
141
|
+
#
|
142
|
+
def searches(name=nil)
|
143
|
+
name ? @searches[ name ] : @searches
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns and array of search definition names
|
147
|
+
#
|
148
|
+
def names
|
149
|
+
@searches.names
|
150
|
+
end
|
151
|
+
|
152
|
+
# Serializes the search definitions as an array of JSON definitions
|
153
|
+
#
|
154
|
+
def to_array
|
155
|
+
@searches.map do |search|
|
156
|
+
header = {}
|
157
|
+
header.update(:index => search.indices.join(',')) unless search.indices.empty?
|
158
|
+
header.update(:type => search.types.join(',')) unless search.types.empty?
|
159
|
+
header.update(:search_type => search.options[:search_type]) if search.options[:search_type]
|
160
|
+
header.update(:routing => search.options[:routing]) if search.options[:routing]
|
161
|
+
header.update(:preference => search.options[:preference]) if search.options[:preference]
|
162
|
+
body = search.to_hash
|
163
|
+
[ header, body ]
|
164
|
+
end.flatten
|
165
|
+
end
|
166
|
+
|
167
|
+
# Serializes the search definitions as a multi-line string payload
|
168
|
+
#
|
169
|
+
def to_payload
|
170
|
+
to_array.map { |line| MultiJson.encode(line) }.join("\n") + "\n"
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the request URL
|
174
|
+
#
|
175
|
+
def url
|
176
|
+
[ Configuration.url, @path ].join
|
177
|
+
end
|
178
|
+
|
179
|
+
# Serializes the request URL parameters
|
180
|
+
#
|
181
|
+
def params
|
182
|
+
options = @options.dup
|
183
|
+
options.empty? ? '' : '?' + options.to_param
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns an enumerable collection of result sets.
|
187
|
+
#
|
188
|
+
# You can simply iterate over them:
|
189
|
+
#
|
190
|
+
# search.results.each do |results|
|
191
|
+
# puts results.each.map(&:name)
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# To iterate over named result sets, use the `each_pair` method:
|
195
|
+
#
|
196
|
+
# search.results.each_pair do |name,results|
|
197
|
+
# puts "Search #{name} got #{results.size} results"
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# To get a specific result set:
|
201
|
+
#
|
202
|
+
# search.results[:myname]
|
203
|
+
#
|
204
|
+
def results
|
205
|
+
@results || perform and @results
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the HTTP response
|
209
|
+
#
|
210
|
+
def response
|
211
|
+
@response || perform and @response
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the raw JSON as a Hash
|
215
|
+
#
|
216
|
+
def json
|
217
|
+
@json || perform and @json
|
218
|
+
end
|
219
|
+
|
220
|
+
def perform
|
221
|
+
@responses = Configuration.client.get(url + params, to_payload)
|
222
|
+
if @responses.failure?
|
223
|
+
STDERR.puts "[REQUEST FAILED] #{to_curl}\n"
|
224
|
+
raise SearchRequestFailed, @responses.to_s
|
225
|
+
end
|
226
|
+
@json = MultiJson.decode(@responses.body)
|
227
|
+
@results = Tire::Search::Multi::Results.new @searches, @json['responses']
|
228
|
+
return self
|
229
|
+
ensure
|
230
|
+
logged
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_curl
|
234
|
+
%Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '\n#{to_payload}'|
|
235
|
+
end
|
236
|
+
|
237
|
+
def logged(endpoint='_msearch')
|
238
|
+
if Configuration.logger
|
239
|
+
|
240
|
+
Configuration.logger.log_request endpoint, indices, to_curl
|
241
|
+
|
242
|
+
took = @json['took'] rescue nil
|
243
|
+
code = @response.code rescue nil
|
244
|
+
|
245
|
+
if Configuration.logger.level.to_s == 'debug'
|
246
|
+
body = if @json
|
247
|
+
MultiJson.encode( @json, :pretty => Configuration.pretty)
|
248
|
+
else
|
249
|
+
MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue ''
|
250
|
+
end
|
251
|
+
else
|
252
|
+
body = ''
|
253
|
+
end
|
254
|
+
|
255
|
+
Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
class Collection
|
5
|
+
include Enumerable
|
6
|
+
include Pagination
|
7
|
+
|
8
|
+
attr_reader :time, :total, :options, :facets, :max_score
|
9
|
+
|
10
|
+
def initialize(response, options={})
|
11
|
+
@response = response
|
12
|
+
@options = options
|
13
|
+
@time = response['took'].to_i
|
14
|
+
@total = response['hits']['total'].to_i rescue nil
|
15
|
+
@facets = response['facets']
|
16
|
+
@max_score = response['hits']['max_score'].to_f rescue nil
|
17
|
+
@wrapper = options[:wrapper] || Configuration.wrapper
|
18
|
+
end
|
19
|
+
|
20
|
+
def results
|
21
|
+
return [] if failure?
|
22
|
+
@results ||= begin
|
23
|
+
hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
|
24
|
+
unless @options[:load]
|
25
|
+
__get_results_without_load(hits)
|
26
|
+
else
|
27
|
+
__get_results_with_load(hits)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Iterates over the `results` collection
|
33
|
+
#
|
34
|
+
def each(&block)
|
35
|
+
results.each(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Iterates over the `results` collection and yields
|
39
|
+
# the `result` object (Item or model instance) and the
|
40
|
+
# `hit` -- raw Elasticsearch response parsed as a Hash
|
41
|
+
#
|
42
|
+
def each_with_hit(&block)
|
43
|
+
results.zip(@response['hits']['hits']).each(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def empty?
|
47
|
+
results.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def size
|
51
|
+
results.size
|
52
|
+
end
|
53
|
+
alias :length :size
|
54
|
+
|
55
|
+
def slice(*args)
|
56
|
+
results.slice(*args)
|
57
|
+
end
|
58
|
+
alias :[] :slice
|
59
|
+
|
60
|
+
def to_ary
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def as_json(options=nil)
|
65
|
+
to_a.map { |item| item.as_json(options) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def error
|
69
|
+
@response['error']
|
70
|
+
end
|
71
|
+
|
72
|
+
def success?
|
73
|
+
error.to_s.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
def failure?
|
77
|
+
! success?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Handles _source prefixed fields properly: strips the prefix and converts fields to nested Hashes
|
81
|
+
#
|
82
|
+
def __parse_fields__(fields={})
|
83
|
+
( fields ||= {} ).clone.each_pair do |key,value|
|
84
|
+
next unless key.to_s =~ /_source/ # Skip regular JSON immediately
|
85
|
+
|
86
|
+
keys = key.to_s.split('.').reject { |n| n == '_source' }
|
87
|
+
fields.delete(key)
|
88
|
+
|
89
|
+
result = {}
|
90
|
+
path = []
|
91
|
+
|
92
|
+
keys.each do |name|
|
93
|
+
path << name
|
94
|
+
eval "result[:#{path.join('][:')}] ||= {}"
|
95
|
+
eval "result[:#{path.join('][:')}] = #{value.inspect}" if keys.last == name
|
96
|
+
end
|
97
|
+
fields.update result
|
98
|
+
end
|
99
|
+
fields
|
100
|
+
end
|
101
|
+
|
102
|
+
def __get_results_without_load(hits)
|
103
|
+
if @wrapper == Hash
|
104
|
+
hits
|
105
|
+
else
|
106
|
+
hits.map do |h|
|
107
|
+
document = {}
|
108
|
+
|
109
|
+
# Update the document with content and ID
|
110
|
+
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
|
111
|
+
document.update( {'id' => h['_id']} )
|
112
|
+
|
113
|
+
# Update the document with meta information
|
114
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
115
|
+
|
116
|
+
# Return an instance of the "wrapper" class
|
117
|
+
@wrapper.new(document)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def __get_results_with_load(hits)
|
123
|
+
return [] if hits.empty?
|
124
|
+
|
125
|
+
records = {}
|
126
|
+
@response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
|
127
|
+
raise NoMethodError, "You have tried to eager load the model instances, " +
|
128
|
+
"but Tire cannot find the model class because " +
|
129
|
+
"document has no _type property." unless type
|
130
|
+
|
131
|
+
begin
|
132
|
+
klass = type.camelize.constantize
|
133
|
+
rescue NameError => e
|
134
|
+
raise NameError, "You have tried to eager load the model instances, but " +
|
135
|
+
"Tire cannot find the model class '#{type.camelize}' " +
|
136
|
+
"based on _type '#{type}'.", e.backtrace
|
137
|
+
end
|
138
|
+
|
139
|
+
records[type] = __find_records_by_ids klass, items.map { |h| h['_id'] }
|
140
|
+
end
|
141
|
+
|
142
|
+
# Reorder records to preserve the order from search results
|
143
|
+
@response['hits']['hits'].map do |item|
|
144
|
+
records[item['_type']].detect do |record|
|
145
|
+
record.id.to_s == item['_id'].to_s
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def __find_records_by_ids(klass, ids)
|
151
|
+
@options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
class Item
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
include ActiveModel::Conversion
|
7
|
+
|
8
|
+
# Create new instance, recursively converting all Hashes to Item
|
9
|
+
# and leaving everything else alone.
|
10
|
+
#
|
11
|
+
def initialize(args={})
|
12
|
+
raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair)
|
13
|
+
@attributes = {}
|
14
|
+
args.each_pair do |key, value|
|
15
|
+
if value.is_a?(Array)
|
16
|
+
@attributes[key.to_sym] = value.map { |item| @attributes[key.to_sym] = item.is_a?(Hash) ? Item.new(item.to_hash) : item }
|
17
|
+
else
|
18
|
+
@attributes[key.to_sym] = value.is_a?(Hash) ? Item.new(value.to_hash) : value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Delegate method to a key in underlying hash, if present, otherwise return +nil+.
|
24
|
+
#
|
25
|
+
def method_missing(method_name, *arguments)
|
26
|
+
@attributes[method_name.to_sym]
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to?(method_name, include_private = false)
|
30
|
+
@attributes.has_key?(method_name.to_sym) || super
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](key)
|
34
|
+
@attributes[key.to_sym]
|
35
|
+
end
|
36
|
+
|
37
|
+
def id
|
38
|
+
@attributes[:_id] || @attributes[:id]
|
39
|
+
end
|
40
|
+
|
41
|
+
def type
|
42
|
+
@attributes[:_type] || @attributes[:type]
|
43
|
+
end
|
44
|
+
|
45
|
+
def persisted?
|
46
|
+
!!id
|
47
|
+
end
|
48
|
+
|
49
|
+
def errors
|
50
|
+
ActiveModel::Errors.new(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_key
|
58
|
+
persisted? ? [id] : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_hash
|
62
|
+
@attributes.reduce({}) do |sum, item|
|
63
|
+
sum[ item.first ] = item.last.respond_to?(:to_hash) ? item.last.to_hash : item.last
|
64
|
+
sum
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def as_json(options=nil)
|
69
|
+
hash = to_hash
|
70
|
+
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access.as_json(options) : hash.as_json(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_json(options=nil)
|
74
|
+
as_json.to_json(options)
|
75
|
+
end
|
76
|
+
alias_method :to_indexed_json, :to_json
|
77
|
+
|
78
|
+
# Let's pretend we're someone else in Rails
|
79
|
+
#
|
80
|
+
def class
|
81
|
+
defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super
|
82
|
+
rescue NameError
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" }
|
88
|
+
%Q|<Item#{self.class.to_s == 'Tire::Results::Item' ? '' : " (#{self.class})"} #{s.join(', ')}>|
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|