outoftime-sunspot 0.0.2 → 0.7.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/History.txt +4 -0
- data/README.rdoc +26 -33
- data/TODO +7 -0
- data/VERSION.yml +2 -2
- data/lib/light_config.rb +5 -5
- data/lib/sunspot/adapters.rb +209 -33
- data/lib/sunspot/configuration.rb +25 -10
- data/lib/sunspot/dsl/fields.rb +42 -11
- data/lib/sunspot/dsl/query.rb +150 -6
- data/lib/sunspot/dsl/scope.rb +16 -26
- data/lib/sunspot/facet.rb +37 -0
- data/lib/sunspot/facet_row.rb +34 -0
- data/lib/sunspot/facets.rb +21 -0
- data/lib/sunspot/field.rb +112 -56
- data/lib/sunspot/indexer.rb +49 -46
- data/lib/sunspot/query.rb +217 -49
- data/lib/sunspot/restriction.rb +158 -14
- data/lib/sunspot/search.rb +79 -28
- data/lib/sunspot/session.rb +75 -8
- data/lib/sunspot/setup.rb +171 -0
- data/lib/sunspot/type.rb +116 -32
- data/lib/sunspot/util.rb +141 -7
- data/lib/sunspot.rb +260 -31
- data/spec/api/build_search_spec.rb +139 -33
- data/spec/api/indexer_spec.rb +33 -2
- data/spec/api/search_retrieval_spec.rb +85 -2
- data/spec/api/session_spec.rb +14 -6
- data/spec/integration/faceting_spec.rb +39 -0
- data/spec/integration/keyword_search_spec.rb +1 -1
- data/spec/integration/scoped_search_spec.rb +175 -0
- data/spec/mocks/mock_adapter.rb +7 -10
- data/spec/mocks/post.rb +7 -2
- data/tasks/rdoc.rake +7 -0
- data/tasks/spec.rake +3 -0
- data/tasks/todo.rake +4 -0
- metadata +12 -7
- data/lib/sunspot/builder.rb +0 -78
- data/spec/api/standard_search_builder_spec.rb +0 -76
- data/spec/custom_expectation.rb +0 -53
- data/spec/integration/field_types_spec.rb +0 -62
data/lib/sunspot/session.rb
CHANGED
@@ -1,43 +1,110 @@
|
|
1
1
|
module Sunspot
|
2
|
+
#
|
3
|
+
# A Sunspot session encapsulates a connection to Solr and a set of
|
4
|
+
# configuration choices. Though users of Sunspot may manually instantiate
|
5
|
+
# Session objects, in the general case it's easier to use the singleton
|
6
|
+
# stored in the Sunspot module. Since the Sunspot module provides all of
|
7
|
+
# the instance methods of Session as class methods, they are not documented
|
8
|
+
# again here.
|
9
|
+
#
|
2
10
|
class Session
|
3
11
|
attr_reader :config
|
4
12
|
|
5
|
-
|
13
|
+
#
|
14
|
+
# Sessions are initialized with a Sunspot configuration and a Solr
|
15
|
+
# connection. Usually you will want to stick with the default arguments
|
16
|
+
# when instantiating your own sessions.
|
17
|
+
#
|
18
|
+
def initialize(config = Configuration.build, connection = nil)
|
6
19
|
@config = config
|
7
20
|
yield(@config) if block_given?
|
8
21
|
@connection = connection
|
9
22
|
end
|
10
23
|
|
24
|
+
#
|
25
|
+
# See Sunspot.search
|
26
|
+
#
|
11
27
|
def search(*types, &block)
|
12
|
-
|
28
|
+
types.flatten!
|
29
|
+
Search.new(connection, @config, *types, &block).execute!
|
13
30
|
end
|
14
31
|
|
32
|
+
#
|
33
|
+
# See Sunspot.index
|
34
|
+
#
|
15
35
|
def index(*objects)
|
36
|
+
objects.flatten!
|
16
37
|
for object in objects
|
17
|
-
|
38
|
+
setup_for(object).indexer(connection).add(object)
|
18
39
|
end
|
19
40
|
end
|
20
41
|
|
42
|
+
#
|
43
|
+
# See Sunspot.index!
|
44
|
+
#
|
45
|
+
def index!(*objects)
|
46
|
+
index(*objects)
|
47
|
+
commit
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# See Sunspot.commit!
|
52
|
+
#
|
53
|
+
def commit
|
54
|
+
connection.commit
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# See Sunspot.remove
|
59
|
+
#
|
21
60
|
def remove(*objects)
|
61
|
+
objects.flatten!
|
22
62
|
for object in objects
|
23
|
-
|
63
|
+
setup_for(object).indexer(connection).remove(object)
|
24
64
|
end
|
25
65
|
end
|
26
66
|
|
67
|
+
#
|
68
|
+
# See Sunspot.remove_all
|
69
|
+
#
|
27
70
|
def remove_all(*classes)
|
71
|
+
classes.flatten!
|
28
72
|
if classes.empty?
|
29
|
-
|
73
|
+
Indexer.remove_all(connection)
|
30
74
|
else
|
31
|
-
|
32
|
-
|
75
|
+
for clazz in classes
|
76
|
+
Setup.for(clazz).indexer(connection).remove_all
|
33
77
|
end
|
34
78
|
end
|
35
79
|
end
|
36
80
|
|
37
81
|
private
|
38
82
|
|
83
|
+
#
|
84
|
+
# Get the Setup object for the given object's class.
|
85
|
+
#
|
86
|
+
# ==== Parameters
|
87
|
+
#
|
88
|
+
# object<Object>:: The object whose setup is to be retrieved
|
89
|
+
#
|
90
|
+
# ==== Returns
|
91
|
+
#
|
92
|
+
# Sunspot::Setup:: The setup for the object's class
|
93
|
+
#
|
94
|
+
def setup_for(object)
|
95
|
+
Setup.for(object.class) || raise(ArgumentError, "Sunspot is not configured for #{object.class.inspect}")
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Retrieve the Solr connection for this session, creating one if it does not
|
100
|
+
# already exist.
|
101
|
+
#
|
102
|
+
# ==== Returns
|
103
|
+
#
|
104
|
+
# Solr::Connection:: The connection for this session
|
105
|
+
#
|
39
106
|
def connection
|
40
|
-
@connection ||= Solr::Connection.new(config.solr.url
|
107
|
+
@connection ||= Solr::Connection.new(config.solr.url)
|
41
108
|
end
|
42
109
|
end
|
43
110
|
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Sunspot
|
2
|
+
#
|
3
|
+
# This class encapsulates the search/indexing setup for a given class. Its
|
4
|
+
# contents are built using the Sunspot.setup method.
|
5
|
+
#
|
6
|
+
class Setup #:nodoc:
|
7
|
+
def initialize(clazz)
|
8
|
+
@class_name = clazz.name
|
9
|
+
@fields, @text_fields = [], []
|
10
|
+
@dsl = DSL::Fields.new(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Add fields for scope/ordering
|
15
|
+
#
|
16
|
+
# ==== Parameters
|
17
|
+
#
|
18
|
+
# fields<Array>:: Array of Sunspot::Field objects
|
19
|
+
#
|
20
|
+
def add_fields(fields)
|
21
|
+
@fields.concat(Array(fields))
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Add fields for fulltext search
|
26
|
+
#
|
27
|
+
# ==== Parameters
|
28
|
+
#
|
29
|
+
# fields<Array>:: Array of Sunspot::Field objects
|
30
|
+
#
|
31
|
+
def add_text_fields(fields)
|
32
|
+
@text_fields.concat(Array(fields))
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Builder method for evaluating the setup DSL
|
37
|
+
#
|
38
|
+
def setup(&block)
|
39
|
+
@dsl.instance_eval(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Get the fields associated with this setup as well as all inherited fields
|
44
|
+
#
|
45
|
+
# ==== Returns
|
46
|
+
#
|
47
|
+
# Array:: Collection of all fields associated with this setup
|
48
|
+
#
|
49
|
+
def fields
|
50
|
+
fields = @fields.dup
|
51
|
+
fields.concat(parent.fields) if parent
|
52
|
+
fields
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Get the text fields associated with this setup as well as all inherited
|
57
|
+
# text fields
|
58
|
+
#
|
59
|
+
# ==== Returns
|
60
|
+
#
|
61
|
+
# Array:: Collection of all text fields associated with this setup
|
62
|
+
#
|
63
|
+
def text_fields
|
64
|
+
text_fields = @text_fields.dup
|
65
|
+
text_fields.concat(parent.text_fields) if parent
|
66
|
+
text_fields
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Get all scope and text fields associated with this setup as well as all
|
71
|
+
# inherited fields
|
72
|
+
#
|
73
|
+
# ==== Returns
|
74
|
+
#
|
75
|
+
# Array:: Collection of all text and scope fields associated with this setup
|
76
|
+
#
|
77
|
+
def all_fields
|
78
|
+
fields + text_fields
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Factory method for an Indexer object configured to use this setup
|
83
|
+
#
|
84
|
+
# ==== Returns
|
85
|
+
#
|
86
|
+
# Sunspot::Indexer:: Indexer configured with this setup
|
87
|
+
def indexer(connection)
|
88
|
+
Indexer.new(connection, self)
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Return the class associated with this setup.
|
93
|
+
#
|
94
|
+
# ==== Returns
|
95
|
+
#
|
96
|
+
# clazz<Class>:: Class setup is configured for
|
97
|
+
#
|
98
|
+
def clazz
|
99
|
+
Util.full_const_get(@class_name)
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
#
|
105
|
+
# Get the nearest inherited setup, if any
|
106
|
+
#
|
107
|
+
# ==== Returns
|
108
|
+
#
|
109
|
+
# Sunspot::Setup:: Setup for the nearest ancestor of this setup's class
|
110
|
+
#
|
111
|
+
def parent
|
112
|
+
Setup.for(clazz.superclass)
|
113
|
+
end
|
114
|
+
|
115
|
+
class <<self
|
116
|
+
#
|
117
|
+
# Retrieve or create the Setup instance for the given class, evaluating
|
118
|
+
# the given block to add to the setup's configuration
|
119
|
+
#
|
120
|
+
def setup(clazz, &block) #:nodoc:
|
121
|
+
self.for!(clazz).setup(&block)
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Retrieve the setup instance for the given class, or for the nearest
|
126
|
+
# ancestor that has a setup, if any.
|
127
|
+
#
|
128
|
+
# ==== Parameters
|
129
|
+
#
|
130
|
+
# clazz<Class>:: Class for which to retrieve a setup
|
131
|
+
#
|
132
|
+
# ==== Returns
|
133
|
+
#
|
134
|
+
# Sunspot::Setup::
|
135
|
+
# Setup instance associated with the given class or its nearest ancestor
|
136
|
+
#
|
137
|
+
def for(clazz) #:nodoc:
|
138
|
+
setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
#
|
144
|
+
# Retrieve or create a Setup instance for this class
|
145
|
+
#
|
146
|
+
# ==== Parameters
|
147
|
+
#
|
148
|
+
# clazz<Class>:: Class for which to retrieve a setup
|
149
|
+
#
|
150
|
+
# ==== Returns
|
151
|
+
#
|
152
|
+
# Sunspot::Setup:: New or existing setup for this class
|
153
|
+
#
|
154
|
+
def for!(clazz) #:nodoc:
|
155
|
+
setups[clazz.name.to_sym] ||= new(clazz)
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# Singleton hash of class names to Setup instances
|
161
|
+
#
|
162
|
+
# ==== Returns
|
163
|
+
#
|
164
|
+
# Hash:: Class names keyed to Setup instances
|
165
|
+
#
|
166
|
+
def setups
|
167
|
+
@setups ||= {}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/lib/sunspot/type.rb
CHANGED
@@ -1,60 +1,144 @@
|
|
1
1
|
module Sunspot
|
2
|
+
#
|
3
|
+
# This module contains singleton objects that represent the types that can be
|
4
|
+
# indexed and searched using Sunspot. Plugin developers should be able to
|
5
|
+
# add new constants to the Type module; as long as they implement the
|
6
|
+
# appropriate methods, Sunspot should be able to integrate them (note that
|
7
|
+
# this capability is untested at the moment). The required methods are:
|
8
|
+
#
|
9
|
+
# +indexed_name+::
|
10
|
+
# Convert a given field name into its form as stored in Solr. This
|
11
|
+
# generally means adding a suffix to match a Solr dynamicField definition.
|
12
|
+
# +to_indexed+::
|
13
|
+
# Convert a value of this type into the appropriate Solr string
|
14
|
+
# representation.
|
15
|
+
# +cast+::
|
16
|
+
# Convert a Solr string representation of a value into the appropriate
|
17
|
+
# Ruby type.
|
18
|
+
#
|
2
19
|
module Type
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
20
|
+
#
|
21
|
+
# Text is a special type that stores data for fulltext search. Unlike other
|
22
|
+
# types, Text fields are tokenized and are made available to the keyword
|
23
|
+
# search phrase. Text fields cannot be faceted, ordered upon, or used in
|
24
|
+
# restrictions. Similarly, text fields are the only fields that are made
|
25
|
+
# available to keyword search.
|
26
|
+
#
|
27
|
+
module TextType
|
28
|
+
class <<self
|
29
|
+
def indexed_name(name) #:nodoc:
|
11
30
|
"#{name}_text"
|
12
|
-
|
31
|
+
end
|
13
32
|
|
14
|
-
|
15
|
-
|
33
|
+
def to_indexed(value) #:nodoc:
|
34
|
+
value.to_s if value
|
35
|
+
end
|
16
36
|
end
|
17
37
|
end
|
18
38
|
|
19
|
-
|
20
|
-
|
39
|
+
#
|
40
|
+
# The String type represents string data.
|
41
|
+
#
|
42
|
+
module StringType
|
43
|
+
class <<self
|
44
|
+
def indexed_name(name) #:nodoc:
|
21
45
|
"#{name}_s"
|
22
|
-
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_indexed(value) #:nodoc:
|
49
|
+
value.to_s if value
|
50
|
+
end
|
23
51
|
|
24
|
-
|
25
|
-
|
52
|
+
def cast(string) #:nodoc:
|
53
|
+
string
|
54
|
+
end
|
26
55
|
end
|
27
56
|
end
|
28
57
|
|
29
|
-
|
30
|
-
|
58
|
+
#
|
59
|
+
# The Integer type represents integers.
|
60
|
+
#
|
61
|
+
module IntegerType
|
62
|
+
class <<self
|
63
|
+
def indexed_name(name) #:nodoc:
|
31
64
|
"#{name}_i"
|
32
|
-
|
65
|
+
end
|
33
66
|
|
34
|
-
|
35
|
-
|
67
|
+
def to_indexed(value) #:nodoc:
|
68
|
+
value.to_i.to_s if value
|
69
|
+
end
|
70
|
+
|
71
|
+
def cast(string) #:nodoc:
|
72
|
+
string.to_i
|
73
|
+
end
|
36
74
|
end
|
37
75
|
end
|
38
76
|
|
39
|
-
|
40
|
-
|
77
|
+
#
|
78
|
+
# The Float type represents floating-point numbers.
|
79
|
+
#
|
80
|
+
module FloatType
|
81
|
+
class <<self
|
82
|
+
def indexed_name(name) #:nodoc:
|
41
83
|
"#{name}_f"
|
42
|
-
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_indexed(value) #:nodoc:
|
87
|
+
value.to_f.to_s if value
|
88
|
+
end
|
43
89
|
|
44
|
-
|
45
|
-
|
90
|
+
def cast(string) #:nodoc:
|
91
|
+
string.to_f
|
92
|
+
end
|
46
93
|
end
|
47
94
|
end
|
48
95
|
|
49
|
-
|
50
|
-
|
96
|
+
#
|
97
|
+
# The time type represents times. Note that times are always converted to
|
98
|
+
# UTC before indexing, and facets of Time fields always return times in UTC.
|
99
|
+
#
|
100
|
+
module TimeType
|
101
|
+
class <<self
|
102
|
+
def indexed_name(name)
|
51
103
|
"#{name}_d"
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_indexed(value)
|
107
|
+
if value
|
108
|
+
time = value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
|
109
|
+
time.utc.xmlschema
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def cast(string)
|
114
|
+
Time.xmlschema(string)
|
115
|
+
end
|
52
116
|
end
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# The boolean type represents true/false values. Note that +nil+ will not be
|
121
|
+
# indexed at all; only +false+ will be indexed with a false value.
|
122
|
+
#
|
123
|
+
module BooleanType
|
124
|
+
class <<self
|
125
|
+
def indexed_name(name)
|
126
|
+
"#{name}_b"
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_indexed(value)
|
130
|
+
unless value.nil?
|
131
|
+
value ? 'true' : 'false'
|
132
|
+
end
|
133
|
+
end
|
53
134
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
135
|
+
def cast(string)
|
136
|
+
case string
|
137
|
+
when 'true'
|
138
|
+
true
|
139
|
+
when 'false'
|
140
|
+
false
|
141
|
+
end
|
58
142
|
end
|
59
143
|
end
|
60
144
|
end
|
data/lib/sunspot/util.rb
CHANGED
@@ -1,12 +1,146 @@
|
|
1
1
|
module Sunspot
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
#
|
3
|
+
# The Sunspot::Util module provides utility methods used elsewhere in the
|
4
|
+
# library.
|
5
|
+
#
|
6
|
+
module Util #:nodoc:
|
7
|
+
class <<self
|
8
|
+
#
|
9
|
+
# Get all of the superclasses for a given class, including the class
|
10
|
+
# itself.
|
11
|
+
#
|
12
|
+
# ==== Parameters
|
13
|
+
#
|
14
|
+
# clazz<Class>:: class for which to get superclasses
|
15
|
+
#
|
16
|
+
# ==== Returns
|
17
|
+
#
|
18
|
+
# Array:: Collection containing class and its superclasses
|
19
|
+
#
|
20
|
+
def superclasses_for(clazz)
|
21
|
+
superclasses = [clazz]
|
22
|
+
superclasses << (clazz = clazz.superclass) while clazz.superclass != Object
|
23
|
+
superclasses
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Convert a string to snake case
|
28
|
+
#
|
29
|
+
# ==== Parameters
|
30
|
+
#
|
31
|
+
# string<String>:: String to convert to snake case
|
32
|
+
#
|
33
|
+
# ==== Returns
|
34
|
+
#
|
35
|
+
# String:: String in snake case
|
36
|
+
#
|
37
|
+
def snake_case(string)
|
38
|
+
string.scan(/(^|[A-Z])([^A-Z]+)/).map! { |word| word.join.downcase }.join('_')
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Convert a string to camel case
|
43
|
+
#
|
44
|
+
# ==== Parameters
|
45
|
+
#
|
46
|
+
# string<String>:: String to convert to camel case
|
47
|
+
#
|
48
|
+
# ==== Returns
|
49
|
+
#
|
50
|
+
# String:: String in camel case
|
51
|
+
#
|
52
|
+
def camel_case(string)
|
53
|
+
string.split('_').map! { |word| word.capitalize }.join
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Get a constant from a fully qualified name
|
58
|
+
#
|
59
|
+
# ==== Parameters
|
60
|
+
#
|
61
|
+
# string<String>:: The fully qualified name of a constant
|
62
|
+
#
|
63
|
+
# ==== Returns
|
64
|
+
#
|
65
|
+
# Object:: Value of constant named
|
66
|
+
#
|
67
|
+
def full_const_get(string)
|
68
|
+
string.split('::').inject(Object) do |context, const_name|
|
69
|
+
context.const_get(const_name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Perform a deep merge of hashes, returning the result as a new hash.
|
75
|
+
# See #deep_merge_into for rules used to merge the hashes
|
76
|
+
#
|
77
|
+
# ==== Parameters
|
78
|
+
#
|
79
|
+
# left<Hash>:: Hash to merge
|
80
|
+
# right<Hash>:: The other hash to merge
|
81
|
+
#
|
82
|
+
# ==== Returns
|
83
|
+
#
|
84
|
+
# Hash:: New hash containing the given hashes deep-merged.
|
85
|
+
#
|
86
|
+
def deep_merge(left, right)
|
87
|
+
deep_merge_into({}, left, right)
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Perform a deep merge of the right hash into the left hash
|
92
|
+
#
|
93
|
+
# ==== Parameters
|
94
|
+
#
|
95
|
+
# left:: Hash to receive merge
|
96
|
+
# right:: Hash to merge into left
|
97
|
+
#
|
98
|
+
# ==== Returns
|
99
|
+
#
|
100
|
+
# Hash:: left
|
101
|
+
#
|
102
|
+
def deep_merge!(left, right)
|
103
|
+
deep_merge_into(left, left, right)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
#
|
109
|
+
# Deep merge two hashes into a third hash, using rules that produce nice
|
110
|
+
# merged parameter hashes. The rules are as follows, for a given key:
|
111
|
+
#
|
112
|
+
# * If only one hash has a value, or if both hashes have the same value,
|
113
|
+
# just use the value.
|
114
|
+
# * If either of the values is not a hash, create arrays out of both
|
115
|
+
# values and concatenate them.
|
116
|
+
# * Otherwise, deep merge the two values (which are both hashes)
|
117
|
+
#
|
118
|
+
# ==== Parameters
|
119
|
+
#
|
120
|
+
# destination<Hash>:: Hash into which to perform the merge
|
121
|
+
# left<Hash>:: One hash to merge
|
122
|
+
# right<Hash>:: The other hash to merge
|
123
|
+
#
|
124
|
+
# ==== Returns
|
125
|
+
#
|
126
|
+
# Hash:: destination
|
127
|
+
#
|
128
|
+
def deep_merge_into(destination, left, right)
|
129
|
+
left.each_pair do |name, left_value|
|
130
|
+
right_value = right[name]
|
131
|
+
destination[name] =
|
132
|
+
if right_value.nil? || left_value == right_value
|
133
|
+
left_value
|
134
|
+
elsif !left_value.respond_to?(:each_pair) || !right_value.respond_to?(:each_pair)
|
135
|
+
Array(left_value) + Array(right_value)
|
136
|
+
else
|
137
|
+
merged_value = {}
|
138
|
+
deep_merge_into(merged_value, left_value, right_value)
|
139
|
+
end
|
9
140
|
end
|
141
|
+
left_keys = Set.new(left.keys)
|
142
|
+
destination.merge!(right.reject { |k, v| left_keys.include?(k) })
|
143
|
+
destination
|
10
144
|
end
|
11
145
|
end
|
12
146
|
end
|