elasticsearch-model-queryable 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
@@ -0,0 +1,472 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ module Elasticsearch
19
+ module Model
20
+
21
+ # Provides the necessary support to set up index options (mappings, settings)
22
+ # as well as instance methods to create, update or delete documents in the index.
23
+ #
24
+ # @see ClassMethods#settings
25
+ # @see ClassMethods#mapping
26
+ #
27
+ # @see InstanceMethods#index_document
28
+ # @see InstanceMethods#update_document
29
+ # @see InstanceMethods#delete_document
30
+ #
31
+ module Indexing
32
+
33
+ # Wraps the [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html)
34
+ #
35
+ class Settings
36
+ attr_accessor :settings
37
+
38
+ def initialize(settings = {})
39
+ @settings = settings
40
+ end
41
+
42
+ def to_hash
43
+ @settings
44
+ end
45
+
46
+ def as_json(options = {})
47
+ to_hash
48
+ end
49
+ end
50
+
51
+ # Wraps the [index mappings](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
52
+ #
53
+ class Mappings
54
+ attr_accessor :options, :type
55
+
56
+ # @private
57
+ TYPES_WITH_EMBEDDED_PROPERTIES = %w(object nested)
58
+
59
+ def initialize(type = nil, options = {})
60
+ @type = type
61
+ @options = options
62
+ @mapping = {}
63
+ end
64
+
65
+ def indexes(name, options = {}, &block)
66
+ @mapping[name] = options
67
+
68
+ if block_given?
69
+ @mapping[name][:type] ||= "object"
70
+ properties = TYPES_WITH_EMBEDDED_PROPERTIES.include?(@mapping[name][:type].to_s) ? :properties : :fields
71
+
72
+ @mapping[name][properties] ||= {}
73
+
74
+ previous = @mapping
75
+ begin
76
+ @mapping = @mapping[name][properties]
77
+ self.instance_eval(&block)
78
+ ensure
79
+ @mapping = previous
80
+ end
81
+ end
82
+
83
+ # Set the type to `text` by default
84
+ @mapping[name][:type] ||= "text"
85
+
86
+ self
87
+ end
88
+
89
+ def to_hash
90
+ # if @type
91
+ # { @type.to_sym => @options.merge(properties: @mapping) }
92
+ # else
93
+ @options.merge(properties: @mapping)
94
+ # end
95
+ end
96
+
97
+ def as_json(options = {})
98
+ to_hash
99
+ end
100
+ end
101
+
102
+ module ClassMethods
103
+
104
+ # Defines mappings for the index
105
+ #
106
+ # @example Define mapping for model
107
+ #
108
+ # class Article
109
+ # mapping dynamic: 'strict' do
110
+ # indexes :foo do
111
+ # indexes :bar
112
+ # end
113
+ # indexes :baz
114
+ # end
115
+ # end
116
+ #
117
+ # Article.mapping.to_hash
118
+ #
119
+ # # => { :article =>
120
+ # # { :dynamic => "strict",
121
+ # # :properties=>
122
+ # # { :foo => {
123
+ # # :type=>"object",
124
+ # # :properties => {
125
+ # # :bar => { :type => "string" }
126
+ # # }
127
+ # # }
128
+ # # },
129
+ # # :baz => { :type=> "string" }
130
+ # # }
131
+ # # }
132
+ #
133
+ # @example Define index settings and mappings
134
+ #
135
+ # class Article
136
+ # settings number_of_shards: 1 do
137
+ # mappings do
138
+ # indexes :foo
139
+ # end
140
+ # end
141
+ # end
142
+ #
143
+ # @example Call the mapping method directly
144
+ #
145
+ # Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' }
146
+ #
147
+ # Article.mapping.to_hash
148
+ #
149
+ # # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
150
+ #
151
+ # The `mappings` and `settings` methods are accessible directly on the model class,
152
+ # when it doesn't already define them. Use the `__elasticsearch__` proxy otherwise.
153
+ #
154
+ def mapping(options = {}, &block)
155
+ @mapping ||= Mappings.new(document_type, options)
156
+
157
+ @mapping.options.update(options) unless options.empty?
158
+
159
+ if block_given?
160
+ @mapping.instance_eval(&block)
161
+ return self
162
+ else
163
+ @mapping
164
+ end
165
+ end
166
+
167
+ alias_method :mappings, :mapping
168
+
169
+ # Define settings for the index
170
+ #
171
+ # @example Define index settings
172
+ #
173
+ # Article.settings(index: { number_of_shards: 1 })
174
+ #
175
+ # Article.settings.to_hash
176
+ #
177
+ # # => {:index=>{:number_of_shards=>1}}
178
+ #
179
+ # You can read settings from any object that responds to :read
180
+ # as long as its return value can be parsed as either YAML or JSON.
181
+ #
182
+ # @example Define index settings from YAML file
183
+ #
184
+ # # config/elasticsearch/articles.yml:
185
+ # #
186
+ # # index:
187
+ # # number_of_shards: 1
188
+ # #
189
+ #
190
+ # Article.settings File.open("config/elasticsearch/articles.yml")
191
+ #
192
+ # Article.settings.to_hash
193
+ #
194
+ # # => { "index" => { "number_of_shards" => 1 } }
195
+ #
196
+ #
197
+ # @example Define index settings from JSON file
198
+ #
199
+ # # config/elasticsearch/articles.json:
200
+ # #
201
+ # # { "index": { "number_of_shards": 1 } }
202
+ # #
203
+ #
204
+ # Article.settings File.open("config/elasticsearch/articles.json")
205
+ #
206
+ # Article.settings.to_hash
207
+ #
208
+ # # => { "index" => { "number_of_shards" => 1 } }
209
+ #
210
+ def settings(settings = {}, &block)
211
+ settings = YAML.load(settings.read) if settings.respond_to?(:read)
212
+ @settings ||= Settings.new(settings)
213
+
214
+ @settings.settings.update(settings) unless settings.empty?
215
+
216
+ if block_given?
217
+ self.instance_eval(&block)
218
+ return self
219
+ else
220
+ @settings
221
+ end
222
+ end
223
+
224
+ def load_settings_from_io(settings)
225
+ YAML.load(settings.read)
226
+ end
227
+
228
+ # Creates an index with correct name, automatically passing
229
+ # `settings` and `mappings` defined in the model
230
+ #
231
+ # @example Create an index for the `Article` model
232
+ #
233
+ # Article.__elasticsearch__.create_index!
234
+ #
235
+ # @example Forcefully create (delete first) an index for the `Article` model
236
+ #
237
+ # Article.__elasticsearch__.create_index! force: true
238
+ #
239
+ # @example Pass a specific index name
240
+ #
241
+ # Article.__elasticsearch__.create_index! index: 'my-index'
242
+ #
243
+ def create_index!(options = {})
244
+ options = options.clone
245
+
246
+ target_index = options.delete(:index) || self.index_name
247
+ settings = options.delete(:settings) || self.settings.to_hash
248
+ mappings = options.delete(:mappings) || self.mappings.to_hash
249
+
250
+ delete_index!(options.merge index: target_index) if options[:force]
251
+
252
+ unless index_exists?(index: target_index)
253
+ options.delete(:force)
254
+ self.client.indices.create({ index: target_index,
255
+ body: {
256
+ settings: settings,
257
+ mappings: mappings,
258
+ } }.merge(options))
259
+ end
260
+ end
261
+
262
+ # Returns true if the index exists
263
+ #
264
+ # @example Check whether the model's index exists
265
+ #
266
+ # Article.__elasticsearch__.index_exists?
267
+ #
268
+ # @example Check whether a specific index exists
269
+ #
270
+ # Article.__elasticsearch__.index_exists? index: 'my-index'
271
+ #
272
+ def index_exists?(options = {})
273
+ target_index = options[:index] || self.index_name
274
+
275
+ self.client.indices.exists(index: target_index, ignore: 404)
276
+ end
277
+
278
+ # Deletes the index with corresponding name
279
+ #
280
+ # @example Delete the index for the `Article` model
281
+ #
282
+ # Article.__elasticsearch__.delete_index!
283
+ #
284
+ # @example Pass a specific index name
285
+ #
286
+ # Article.__elasticsearch__.delete_index! index: 'my-index'
287
+ #
288
+ def delete_index!(options = {})
289
+ target_index = options.delete(:index) || self.index_name
290
+
291
+ begin
292
+ self.client.indices.delete index: target_index
293
+ rescue Exception => e
294
+ if e.class.to_s =~ /NotFound/ && options[:force]
295
+ client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger
296
+ nil
297
+ else
298
+ raise e
299
+ end
300
+ end
301
+ end
302
+
303
+ # Performs the "refresh" operation for the index (useful e.g. in tests)
304
+ #
305
+ # @example Refresh the index for the `Article` model
306
+ #
307
+ # Article.__elasticsearch__.refresh_index!
308
+ #
309
+ # @example Pass a specific index name
310
+ #
311
+ # Article.__elasticsearch__.refresh_index! index: 'my-index'
312
+ #
313
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
314
+ #
315
+ def refresh_index!(options = {})
316
+ target_index = options.delete(:index) || self.index_name
317
+
318
+ begin
319
+ self.client.indices.refresh index: target_index
320
+ rescue Exception => e
321
+ if e.class.to_s =~ /NotFound/ && options[:force]
322
+ client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger
323
+ nil
324
+ else
325
+ raise e
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ module InstanceMethods
332
+ def self.included(base)
333
+ # Register callback for storing changed attributes for models
334
+ # which implement `before_save` and return changed attributes
335
+ # (ie. when `Elasticsearch::Model` is included)
336
+ #
337
+ # @note This is typically triggered only when the module would be
338
+ # included in the model directly, not within the proxy.
339
+ #
340
+ # @see #update_document
341
+ #
342
+ base.before_save do |obj|
343
+ if obj.respond_to?(:changes_to_save) # Rails 5.1
344
+ changes_to_save = obj.changes_to_save
345
+ elsif obj.respond_to?(:changes)
346
+ changes_to_save = obj.changes
347
+ end
348
+
349
+ if changes_to_save
350
+ attrs = obj.instance_variable_get(:@__changed_model_attributes) || {}
351
+ latest_changes = changes_to_save.inject({}) { |latest_changes, (k, v)| latest_changes.merge!(k => v.last) }
352
+ obj.instance_variable_set(:@__changed_model_attributes, attrs.merge(latest_changes))
353
+ end
354
+ end if base.respond_to?(:before_save)
355
+ end
356
+
357
+ # Serializes the model instance into JSON (by calling `as_indexed_json`),
358
+ # and saves the document into the Elasticsearch index.
359
+ #
360
+ # @param options [Hash] Optional arguments for passing to the client
361
+ #
362
+ # @example Index a record
363
+ #
364
+ # @article.__elasticsearch__.index_document
365
+ # 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ...
366
+ #
367
+ # @return [Hash] The response from Elasticsearch
368
+ #
369
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index
370
+ #
371
+ def index_document(options = {})
372
+ document = as_indexed_json
373
+ request = { index: index_name,
374
+ id: id,
375
+ body: document }
376
+ request.merge!(type: document_type) if document_type
377
+
378
+ client.index(request.merge!(options))
379
+ end
380
+
381
+ # Deletes the model instance from the index
382
+ #
383
+ # @param options [Hash] Optional arguments for passing to the client
384
+ #
385
+ # @example Delete a record
386
+ #
387
+ # @article.__elasticsearch__.delete_document
388
+ # 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1
389
+ #
390
+ # @return [Hash] The response from Elasticsearch
391
+ #
392
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete
393
+ #
394
+ def delete_document(options = {})
395
+ request = { index: index_name,
396
+ id: self.id }
397
+ request.merge!(type: document_type) if document_type
398
+
399
+ client.delete(request.merge!(options))
400
+ end
401
+
402
+ # Tries to gather the changed attributes of a model instance
403
+ # (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)),
404
+ # performing a _partial_ update of the document.
405
+ #
406
+ # When the changed attributes are not available, performs full re-index of the record.
407
+ #
408
+ # See the {#update_document_attributes} method for updating specific attributes directly.
409
+ #
410
+ # @param options [Hash] Optional arguments for passing to the client
411
+ #
412
+ # @example Update a document corresponding to the record
413
+ #
414
+ # @article = Article.first
415
+ # @article.update_attribute :title, 'Updated'
416
+ # # SQL (0.3ms) UPDATE "articles" SET "title" = ?...
417
+ #
418
+ # @article.__elasticsearch__.update_document
419
+ # # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ...
420
+ # # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}}
421
+ #
422
+ # @return [Hash] The response from Elasticsearch
423
+ #
424
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
425
+ #
426
+ def update_document(options = {})
427
+ if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes).presence
428
+ attributes = if respond_to?(:as_indexed_json)
429
+ self.as_indexed_json.select { |k, v| attributes_in_database.keys.map(&:to_s).include? k.to_s }
430
+ else
431
+ attributes_in_database
432
+ end
433
+
434
+ unless attributes.empty?
435
+ request = { index: index_name,
436
+ id: self.id,
437
+ body: { doc: attributes } }
438
+ request.merge!(type: document_type) if document_type
439
+
440
+ client.update(request.merge!(options))
441
+ end
442
+ else
443
+ index_document(options)
444
+ end
445
+ end
446
+
447
+ # Perform a _partial_ update of specific document attributes
448
+ # (without consideration for changed attributes as in {#update_document})
449
+ #
450
+ # @param attributes [Hash] Attributes to be updated
451
+ # @param options [Hash] Optional arguments for passing to the client
452
+ #
453
+ # @example Update the `title` attribute
454
+ #
455
+ # @article = Article.first
456
+ # @article.title = "New title"
457
+ # @article.__elasticsearch__.update_document_attributes title: "New title"
458
+ #
459
+ # @return [Hash] The response from Elasticsearch
460
+ #
461
+ def update_document_attributes(attributes, options = {})
462
+ request = { index: index_name,
463
+ id: self.id,
464
+ body: { doc: attributes } }
465
+ request.merge!(type: document_type) if document_type
466
+
467
+ client.update(request.merge!(options))
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end
@@ -0,0 +1,101 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Provides methods for getting and setting index name and document type for the model
5
+ #
6
+ module Naming
7
+
8
+ module ClassMethods
9
+
10
+ # Get or set the name of the index
11
+ #
12
+ # @example Set the index name for the `Article` model
13
+ #
14
+ # class Article
15
+ # index_name "articles-#{Rails.env}"
16
+ # end
17
+ #
18
+ # @example Directly set the index name for the `Article` model
19
+ #
20
+ # Article.index_name "articles-#{Rails.env}"
21
+ #
22
+ # TODO: Dynamic names a la Tire -- `Article.index_name { "articles-#{Time.now.year}" }`
23
+ #
24
+ def index_name name=nil
25
+ @index_name = name || @index_name || self.model_name.collection.gsub(/\//, '-')
26
+ end
27
+
28
+ # Set the index name
29
+ #
30
+ # @see index_name
31
+ def index_name=(name)
32
+ @index_name = name
33
+ end
34
+
35
+ # Get or set the document type
36
+ #
37
+ # @example Set the document type for the `Article` model
38
+ #
39
+ # class Article
40
+ # document_type "my-article"
41
+ # end
42
+ #
43
+ # @example Directly set the document type for the `Article` model
44
+ #
45
+ # Article.document_type "my-article"
46
+ #
47
+ def document_type name=nil
48
+ @document_type = name || @document_type || self.model_name.element
49
+ end
50
+
51
+
52
+ # Set the document type
53
+ #
54
+ # @see document_type
55
+ #
56
+ def document_type=(name)
57
+ @document_type = name
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+
63
+ # Get or set the index name for the model instance
64
+ #
65
+ # @example Set the index name for an instance of the `Article` model
66
+ #
67
+ # @article.index_name "articles-#{@article.user_id}"
68
+ # @article.__elasticsearch__.update_document
69
+ #
70
+ def index_name name=nil
71
+ @index_name = name || @index_name || self.class.index_name
72
+ end
73
+
74
+ # Set the index name
75
+ #
76
+ # @see index_name
77
+ def index_name=(name)
78
+ @index_name = name
79
+ end
80
+
81
+ # @example Set the document type for an instance of the `Article` model
82
+ #
83
+ # @article.document_type "my-article"
84
+ # @article.__elasticsearch__.update_document
85
+ #
86
+ def document_type name=nil
87
+ @document_type = name || @document_type || self.class.document_type
88
+ end
89
+
90
+ # Set the document type
91
+ #
92
+ # @see document_type
93
+ #
94
+ def document_type=(name)
95
+ @document_type = name
96
+ end
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,127 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # This module provides a proxy interfacing between the including class and
5
+ # {Elasticsearch::Model}, preventing the pollution of the including class namespace.
6
+ #
7
+ # The only "gateway" between the model and Elasticsearch::Model is the
8
+ # `__elasticsearch__` class and instance method.
9
+ #
10
+ # The including class must be compatible with
11
+ # [ActiveModel](https://github.com/rails/rails/tree/master/activemodel).
12
+ #
13
+ # @example Include the {Elasticsearch::Model} module into an `Article` model
14
+ #
15
+ # class Article < ActiveRecord::Base
16
+ # include Elasticsearch::Model
17
+ # end
18
+ #
19
+ # Article.__elasticsearch__.respond_to?(:search)
20
+ # # => true
21
+ #
22
+ # article = Article.first
23
+ #
24
+ # article.respond_to? :index_document
25
+ # # => false
26
+ #
27
+ # article.__elasticsearch__.respond_to?(:index_document)
28
+ # # => true
29
+ #
30
+ module Proxy
31
+
32
+ # Define the `__elasticsearch__` class and instance methods in the including class
33
+ # and register a callback for intercepting changes in the model.
34
+ #
35
+ # @note The callback is triggered only when `Elasticsearch::Model` is included in the
36
+ # module and the functionality is accessible via the proxy.
37
+ #
38
+ def self.included(base)
39
+ base.class_eval do
40
+ # {ClassMethodsProxy} instance, accessed as `MyModel.__elasticsearch__`
41
+ #
42
+ def self.__elasticsearch__ &block
43
+ @__elasticsearch__ ||= ClassMethodsProxy.new(self)
44
+ @__elasticsearch__.instance_eval(&block) if block_given?
45
+ @__elasticsearch__
46
+ end
47
+
48
+ # {InstanceMethodsProxy}, accessed as `@mymodel.__elasticsearch__`
49
+ #
50
+ def __elasticsearch__ &block
51
+ @__elasticsearch__ ||= InstanceMethodsProxy.new(self)
52
+ @__elasticsearch__.instance_eval(&block) if block_given?
53
+ @__elasticsearch__
54
+ end
55
+
56
+ # Register a callback for storing changed attributes for models which implement
57
+ # `before_save` and `changed_attributes` methods (when `Elasticsearch::Model` is included)
58
+ #
59
+ # @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
60
+ #
61
+ before_save do |i|
62
+ i.__elasticsearch__.instance_variable_set(:@__changed_attributes,
63
+ Hash[ i.changes.map { |key, value| [key, value.last] } ])
64
+ end if respond_to?(:before_save) && instance_methods.include?(:changed_attributes)
65
+ end
66
+ end
67
+
68
+ # Common module for the proxy classes
69
+ #
70
+ module Base
71
+ attr_reader :target
72
+
73
+ def initialize(target)
74
+ @target = target
75
+ end
76
+
77
+ # Delegate methods to `@target`
78
+ #
79
+ def method_missing(method_name, *arguments, &block)
80
+ target.respond_to?(method_name) ? target.__send__(method_name, *arguments, &block) : super
81
+ end
82
+
83
+ # Respond to methods from `@target`
84
+ #
85
+ def respond_to?(method_name, include_private = false)
86
+ target.respond_to?(method_name) || super
87
+ end
88
+
89
+ def inspect
90
+ "[PROXY] #{target.inspect}"
91
+ end
92
+ end
93
+
94
+ # A proxy interfacing between Elasticsearch::Model class methods and model class methods
95
+ #
96
+ # TODO: Inherit from BasicObject and make Pry's `ls` command behave?
97
+ #
98
+ class ClassMethodsProxy
99
+ include Base
100
+ end
101
+
102
+ # A proxy interfacing between Elasticsearch::Model instance methods and model instance methods
103
+ #
104
+ # TODO: Inherit from BasicObject and make Pry's `ls` command behave?
105
+ #
106
+ class InstanceMethodsProxy
107
+ include Base
108
+
109
+ def klass
110
+ target.class
111
+ end
112
+
113
+ def class
114
+ klass.__elasticsearch__
115
+ end
116
+
117
+ # Need to redefine `as_json` because we're not inheriting from `BasicObject`;
118
+ # see TODO note above.
119
+ #
120
+ def as_json(options={})
121
+ target.as_json(options)
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end