chewy 0.0.1 → 0.1.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.
- checksums.yaml +13 -5
- data/.gitignore +1 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +75 -0
- data/README.md +487 -92
- data/Rakefile +3 -2
- data/chewy.gemspec +2 -2
- data/filters +76 -0
- data/lib/chewy.rb +5 -3
- data/lib/chewy/config.rb +36 -19
- data/lib/chewy/fields/base.rb +5 -1
- data/lib/chewy/index.rb +22 -10
- data/lib/chewy/index/actions.rb +13 -13
- data/lib/chewy/index/search.rb +7 -2
- data/lib/chewy/query.rb +382 -64
- data/lib/chewy/query/context.rb +174 -0
- data/lib/chewy/query/criteria.rb +127 -34
- data/lib/chewy/query/loading.rb +9 -9
- data/lib/chewy/query/nodes/and.rb +25 -0
- data/lib/chewy/query/nodes/base.rb +17 -0
- data/lib/chewy/query/nodes/bool.rb +32 -0
- data/lib/chewy/query/nodes/equal.rb +34 -0
- data/lib/chewy/query/nodes/exists.rb +20 -0
- data/lib/chewy/query/nodes/expr.rb +28 -0
- data/lib/chewy/query/nodes/field.rb +106 -0
- data/lib/chewy/query/nodes/missing.rb +20 -0
- data/lib/chewy/query/nodes/not.rb +25 -0
- data/lib/chewy/query/nodes/or.rb +25 -0
- data/lib/chewy/query/nodes/prefix.rb +18 -0
- data/lib/chewy/query/nodes/query.rb +20 -0
- data/lib/chewy/query/nodes/range.rb +63 -0
- data/lib/chewy/query/nodes/raw.rb +15 -0
- data/lib/chewy/query/nodes/regexp.rb +31 -0
- data/lib/chewy/query/nodes/script.rb +20 -0
- data/lib/chewy/query/pagination.rb +28 -22
- data/lib/chewy/railtie.rb +23 -0
- data/lib/chewy/rspec/update_index.rb +20 -3
- data/lib/chewy/type/adapter/active_record.rb +78 -5
- data/lib/chewy/type/adapter/base.rb +46 -0
- data/lib/chewy/type/adapter/object.rb +40 -8
- data/lib/chewy/type/base.rb +1 -1
- data/lib/chewy/type/import.rb +18 -44
- data/lib/chewy/type/observe.rb +24 -14
- data/lib/chewy/version.rb +1 -1
- data/lib/tasks/chewy.rake +27 -0
- data/spec/chewy/config_spec.rb +30 -12
- data/spec/chewy/fields/base_spec.rb +11 -5
- data/spec/chewy/index/actions_spec.rb +20 -20
- data/spec/chewy/index/search_spec.rb +5 -5
- data/spec/chewy/index_spec.rb +28 -8
- data/spec/chewy/query/context_spec.rb +173 -0
- data/spec/chewy/query/criteria_spec.rb +219 -12
- data/spec/chewy/query/loading_spec.rb +6 -4
- data/spec/chewy/query/nodes/and_spec.rb +16 -0
- data/spec/chewy/query/nodes/bool_spec.rb +22 -0
- data/spec/chewy/query/nodes/equal_spec.rb +32 -0
- data/spec/chewy/query/nodes/exists_spec.rb +18 -0
- data/spec/chewy/query/nodes/missing_spec.rb +15 -0
- data/spec/chewy/query/nodes/not_spec.rb +16 -0
- data/spec/chewy/query/nodes/or_spec.rb +16 -0
- data/spec/chewy/query/nodes/prefix_spec.rb +16 -0
- data/spec/chewy/query/nodes/query_spec.rb +12 -0
- data/spec/chewy/query/nodes/range_spec.rb +32 -0
- data/spec/chewy/query/nodes/raw_spec.rb +11 -0
- data/spec/chewy/query/nodes/regexp_spec.rb +31 -0
- data/spec/chewy/query/nodes/script_spec.rb +15 -0
- data/spec/chewy/query/pagination_spec.rb +3 -2
- data/spec/chewy/query_spec.rb +83 -26
- data/spec/chewy/rspec/update_index_spec.rb +20 -0
- data/spec/chewy/type/adapter/active_record_spec.rb +102 -0
- data/spec/chewy/type/adapter/object_spec.rb +82 -0
- data/spec/chewy/type/import_spec.rb +30 -1
- data/spec/chewy/type/mapping_spec.rb +1 -1
- data/spec/chewy/type/observe_spec.rb +46 -12
- data/spec/spec_helper.rb +7 -6
- data/spec/support/class_helpers.rb +2 -2
- metadata +98 -48
- data/.rvmrc +0 -1
- data/lib/chewy/index/client.rb +0 -13
- data/spec/chewy/index/client_spec.rb +0 -18
@@ -0,0 +1,20 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Query
|
3
|
+
module Nodes
|
4
|
+
class Script < Expr
|
5
|
+
def initialize script, params = {}
|
6
|
+
@script = script
|
7
|
+
@params = params
|
8
|
+
@options = params.extract!(:cache)
|
9
|
+
end
|
10
|
+
|
11
|
+
def __render__
|
12
|
+
script = {script: @script}
|
13
|
+
script.merge!(params: @params) if @params.present?
|
14
|
+
script.merge!(_cache: !!@options[:cache]) if @options.key?(:cache)
|
15
|
+
{script: script}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,38 +1,44 @@
|
|
1
|
-
require 'kaminari'
|
2
|
-
|
3
1
|
module Chewy
|
4
2
|
class Query
|
5
3
|
module Pagination
|
6
4
|
extend ActiveSupport::Concern
|
7
5
|
|
8
6
|
included do
|
9
|
-
include Kaminari::
|
7
|
+
include Kaminari if defined?(::Kaminari)
|
8
|
+
end
|
10
9
|
|
11
|
-
|
10
|
+
module Kaminari
|
11
|
+
extend ActiveSupport::Concern
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
16
|
-
end
|
17
|
-
RUBY
|
18
|
-
end
|
13
|
+
included do
|
14
|
+
include ::Kaminari::PageScopeMethods
|
19
15
|
|
20
|
-
|
21
|
-
_response['hits']['total']
|
22
|
-
end
|
16
|
+
delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
def #{::Kaminari.config.page_method_name}(num = 1)
|
20
|
+
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
21
|
+
end
|
22
|
+
RUBY
|
23
|
+
end
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
def total_count
|
26
|
+
_response['hits']['total']
|
27
|
+
end
|
28
|
+
|
29
|
+
def limit_value
|
30
|
+
(criteria.options[:size].presence || default_per_page).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def offset_value
|
34
|
+
criteria.options[:from].to_i
|
35
|
+
end
|
31
36
|
|
32
|
-
|
37
|
+
private
|
33
38
|
|
34
|
-
|
35
|
-
|
39
|
+
def _kaminari_config
|
40
|
+
::Kaminari.config
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
rake_tasks do
|
4
|
+
load 'tasks/chewy.rake'
|
5
|
+
end
|
6
|
+
|
7
|
+
initializer 'chewy.add_app_chewy_path' do |app|
|
8
|
+
app.config.paths.add 'app/chewy'
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer 'chewy.add_requests_logging' do |app|
|
12
|
+
ActiveSupport::Notifications.subscribe('import_objects.chewy') do |name, start, finish, id, payload|
|
13
|
+
duration = ((finish - start).to_f * 10000).round / 10.0
|
14
|
+
Rails.logger.debug(" \e[1m\e[33m#{payload[:type]} Import (#{duration}ms)\e[0m #{payload[:import]}")
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport::Notifications.subscribe('search_query.chewy') do |name, start, finish, id, payload|
|
18
|
+
duration = ((finish - start).to_f * 10000).round / 10.0
|
19
|
+
Rails.logger.debug(" \e[1m\e[33m#{payload[:index]} Search (#{duration}ms)\e[0m #{payload[:request]}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
RSpec::Matchers.define :update_index do |type_name|
|
1
|
+
RSpec::Matchers.define :update_index do |type_name, options = {}|
|
2
2
|
chain(:and_reindex) do |*args|
|
3
3
|
@reindex ||= {}
|
4
4
|
@reindex.merge!(extract_documents(*args))
|
@@ -9,9 +9,15 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
9
9
|
@delete.merge!(extract_documents(*args))
|
10
10
|
end
|
11
11
|
|
12
|
+
chain(:only) do |*args|
|
13
|
+
@only = true
|
14
|
+
end
|
15
|
+
|
12
16
|
match do |block|
|
13
17
|
@reindex ||= {}
|
14
18
|
@delete ||= {}
|
19
|
+
@missed_reindex = []
|
20
|
+
@missed_delete = []
|
15
21
|
|
16
22
|
type = Chewy.derive_type(type_name)
|
17
23
|
updated = []
|
@@ -24,7 +30,11 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
27
|
-
|
33
|
+
if options[:atomic] == false
|
34
|
+
block.call
|
35
|
+
else
|
36
|
+
Chewy.atomic { block.call }
|
37
|
+
end
|
28
38
|
|
29
39
|
@updated = updated
|
30
40
|
@updated.each do |updated_document|
|
@@ -32,10 +42,14 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
32
42
|
if document = @reindex[body[:_id].to_s]
|
33
43
|
document[:real_count] += 1
|
34
44
|
document[:real_attributes].merge!(body[:data])
|
45
|
+
else
|
46
|
+
@missed_reindex.push(body[:_id].to_s) if @only
|
35
47
|
end
|
36
48
|
elsif body = updated_document[:delete]
|
37
49
|
if document = @delete[body[:_id].to_s]
|
38
50
|
document[:real_count] += 1
|
51
|
+
else
|
52
|
+
@missed_delete.push(body[:_id].to_s) if @only
|
39
53
|
end
|
40
54
|
end
|
41
55
|
end
|
@@ -51,7 +65,7 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
51
65
|
(document[:expected_count] && document[:expected_count] == document[:real_count])
|
52
66
|
end
|
53
67
|
|
54
|
-
@updated.any? &&
|
68
|
+
@updated.any? && @missed_reindex.none? && @missed_delete.none? &&
|
55
69
|
@reindex.all? { |_, document| document[:match_count] && document[:match_attributes] } &&
|
56
70
|
@delete.all? { |_, document| document[:match_count] }
|
57
71
|
end
|
@@ -61,6 +75,9 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
61
75
|
|
62
76
|
if @updated.none?
|
63
77
|
output << "Expected index `#{type_name}` to be updated, but it was not\n"
|
78
|
+
else
|
79
|
+
output << "Expected index `#{type_name}` to update documents #{@reindex.keys} only, but #{@missed_reindex} was updated also\n" if @missed_reindex.any?
|
80
|
+
output << "Expected index `#{type_name}` to delete documents #{@delete.keys} only, but #{@missed_delete} was deleted also\n" if @missed_delete.any?
|
64
81
|
end
|
65
82
|
|
66
83
|
output << @reindex.each.with_object('') do |(id, document), output|
|
@@ -1,11 +1,12 @@
|
|
1
|
+
require 'chewy/type/adapter/base'
|
2
|
+
|
1
3
|
module Chewy
|
2
4
|
module Type
|
3
5
|
module Adapter
|
4
|
-
class ActiveRecord
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@options = options
|
6
|
+
class ActiveRecord < Base
|
7
|
+
def initialize *args
|
8
|
+
@options = args.extract_options!
|
9
|
+
subject = args.first
|
9
10
|
if subject.is_a?(::ActiveRecord::Relation)
|
10
11
|
@model = subject.klass
|
11
12
|
@scope = subject
|
@@ -21,6 +22,78 @@ module Chewy
|
|
21
22
|
def type_name
|
22
23
|
@type_name ||= (options[:name].presence || model.model_name).to_s.underscore
|
23
24
|
end
|
25
|
+
|
26
|
+
def import *args, &block
|
27
|
+
import_options = args.extract_options!
|
28
|
+
import_options[:batch_size] ||= BATCH_SIZE
|
29
|
+
collection = args.none? ? model_all :
|
30
|
+
(args.one? && args.first.is_a?(::ActiveRecord::Relation) ? args.first : args.flatten)
|
31
|
+
if collection.is_a?(::ActiveRecord::Relation)
|
32
|
+
result = false
|
33
|
+
merged_scope(collection).find_in_batches(import_options.slice(:batch_size)) do |group|
|
34
|
+
result = block.call grouped_objects(group)
|
35
|
+
end
|
36
|
+
result
|
37
|
+
else
|
38
|
+
if collection.all? { |object| object.respond_to?(:id) }
|
39
|
+
collection.in_groups_of(import_options[:batch_size], false).all? do |group|
|
40
|
+
block.call grouped_objects(group)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
import_ids(collection, import_options, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def load *args
|
49
|
+
load_options = args.extract_options!
|
50
|
+
objects = args.flatten
|
51
|
+
|
52
|
+
scope = model.where(id: objects.map(&:id))
|
53
|
+
loaded_objects = if load_options[:scope].is_a?(Proc)
|
54
|
+
scope.instance_eval(&load_options[:scope])
|
55
|
+
elsif load_options[:scope].is_a?(::ActiveRecord::Relation)
|
56
|
+
scope.merge(load_options[:scope])
|
57
|
+
else
|
58
|
+
scope
|
59
|
+
end.index_by { |object| object.id.to_s }
|
60
|
+
|
61
|
+
objects.map { |object| loaded_objects[object.id.to_s] }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :model, :scope, :options
|
67
|
+
|
68
|
+
def import_ids(ids, import_options = {}, &block)
|
69
|
+
ids = ids.map(&:to_i).uniq
|
70
|
+
|
71
|
+
indexed = false
|
72
|
+
merged_scope(model.where(id: ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
|
73
|
+
ids -= objects.map(&:id)
|
74
|
+
indexed = block.call index: objects
|
75
|
+
end
|
76
|
+
|
77
|
+
deleted = ids.in_groups_of(import_options[:batch_size], false).all? do |group|
|
78
|
+
block.call(delete: group)
|
79
|
+
end
|
80
|
+
|
81
|
+
indexed && deleted
|
82
|
+
end
|
83
|
+
|
84
|
+
def grouped_objects(objects)
|
85
|
+
objects.group_by do |object|
|
86
|
+
object.destroyed? ? :delete : :index
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def merged_scope(target)
|
91
|
+
scope ? scope.clone.merge(target) : target
|
92
|
+
end
|
93
|
+
|
94
|
+
def model_all
|
95
|
+
::ActiveRecord::VERSION::MAJOR < 4 ? model.scoped : model.all
|
96
|
+
end
|
24
97
|
end
|
25
98
|
end
|
26
99
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Adapter
|
4
|
+
# Basic adapter class. Contains interface, need to implement to add any classes support
|
5
|
+
class Base
|
6
|
+
BATCH_SIZE = 1000
|
7
|
+
|
8
|
+
# Camelcased name, used as type class constant name.
|
9
|
+
# For returned value 'Product' will be generated class name `ProductsIndex::Product`
|
10
|
+
#
|
11
|
+
def name
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Underscored type name, user for elasticsearch type creation
|
16
|
+
# and for type class access with ProductsIndex.type_hash hash or method.
|
17
|
+
# `ProductsIndex.type_hash['product']` or `ProductsIndex.product`
|
18
|
+
#
|
19
|
+
def type_name
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
# Splits passed objects to groups according to `:batch_size` options.
|
24
|
+
# For every group crates hash with action keys. Example:
|
25
|
+
#
|
26
|
+
# { delete: [object1, object2], index: [object3, object4, object5] }
|
27
|
+
#
|
28
|
+
# Returns true id all the block call returns true and false otherwise
|
29
|
+
#
|
30
|
+
def import *args, &block
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns array of loaded objects for passed objects array. If some object
|
35
|
+
# was not loaded, it returns `nil` in the place of this object
|
36
|
+
#
|
37
|
+
# load(double(id: 1), double(id: 2), double(id: 3)) #=>
|
38
|
+
# # [<Product id: 1>, nil, <Product id: 3>], assuming, #2 was not found
|
39
|
+
#
|
40
|
+
def load *args
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,20 +1,52 @@
|
|
1
|
+
require 'chewy/type/adapter/base'
|
2
|
+
|
1
3
|
module Chewy
|
2
4
|
module Type
|
3
5
|
module Adapter
|
4
|
-
class Object
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@options = options
|
9
|
-
@subject = subject
|
6
|
+
class Object < Base
|
7
|
+
def initialize *args
|
8
|
+
@options = args.extract_options!
|
9
|
+
@target = args.first
|
10
10
|
end
|
11
11
|
|
12
12
|
def name
|
13
|
-
@name ||=
|
13
|
+
@name ||= (options[:name] || target).to_s.camelize
|
14
14
|
end
|
15
15
|
|
16
16
|
def type_name
|
17
|
-
@type_name ||=
|
17
|
+
@type_name ||= (options[:name] || target).to_s.underscore
|
18
|
+
end
|
19
|
+
|
20
|
+
def import *args, &block
|
21
|
+
import_options = args.extract_options!
|
22
|
+
batch_size = import_options.delete(:batch_size) || BATCH_SIZE
|
23
|
+
objects = args.flatten
|
24
|
+
|
25
|
+
objects.in_groups_of(batch_size, false).all? do |group|
|
26
|
+
action_groups = group.group_by do |object|
|
27
|
+
raise "Object is not a `#{target}`" if class_target? && !object.is_a?(target)
|
28
|
+
object.respond_to?(:destroyed?) && object.destroyed? ? :delete : :index
|
29
|
+
end
|
30
|
+
block.call action_groups
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def load *args
|
35
|
+
load_options = args.extract_options!
|
36
|
+
objects = args.flatten
|
37
|
+
if class_target?
|
38
|
+
objects.map { |object| target.wrap(object) }
|
39
|
+
else
|
40
|
+
objects
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :target, :options
|
47
|
+
|
48
|
+
def class_target?
|
49
|
+
@class_target ||= @target.is_a?(Class)
|
18
50
|
end
|
19
51
|
end
|
20
52
|
end
|
data/lib/chewy/type/base.rb
CHANGED
data/lib/chewy/type/import.rb
CHANGED
@@ -3,62 +3,36 @@ module Chewy
|
|
3
3
|
module Import
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
included do
|
7
|
-
end
|
8
|
-
|
9
6
|
module ClassMethods
|
10
7
|
def bulk(options = {})
|
11
8
|
client.bulk options.merge(index: index.index_name, type: type_name)
|
12
9
|
end
|
13
10
|
|
14
11
|
def import(*args)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
12
|
+
import_options = args.extract_options!
|
13
|
+
bulk_options = import_options.extract!(:refresh).reverse_merge!(refresh: true)
|
14
|
+
identify = {_index: index.index_name, _type: type_name}
|
15
|
+
|
16
|
+
adapter.import(*args, import_options) do |action_objects|
|
17
|
+
payload = {type: self}
|
18
|
+
payload.merge! import: Hash[action_objects.map { |action, objects| [action, objects.count] }]
|
19
|
+
|
20
|
+
ActiveSupport::Notifications.instrument 'import_objects.chewy', payload do
|
21
|
+
body = action_objects.each.with_object([]) do |(action, objects), result|
|
22
|
+
result.concat(if action == :delete
|
23
|
+
objects.map { |object| { action => identify.merge(_id: object.respond_to?(:id) ? object.id : object) } }
|
24
|
+
else
|
25
|
+
objects.map { |object| { action => identify.merge(_id: object.id, data: object_data(object)) } }
|
26
|
+
end)
|
27
|
+
end
|
28
|
+
body.any? ? !!bulk(bulk_options.merge(body: body)) : true
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
33
|
-
def
|
34
|
-
adapter.scope ? relation.merge(adapter.scope) : relation
|
35
|
-
end
|
36
|
-
|
37
|
-
def import_ids(ids, options = {})
|
38
|
-
ids = ids.map(&:to_i).uniq
|
39
|
-
scoped_relation(adapter.model.where(id: ids))
|
40
|
-
.find_in_batches(options.slice(:batch_size)) do |objects|
|
41
|
-
ids -= objects.map(&:id)
|
42
|
-
import_objects objects
|
43
|
-
end
|
44
|
-
|
45
|
-
body = ids.map { |id| {delete: {_index: index.index_name, _type: type_name, _id: id}} }
|
46
|
-
bulk refresh: true, body: body if body.any?
|
47
|
-
end
|
48
|
-
|
49
|
-
def import_objects(objects)
|
50
|
-
body = objects.map do |object|
|
51
|
-
identify = {_index: index.index_name, _type: type_name, _id: object.id}
|
52
|
-
if object.respond_to?(:destroyed?) && object.destroyed?
|
53
|
-
{delete: identify}
|
54
|
-
else
|
55
|
-
{index: identify.merge!(data: object_to_data(object))}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
bulk refresh: true, body: body if body.any?
|
59
|
-
end
|
60
|
-
|
61
|
-
def object_to_data(object)
|
35
|
+
def object_data(object)
|
62
36
|
(self.root_object ||= build_root).compose(object)[type_name.to_sym]
|
63
37
|
end
|
64
38
|
end
|