elasticsearch-model-queryable 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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