elastictastic 0.5.0
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.
- data/LICENSE +19 -0
- data/README.md +326 -0
- data/lib/elastictastic/association.rb +21 -0
- data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
- data/lib/elastictastic/callbacks.rb +30 -0
- data/lib/elastictastic/child_collection_proxy.rb +56 -0
- data/lib/elastictastic/client.rb +101 -0
- data/lib/elastictastic/configuration.rb +35 -0
- data/lib/elastictastic/dirty.rb +130 -0
- data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
- data/lib/elastictastic/document.rb +98 -0
- data/lib/elastictastic/errors.rb +7 -0
- data/lib/elastictastic/field.rb +38 -0
- data/lib/elastictastic/index.rb +19 -0
- data/lib/elastictastic/mass_assignment_security.rb +15 -0
- data/lib/elastictastic/middleware.rb +119 -0
- data/lib/elastictastic/nested_document.rb +29 -0
- data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
- data/lib/elastictastic/observer.rb +3 -0
- data/lib/elastictastic/observing.rb +21 -0
- data/lib/elastictastic/parent_child.rb +115 -0
- data/lib/elastictastic/persistence.rb +67 -0
- data/lib/elastictastic/properties.rb +236 -0
- data/lib/elastictastic/railtie.rb +35 -0
- data/lib/elastictastic/resource.rb +4 -0
- data/lib/elastictastic/scope.rb +283 -0
- data/lib/elastictastic/scope_builder.rb +32 -0
- data/lib/elastictastic/scoped.rb +20 -0
- data/lib/elastictastic/search.rb +180 -0
- data/lib/elastictastic/server_error.rb +15 -0
- data/lib/elastictastic/test_helpers.rb +172 -0
- data/lib/elastictastic/util.rb +63 -0
- data/lib/elastictastic/validations.rb +45 -0
- data/lib/elastictastic/version.rb +3 -0
- data/lib/elastictastic.rb +82 -0
- data/spec/environment.rb +6 -0
- data/spec/examples/active_model_lint_spec.rb +20 -0
- data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
- data/spec/examples/callbacks_spec.rb +96 -0
- data/spec/examples/dirty_spec.rb +238 -0
- data/spec/examples/document_spec.rb +600 -0
- data/spec/examples/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/middleware_spec.rb +92 -0
- data/spec/examples/observing_spec.rb +141 -0
- data/spec/examples/parent_child_spec.rb +308 -0
- data/spec/examples/properties_spec.rb +92 -0
- data/spec/examples/scope_spec.rb +491 -0
- data/spec/examples/search_spec.rb +382 -0
- data/spec/examples/spec_helper.rb +15 -0
- data/spec/examples/validation_spec.rb +65 -0
- data/spec/models/author.rb +9 -0
- data/spec/models/blog.rb +5 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/post.rb +41 -0
- data/spec/models/post_observer.rb +11 -0
- data/spec/support/fakeweb_request_history.rb +13 -0
- metadata +227 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module Dirty
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActiveModel::Dirty
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def define_field(field_name, options, &block)
|
11
|
+
super
|
12
|
+
define_dirty_accessors(field_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def define_embed(embed_name, options)
|
16
|
+
super
|
17
|
+
define_dirty_accessors(embed_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
#
|
23
|
+
# We have to rewrite ActiveModel functionality here because in Rails 3.0,
|
24
|
+
# #define_attribute_methods has to be called exactly one time, and there's
|
25
|
+
# no place for us to do that. This appears to be fixed in ActiveModel 3.1
|
26
|
+
#
|
27
|
+
def define_dirty_accessors(attribute)
|
28
|
+
attribute = attribute.to_s
|
29
|
+
module_eval <<-RUBY, __FILE__, __LINE__+1
|
30
|
+
def #{attribute}_changed?
|
31
|
+
attribute_changed?(#{attribute.inspect})
|
32
|
+
end
|
33
|
+
|
34
|
+
def #{attribute}_change
|
35
|
+
attribute_change(#{attribute.inspect})
|
36
|
+
end
|
37
|
+
|
38
|
+
def #{attribute}_will_change!
|
39
|
+
attribute_will_change!(#{attribute.inspect})
|
40
|
+
end
|
41
|
+
|
42
|
+
def #{attribute}_was
|
43
|
+
attribute_was(#{attribute.inspect})
|
44
|
+
end
|
45
|
+
|
46
|
+
def reset_#{attribute}!
|
47
|
+
reset_attribute!(#{attribute})
|
48
|
+
end
|
49
|
+
RUBY
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module InstanceMethods
|
54
|
+
def write_attribute(field, value)
|
55
|
+
attribute_will_change!(field)
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_embed(field, value)
|
60
|
+
attribute_will_change!(field)
|
61
|
+
if Array === value
|
62
|
+
value.each do |el|
|
63
|
+
el.nesting_document = self
|
64
|
+
el.nesting_association = field
|
65
|
+
end
|
66
|
+
super(field, NestedCollectionProxy.new(self, field, value))
|
67
|
+
else
|
68
|
+
value.nesting_document = self
|
69
|
+
value.nesting_association = field
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def save
|
75
|
+
super
|
76
|
+
clean_attributes!
|
77
|
+
end
|
78
|
+
|
79
|
+
def elasticsearch_doc=(doc)
|
80
|
+
super
|
81
|
+
clean_attributes!
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def clean_attributes!
|
87
|
+
changed_attributes.clear
|
88
|
+
@embeds.each_pair do |name, embedded|
|
89
|
+
Util.call_or_map(embedded) { |doc| doc.clean_attributes! }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module NestedDocumentMethods
|
95
|
+
attr_writer :nesting_document, :nesting_association
|
96
|
+
|
97
|
+
def attribute_will_change!(field)
|
98
|
+
super
|
99
|
+
if @nesting_document
|
100
|
+
@nesting_document.__send__("attribute_will_change!", @nesting_association)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class NestedCollectionProxy < Array
|
106
|
+
def initialize(owner, embed_name, collection = [])
|
107
|
+
@owner, @embed_name = owner, embed_name
|
108
|
+
super(collection)
|
109
|
+
end
|
110
|
+
|
111
|
+
[
|
112
|
+
:<<, :[]=, :collect!, :compact!, :delete, :delete_at, :delete_if,
|
113
|
+
:flatten!, :insert, :keep_if, :map!, :push, :reject!, :replace,
|
114
|
+
:reverse!, :rotate!, :select!, :shuffle!, :slice!, :sort!, :sort_by!,
|
115
|
+
:uniq!
|
116
|
+
].each do |destructive_method|
|
117
|
+
module_eval <<-RUBY, __FILE__, __LINE__+1
|
118
|
+
def #{destructive_method}(*args)
|
119
|
+
@owner.__send__("\#{@embed_name}_will_change!")
|
120
|
+
super
|
121
|
+
end
|
122
|
+
RUBY
|
123
|
+
end
|
124
|
+
|
125
|
+
def clone
|
126
|
+
NestedCollectionProxy.new(@owner, @embed_name, map { |el| el.clone })
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Elastictastic
|
4
|
+
class DiscretePersistenceStrategy
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_accessor :auto_refresh
|
8
|
+
|
9
|
+
def create(doc)
|
10
|
+
response = Elastictastic.client.create(
|
11
|
+
doc.index,
|
12
|
+
doc.class.type,
|
13
|
+
doc.id,
|
14
|
+
doc.elasticsearch_doc,
|
15
|
+
params_for(doc)
|
16
|
+
)
|
17
|
+
doc.id = response['_id']
|
18
|
+
doc.persisted!
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(doc)
|
22
|
+
Elastictastic.client.update(
|
23
|
+
doc.index,
|
24
|
+
doc.class.type,
|
25
|
+
doc.id,
|
26
|
+
doc.elasticsearch_doc,
|
27
|
+
params_for(doc)
|
28
|
+
)
|
29
|
+
doc.persisted!
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy(doc)
|
33
|
+
response = Elastictastic.client.delete(
|
34
|
+
doc.index.name,
|
35
|
+
doc.class.type,
|
36
|
+
doc.id,
|
37
|
+
params_for(doc)
|
38
|
+
)
|
39
|
+
doc.transient!
|
40
|
+
response['found']
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def params_for(doc)
|
46
|
+
{}.tap do |params|
|
47
|
+
params[:refresh] = true if Elastictastic.config.auto_refresh
|
48
|
+
params[:parent] = doc._parent_id if doc._parent_id
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module Document
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
extend Scoped
|
7
|
+
include Properties
|
8
|
+
include Persistence
|
9
|
+
include ParentChild
|
10
|
+
include Callbacks
|
11
|
+
include Observing
|
12
|
+
include Dirty
|
13
|
+
include MassAssignmentSecurity
|
14
|
+
include Validations
|
15
|
+
|
16
|
+
extend ActiveModel::Naming
|
17
|
+
include ActiveModel::Conversion
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
delegate :find, :destroy_all, :sync_mapping, :inspect, :find_each,
|
22
|
+
:find_in_batches, :first, :count, :empty?, :any?, :all,
|
23
|
+
:query, :filter, :from, :size, :sort, :highlight, :fields,
|
24
|
+
:script_fields, :preference, :facets, :to => :current_scope
|
25
|
+
|
26
|
+
def mapping
|
27
|
+
{ type => { 'properties' => properties }}
|
28
|
+
end
|
29
|
+
|
30
|
+
def type
|
31
|
+
name.underscore
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_index(name_or_index)
|
35
|
+
Scope.new(Elastictastic::Index(name_or_index), self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def scoped(params)
|
39
|
+
current_scope.scoped(params)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def default_scope
|
45
|
+
in_index(Index.default)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
attr_reader :id
|
51
|
+
|
52
|
+
def initialize(attributes = {})
|
53
|
+
self.class.current_scope.initialize_instance(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def elasticsearch_hit=(hit) #:nodoc:
|
57
|
+
@id = hit['_id']
|
58
|
+
@index = Index.new(hit['_index'])
|
59
|
+
persisted!
|
60
|
+
|
61
|
+
doc = {}
|
62
|
+
doc.merge!(hit['_source']) if hit['_source']
|
63
|
+
fields = hit['fields']
|
64
|
+
if fields
|
65
|
+
unflattened_fields =
|
66
|
+
Util.unflatten_hash(fields.reject { |k, v| v.nil? })
|
67
|
+
if unflattened_fields.has_key?('_source')
|
68
|
+
doc.merge!(unflattened_fields.delete('_source'))
|
69
|
+
end
|
70
|
+
doc.merge!(unflattened_fields)
|
71
|
+
end
|
72
|
+
self.elasticsearch_doc=(doc)
|
73
|
+
end
|
74
|
+
|
75
|
+
def id=(id)
|
76
|
+
assert_transient!
|
77
|
+
@id = id
|
78
|
+
end
|
79
|
+
|
80
|
+
def index
|
81
|
+
return @index if defined? @index
|
82
|
+
@index = Index.default
|
83
|
+
end
|
84
|
+
|
85
|
+
def ==(other)
|
86
|
+
index == other.index && id == other.id
|
87
|
+
end
|
88
|
+
|
89
|
+
def inspect
|
90
|
+
inspected = "#<#{self.class.name} id: #{id}, index: #{index.name}"
|
91
|
+
attributes.each_pair do |attr, value|
|
92
|
+
inspected << ", #{attr}: #{value.inspect}"
|
93
|
+
end
|
94
|
+
inspected << ">"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
CancelBulkOperation = Class.new(StandardError)
|
3
|
+
IllegalModificationError = Class.new(StandardError)
|
4
|
+
OperationNotAllowed = Class.new(StandardError)
|
5
|
+
NoServerAvailable = Class.new(StandardError)
|
6
|
+
RecordInvalid = Class.new(StandardError)
|
7
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
class Field < BasicObject
|
3
|
+
private_class_method :new
|
4
|
+
|
5
|
+
def self.process(field_name, default_options, &block)
|
6
|
+
{}.tap do |properties|
|
7
|
+
new(field_name, default_options, properties, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.with_defaults(options)
|
12
|
+
options = Util.deep_stringify(options)
|
13
|
+
{ 'type' => 'string' }.merge(options).tap do |field_properties|
|
14
|
+
if field_properties['type'].to_s == 'date'
|
15
|
+
field_properties['format'] = 'date_time_no_millis'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(field_name, default_options, properties, &block)
|
21
|
+
@field_name = field_name
|
22
|
+
@properties = properties
|
23
|
+
if block
|
24
|
+
@properties['type'] = 'multi_field'
|
25
|
+
@properties['fields'] =
|
26
|
+
{ field_name.to_s => Field.with_defaults(default_options) }
|
27
|
+
instance_eval(&block)
|
28
|
+
else
|
29
|
+
@properties.merge!(Field.with_defaults(default_options))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def field(field_name, options = {})
|
34
|
+
@properties['fields'][field_name.to_s] =
|
35
|
+
Field.with_defaults(options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActiveModel::MassAssignmentSecurity
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def attributes=(attributes)
|
11
|
+
super(sanitize_for_mass_assignment(attributes))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module Elastictastic
|
4
|
+
module Middleware
|
5
|
+
class JsonEncodeBody < Faraday::Middleware
|
6
|
+
def call(env)
|
7
|
+
case env[:body]
|
8
|
+
when String, nil
|
9
|
+
# nothing
|
10
|
+
else env[:body] = env[:body].to_json
|
11
|
+
end
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class JsonDecodeResponse < Faraday::Middleware
|
17
|
+
def call(env)
|
18
|
+
@app.call(env).on_complete do
|
19
|
+
env[:body] &&= JSON.parse(env[:body])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class RaiseServerErrors < Faraday::Middleware
|
25
|
+
ERROR_PATTERN = /^([A-Z][A-Za-z]*)(?::\s*)?(.*)$/
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
@app.call(env).on_complete do
|
29
|
+
body = env[:body]
|
30
|
+
if body['error']
|
31
|
+
raise_error(body['error'], body['status'])
|
32
|
+
elsif body['_shards'] && body['_shards']['failures']
|
33
|
+
raise_error(
|
34
|
+
body['_shards']['failures'].first['reason'], body['status'])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def raise_error(server_message, status)
|
42
|
+
match = ERROR_PATTERN.match(server_message)
|
43
|
+
if match
|
44
|
+
clazz = Elastictastic::ServerError.const_get(match[1])
|
45
|
+
error = clazz.new(match[2])
|
46
|
+
error.status = status
|
47
|
+
Kernel.raise error
|
48
|
+
else
|
49
|
+
Kernel.raise Elastictastic::ServerError::ServerError, server_message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class LogRequests < Faraday::Middleware
|
55
|
+
def initialize(app, logger)
|
56
|
+
super(app)
|
57
|
+
@logger = logger
|
58
|
+
end
|
59
|
+
|
60
|
+
def call(env)
|
61
|
+
now = Time.now
|
62
|
+
body = env[:body]
|
63
|
+
@app.call(env).on_complete do
|
64
|
+
method = env[:method].to_s.upcase
|
65
|
+
time = ((Time.now - now) * 1000).to_i
|
66
|
+
message = "ElasticSearch #{method} (#{time}ms) #{env[:url].path}"
|
67
|
+
message << ' ' << body if body
|
68
|
+
@logger.debug(message)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Rotor < Faraday::Middleware
|
74
|
+
def initialize(app, *hosts)
|
75
|
+
first = nil
|
76
|
+
hosts.each do |host|
|
77
|
+
node = Node.new(app, host)
|
78
|
+
first ||= node
|
79
|
+
@head.next = node if @head
|
80
|
+
@head = node
|
81
|
+
end
|
82
|
+
@head.next = first
|
83
|
+
end
|
84
|
+
|
85
|
+
def call(env)
|
86
|
+
last = @head
|
87
|
+
begin
|
88
|
+
@head = @head.next
|
89
|
+
@head.call(env)
|
90
|
+
rescue Faraday::Error::ConnectionFailed => e
|
91
|
+
raise NoServerAvailable if @head == last
|
92
|
+
retry
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Node < Faraday::Middleware
|
97
|
+
attr_accessor :next
|
98
|
+
|
99
|
+
def initialize(app, url)
|
100
|
+
super(app)
|
101
|
+
# Create a connection instance so we can use its #build_url method.
|
102
|
+
# Kinda lame -- seems like it would make more sense for Faraday to
|
103
|
+
# just implement a middleware for injecting a host/path prefix.
|
104
|
+
@connection = Faraday::Connection.new(:url => url)
|
105
|
+
end
|
106
|
+
|
107
|
+
def call(env)
|
108
|
+
original_url = env[:url]
|
109
|
+
begin
|
110
|
+
env[:url] = @connection.build_url(original_url)
|
111
|
+
@app.call(env)
|
112
|
+
ensure
|
113
|
+
env[:url] = original_url
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module NestedDocument
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include Properties
|
7
|
+
include Dirty
|
8
|
+
include Dirty::NestedDocumentMethods
|
9
|
+
include MassAssignmentSecurity
|
10
|
+
include Validations
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
def initialize_copy(original)
|
15
|
+
self.write_attributes(original.read_attributes.dup)
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
inspected = "#<#{self.class.name}"
|
20
|
+
if attributes.any?
|
21
|
+
inspected << ' ' << attributes.each_pair.map do |attr, value|
|
22
|
+
"#{attr}: #{value.inspect}"
|
23
|
+
end.join(', ')
|
24
|
+
end
|
25
|
+
inspected << '>'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
begin
|
2
|
+
require 'new_relic/agent/method_tracer'
|
3
|
+
rescue LoadError => e
|
4
|
+
raise LoadError, "Can't use NewRelic instrumentation without NewRelic gem"
|
5
|
+
end
|
6
|
+
|
7
|
+
module Elastictastic
|
8
|
+
module NewRelicInstrumentation
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
include NewRelic::Agent::MethodTracer
|
13
|
+
|
14
|
+
add_method_tracer :create, 'ElasticSearch/#{args[1].classify}#create'
|
15
|
+
add_method_tracer :delete, 'ElasticSearch/#{args[1].classify + "#" if args[1]}/delete'
|
16
|
+
add_method_tracer :get, 'ElasticSearch/#{args[1].classify}#get'
|
17
|
+
add_method_tracer :mget, 'ElasticSearch/mget'
|
18
|
+
add_method_tracer :put_mapping, 'ElasticSearch/#{args[1].classify}#put_mapping'
|
19
|
+
add_method_tracer :scroll, 'ElasticSearch/scroll'
|
20
|
+
add_method_tracer :search, 'ElasticSearch/#{args[1].classify}#search'
|
21
|
+
add_method_tracer :update, 'ElasticSearch/#{args[1].classify}#update'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Elastictastic::Client.module_eval { include Elastictastic::NewRelicInstrumentation }
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module Observing
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
extend ActiveModel::Observing::ClassMethods
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActiveModel::Observing
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
Callbacks::HOOKS.each do |method|
|
12
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
|
+
def #{method}(*args)
|
14
|
+
notify_observers(:before_#{method})
|
15
|
+
super.tap { notify_observers(:after_#{method}) }
|
16
|
+
end
|
17
|
+
RUBY
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Elastictastic
|
2
|
+
module ParentChild
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
attr_reader :parent_association
|
7
|
+
|
8
|
+
def belongs_to(parent_name, options = {})
|
9
|
+
@parent_association = Association.new(parent_name, options)
|
10
|
+
|
11
|
+
module_eval(<<-RUBY, __FILE__, __LINE__+1)
|
12
|
+
def #{parent_name}
|
13
|
+
_parent
|
14
|
+
end
|
15
|
+
RUBY
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_many(children_name, options = {})
|
19
|
+
children_name = children_name.to_s
|
20
|
+
child_associations[children_name] = Association.new(children_name, options)
|
21
|
+
|
22
|
+
module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
23
|
+
def #{children_name}
|
24
|
+
read_child(#{children_name.inspect})
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
|
29
|
+
def child_association(name)
|
30
|
+
child_associations[name.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
def child_associations
|
34
|
+
@child_associations ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def mapping
|
38
|
+
super.tap do |mapping|
|
39
|
+
mapping[type]['_parent'] = { 'type' => @parent_association.clazz.type } if @parent_association
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module InstanceMethods
|
45
|
+
|
46
|
+
def initialize(attributes = {})
|
47
|
+
super
|
48
|
+
@children = Hash.new do |hash, child_association_name|
|
49
|
+
hash[child_association_name] = Elastictastic::ChildCollectionProxy.new(
|
50
|
+
self.class.child_association(child_association_name.to_s),
|
51
|
+
self
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def elasticsearch_doc=(doc)
|
57
|
+
@parent_id = doc.delete('_parent')
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def _parent #:nodoc:
|
62
|
+
return @parent if defined? @parent
|
63
|
+
@parent =
|
64
|
+
if @parent_id
|
65
|
+
self.class.parent_association.clazz.find(@parent_id)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def _parent_id #:nodoc:
|
70
|
+
if @parent
|
71
|
+
@parent_id = @parent.id
|
72
|
+
elsif @parent_id
|
73
|
+
@parent_id
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def parent_collection=(parent_collection)
|
78
|
+
if @parent_collection
|
79
|
+
raise Elastictastic::IllegalModificationError,
|
80
|
+
"Document is already a child of #{_parent}"
|
81
|
+
end
|
82
|
+
if persisted?
|
83
|
+
raise Elastictastic::IllegalModificationError,
|
84
|
+
"Can't change parent of persisted object"
|
85
|
+
end
|
86
|
+
@parent_collection = parent_collection
|
87
|
+
@parent = parent_collection.parent
|
88
|
+
end
|
89
|
+
|
90
|
+
def save
|
91
|
+
super
|
92
|
+
self.class.child_associations.each_pair do |name, association|
|
93
|
+
association.extract(self).transient_children.each do |child|
|
94
|
+
child.save unless child.pending_save?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def persisted!
|
100
|
+
was_persisted = @persisted
|
101
|
+
super
|
102
|
+
if @parent_collection && !was_persisted
|
103
|
+
@parent_collection.persisted!(self)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def read_child(field_name)
|
110
|
+
@children[field_name.to_s]
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|