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