http-api-tools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +485 -0
- data/Rakefile +4 -0
- data/http-api-tools.gemspec +29 -0
- data/lib/hat/base_json_serializer.rb +107 -0
- data/lib/hat/expanded_relation_includes.rb +77 -0
- data/lib/hat/identity_map.rb +42 -0
- data/lib/hat/json_serializer_dsl.rb +62 -0
- data/lib/hat/model/acts_like_active_model.rb +16 -0
- data/lib/hat/model/attributes.rb +159 -0
- data/lib/hat/model/has_many_array.rb +47 -0
- data/lib/hat/model/transformers/date_time_transformer.rb +31 -0
- data/lib/hat/model/transformers/registry.rb +55 -0
- data/lib/hat/model.rb +2 -0
- data/lib/hat/nesting/json_serializer.rb +45 -0
- data/lib/hat/nesting/relation_loader.rb +89 -0
- data/lib/hat/relation_includes.rb +140 -0
- data/lib/hat/serializer_registry.rb +27 -0
- data/lib/hat/sideloading/json_deserializer.rb +121 -0
- data/lib/hat/sideloading/json_deserializer_mapping.rb +27 -0
- data/lib/hat/sideloading/json_serializer.rb +125 -0
- data/lib/hat/sideloading/relation_sideloader.rb +79 -0
- data/lib/hat/sideloading/sideload_map.rb +54 -0
- data/lib/hat/type_key_resolver.rb +27 -0
- data/lib/hat/version.rb +3 -0
- data/lib/hat.rb +9 -0
- data/reports/empty.png +0 -0
- data/reports/minus.png +0 -0
- data/reports/plus.png +0 -0
- data/spec/hat/expanded_relation_includes_spec.rb +32 -0
- data/spec/hat/identity_map_spec.rb +31 -0
- data/spec/hat/model/attributes_spec.rb +170 -0
- data/spec/hat/model/has_many_array_spec.rb +48 -0
- data/spec/hat/model/transformers/date_time_transformer_spec.rb +36 -0
- data/spec/hat/model/transformers/registry_spec.rb +53 -0
- data/spec/hat/nesting/json_serializer_spec.rb +173 -0
- data/spec/hat/relation_includes_spec.rb +185 -0
- data/spec/hat/sideloading/json_deserializer_spec.rb +93 -0
- data/spec/hat/sideloading/json_serializer_performance_spec.rb +51 -0
- data/spec/hat/sideloading/json_serializer_spec.rb +185 -0
- data/spec/hat/sideloading/sideload_map_spec.rb +59 -0
- data/spec/hat/support/company_deserializer_mapping.rb +11 -0
- data/spec/hat/support/person_deserializer_mapping.rb +9 -0
- data/spec/hat/support/spec_models.rb +89 -0
- data/spec/hat/support/spec_nesting_serializers.rb +41 -0
- data/spec/hat/support/spec_sideloading_serializers.rb +41 -0
- data/spec/hat/type_key_resolver_spec.rb +19 -0
- data/spec/spec_helper.rb +8 -0
- metadata +214 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# Provides an expanded version of includes for use in queries where ids for has_many relationships are
|
2
|
+
# going to be referenced such as with the json serializer. Eradicates n+1 issues in these instances.
|
3
|
+
module Hat
|
4
|
+
class ExpandedRelationIncludes
|
5
|
+
|
6
|
+
def initialize(relation_includes, serializer)
|
7
|
+
@relation_includes = relation_includes
|
8
|
+
@serializer = serializer
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_a
|
12
|
+
@expanded_includes ||= begin
|
13
|
+
expanded_includes = []
|
14
|
+
expand_includes(serializer.class, relation_includes, expanded_includes)
|
15
|
+
expanded_includes
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :serializer, :relation_includes
|
22
|
+
|
23
|
+
def expand_includes(target_serializer_class, base_includes, expanded_includes)
|
24
|
+
|
25
|
+
append_has_many_includes(target_serializer_class, base_includes, expanded_includes)
|
26
|
+
|
27
|
+
base_includes.each do |include_item|
|
28
|
+
if include_item.kind_of?(Symbol)
|
29
|
+
expand_includes_for_symbol(include_item, target_serializer_class, expanded_includes)
|
30
|
+
elsif include_item.kind_of?(Hash)
|
31
|
+
expand_includes_for_hash(include_item, target_serializer_class, expanded_includes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def expand_includes_for_symbol(include_item, target_serializer_class, expanded_includes)
|
38
|
+
|
39
|
+
related_type = target_serializer_class.serializable_type.reflections[include_item].class_name.constantize
|
40
|
+
related_serializer = SerializerRegistry.instance.get(target_serializer_class.serializer_type, related_type)
|
41
|
+
new_nested_includes = []
|
42
|
+
|
43
|
+
append_has_many_includes(related_serializer, [], new_nested_includes)
|
44
|
+
|
45
|
+
if new_nested_includes.empty?
|
46
|
+
expanded_includes << include_item
|
47
|
+
else
|
48
|
+
expanded_includes << { include_item => new_nested_includes }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def expand_includes_for_hash(include_item, target_serializer_class, expanded_includes)
|
54
|
+
|
55
|
+
nested_include_key = include_item.keys.first
|
56
|
+
nested_includes = include_item[nested_include_key]
|
57
|
+
|
58
|
+
related_type = target_serializer_class.serializable_type.reflections[nested_include_key].class_name.constantize
|
59
|
+
related_serializer = SerializerRegistry.instance.get(target_serializer_class.serializer_type, related_type)
|
60
|
+
new_nested_includes = []
|
61
|
+
|
62
|
+
expanded_includes << { nested_include_key => new_nested_includes }
|
63
|
+
expand_includes(related_serializer, nested_includes, new_nested_includes)
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def append_has_many_includes(related_serializer, base_includes, expanded_includes)
|
68
|
+
|
69
|
+
related_serializer.has_manys.each do |has_many_attr|
|
70
|
+
expanded_includes << has_many_attr unless RelationIncludes.new(*base_includes).find(has_many_attr)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#type/id map for mapping string or symbol keys and ids to objects.
|
2
|
+
module Hat
|
3
|
+
class IdentityMap
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
#Optimised for speed...as tempting as it might be, don't rewrite this to use hash with indifferent access as it is slower.
|
7
|
+
@identity_map = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(type, id)
|
11
|
+
if id_map = identity_map[type.to_sym]
|
12
|
+
id_map[id]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def put(type, id, object)
|
17
|
+
|
18
|
+
type_symbol = type.to_sym
|
19
|
+
|
20
|
+
unless identity_map[type_symbol]
|
21
|
+
identity_map[type_symbol] = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
identity_map[type_symbol][id] = object
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
@identity_map
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
identity_map.inspect
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_accessor :identity_map
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative 'serializer_registry'
|
2
|
+
|
3
|
+
module Hat
|
4
|
+
module JsonSerializerDsl
|
5
|
+
|
6
|
+
def self.apply_to(serializer_class)
|
7
|
+
serializer_class.class_attribute :_attributes
|
8
|
+
serializer_class.class_attribute :_relationships
|
9
|
+
serializer_class.class_attribute :_includable
|
10
|
+
serializer_class.class_attribute :_serializes
|
11
|
+
|
12
|
+
serializer_class._attributes = []
|
13
|
+
serializer_class._relationships = { has_ones: [], has_manys: [] }
|
14
|
+
|
15
|
+
serializer_class.extend(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def serializes(klass)
|
19
|
+
self._serializes = klass
|
20
|
+
Hat::SerializerRegistry.instance.register(serializer_type, klass, self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def serializable_type
|
24
|
+
self._serializes
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_ones
|
28
|
+
self._relationships[:has_ones]
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_manys
|
32
|
+
self._relationships[:has_manys]
|
33
|
+
end
|
34
|
+
|
35
|
+
def attributes(*args)
|
36
|
+
self._attributes = args
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_one(has_one)
|
40
|
+
self.has_ones << has_one
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_many(has_many)
|
44
|
+
self.has_manys << has_many
|
45
|
+
end
|
46
|
+
|
47
|
+
def includable(*includes)
|
48
|
+
self._includable = RelationIncludes.new(*includes)
|
49
|
+
end
|
50
|
+
|
51
|
+
def serializer_type
|
52
|
+
if self.ancestors.any? { |klass| klass == Hat::Sideloading::JsonSerializer }
|
53
|
+
:sideloading
|
54
|
+
elsif self.ancestors.any? { |klass| klass == Hat::Nesting::JsonSerializer }
|
55
|
+
:nesting
|
56
|
+
else
|
57
|
+
raise "Unsupported serializer_type. Must be one of either 'sideloading' or 'nesting' serializer."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#Adds methods that help a client to behave as if it's dealing with an ActiveModel object.
|
2
|
+
#Principle of least surprise here - if someone is working in Rails and using a model it should
|
3
|
+
#feel normal and they should be able to do all the things the'd do with an active model object except
|
4
|
+
#interact with the database.
|
5
|
+
module Hat
|
6
|
+
module Model
|
7
|
+
module ActsLikeActiveModel
|
8
|
+
|
9
|
+
def to_param
|
10
|
+
if self.respond_to?(:id)
|
11
|
+
self.id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "active_support/core_ext/class/attribute"
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
require_relative "transformers/registry"
|
6
|
+
require_relative "has_many_array"
|
7
|
+
|
8
|
+
#Mix in to PORO to get basic attribute definition with type coercion and defaults.
|
9
|
+
#class MyClass
|
10
|
+
# attribute :name
|
11
|
+
# attribute :date, type: :date_time
|
12
|
+
# attribute :tags, default: []
|
13
|
+
#
|
14
|
+
#NOTE: Be careful of adding default values other than primitives, arrays or basic hashes. Anything more
|
15
|
+
#Complex will need to be copied into each object rather than by direct reference - see the way
|
16
|
+
#arrays and hashes are handled in the 'default_for' method below.
|
17
|
+
module Hat
|
18
|
+
module Model
|
19
|
+
module Attributes
|
20
|
+
|
21
|
+
def initialize(attrs = {})
|
22
|
+
|
23
|
+
attrs = attrs.with_indifferent_access
|
24
|
+
|
25
|
+
attributes.each do |attr_name, attr_options|
|
26
|
+
raw_value = attrs[attr_name] == nil ? default_for(attr_options) : attrs[attr_name]
|
27
|
+
set_raw_value(attr_name, raw_value, true) unless raw_value == nil
|
28
|
+
end
|
29
|
+
|
30
|
+
self.errors = attrs[:errors] || {}
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def attributes
|
35
|
+
self.class._attributes
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_many_changed(has_many_name)
|
39
|
+
send("#{has_many_name.to_s.singularize}_ids=", send(has_many_name).map(&:id).compact)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def set_raw_value(attr_name, raw_value, apply_if_read_only = false)
|
45
|
+
|
46
|
+
attr_def = attributes[attr_name]
|
47
|
+
value = transformed_value(attr_def[:type], raw_value)
|
48
|
+
|
49
|
+
if attr_def[:read_only] && apply_if_read_only
|
50
|
+
instance_variable_set("@#{attr_name}", value)
|
51
|
+
elsif
|
52
|
+
self.send("#{attr_name}=", value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def transformed_value(type, raw_value)
|
57
|
+
if type
|
58
|
+
transformer_registry.from_raw(type, raw_value)
|
59
|
+
else
|
60
|
+
raw_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def transformer_registry
|
65
|
+
Transformers::Registry.instance
|
66
|
+
end
|
67
|
+
|
68
|
+
#make sure we don't pass references to the same default object to each instance. Copy/dup where appropriate
|
69
|
+
def default_for(options)
|
70
|
+
assert_default_type_valid(options)
|
71
|
+
default = options[:default]
|
72
|
+
if default.kind_of? Array
|
73
|
+
[].concat(default)
|
74
|
+
elsif default.kind_of? Hash
|
75
|
+
default.dup
|
76
|
+
else
|
77
|
+
default
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def assert_default_type_valid(options)
|
82
|
+
if options[:default]
|
83
|
+
default_class = options[:default].class
|
84
|
+
unless [Array, Hash, Integer, Float, String].include? default_class
|
85
|
+
raise "Default values of type #{default_class.name} are not supported."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_belongs_to_value(attr_name, value)
|
91
|
+
instance_variable_set("@#{attr_name}", value)
|
92
|
+
send("#{attr_name}_id=", value.try(:id))
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_has_many_value(attr_name, value)
|
96
|
+
instance_variable_set("@#{attr_name}", HasManyArray.new(value, self, attr_name))
|
97
|
+
has_many_changed(attr_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
#----Module Inclusion
|
101
|
+
|
102
|
+
def self.included(base)
|
103
|
+
base.class_attribute :_attributes
|
104
|
+
base._attributes = {}
|
105
|
+
base.extend(ClassMethods)
|
106
|
+
base.send(:attr_accessor, :errors)
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
|
111
|
+
def attribute(name, options = {})
|
112
|
+
self._attributes[name] = options
|
113
|
+
if options[:read_only]
|
114
|
+
self.send(:attr_reader, name.to_sym)
|
115
|
+
else
|
116
|
+
self.send(:attr_accessor, name.to_sym)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def belongs_to(attr_name, options = {})
|
121
|
+
|
122
|
+
id_attr_name = "#{attr_name}_id"
|
123
|
+
id_setter_method_name = "#{id_attr_name}="
|
124
|
+
|
125
|
+
send(:attr_reader, attr_name)
|
126
|
+
send(:attr_reader, id_attr_name)
|
127
|
+
|
128
|
+
define_method("#{attr_name}=") do |value|
|
129
|
+
set_belongs_to_value(attr_name, value)
|
130
|
+
end
|
131
|
+
|
132
|
+
define_method(id_setter_method_name) do |value|
|
133
|
+
instance_variable_set("@#{id_attr_name}", value)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def has_many(attr_name, options = {})
|
139
|
+
|
140
|
+
ids_attr_name = "#{attr_name.to_s.singularize}_ids"
|
141
|
+
id_setter_method_name = "#{ids_attr_name}="
|
142
|
+
|
143
|
+
send(:attr_reader, attr_name)
|
144
|
+
send(:attr_reader, ids_attr_name)
|
145
|
+
|
146
|
+
define_method("#{attr_name}=") do |value|
|
147
|
+
set_has_many_value(attr_name, value)
|
148
|
+
end
|
149
|
+
|
150
|
+
define_method(id_setter_method_name) do |value|
|
151
|
+
instance_variable_set("@#{ids_attr_name}", value)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Hat
|
2
|
+
module Model
|
3
|
+
class HasManyArray < SimpleDelegator
|
4
|
+
|
5
|
+
def initialize(array, owner, key)
|
6
|
+
|
7
|
+
if array.kind_of?(HasManyArray)
|
8
|
+
array = array.target_array
|
9
|
+
end
|
10
|
+
|
11
|
+
super(array)
|
12
|
+
|
13
|
+
@owner = owner
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
17
|
+
[:<<, :push, :delete, :delete_at].each do |method_name|
|
18
|
+
define_method(method_name) do |arg|
|
19
|
+
target_array.send(method_name, arg)
|
20
|
+
notify_owner
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
[:clear, :shift].each do |method_name|
|
25
|
+
define_method(method_name) do
|
26
|
+
target_array.send(method_name)
|
27
|
+
notify_owner
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def target_array
|
34
|
+
__getobj__
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :owner, :key
|
40
|
+
|
41
|
+
def notify_owner
|
42
|
+
owner.has_many_changed(self, key)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Hat
|
4
|
+
module Model
|
5
|
+
module Transformers
|
6
|
+
module DateTimeTransformer
|
7
|
+
|
8
|
+
def self.from_raw(value)
|
9
|
+
if value && value.kind_of?(String)
|
10
|
+
::DateTime.parse(value)
|
11
|
+
elsif value.kind_of?(DateTime) || value.kind_of?(Date) || value.kind_of?(Time)
|
12
|
+
value
|
13
|
+
elsif value == nil
|
14
|
+
nil
|
15
|
+
else
|
16
|
+
raise TransformError, "Cannot transform #{value.class.name} to DateTime"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.to_raw(date_time)
|
21
|
+
if date_time
|
22
|
+
date_time.iso8601
|
23
|
+
else
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative 'date_time_transformer'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Hat
|
5
|
+
module Model
|
6
|
+
module Transformers
|
7
|
+
class Registry
|
8
|
+
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def from_raw(type, value)
|
12
|
+
if transformer = get(type)
|
13
|
+
transformer.from_raw(value)
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_raw(type, value)
|
20
|
+
if transformer = get(type)
|
21
|
+
transformer.to_raw(value)
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(type)
|
28
|
+
registry[type.to_sym]
|
29
|
+
end
|
30
|
+
|
31
|
+
def register(type, transformer)
|
32
|
+
if existing_transformer = get(type)
|
33
|
+
raise "'#{type}' has already been registered as #{existing_transformer.name}"
|
34
|
+
else
|
35
|
+
registry[type.to_sym] = transformer
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def registry
|
42
|
+
@registry ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
class TransformError < StandardError ; end
|
48
|
+
|
49
|
+
#Register Common Transformers
|
50
|
+
Registry.instance.register(:date_time, Transformers::DateTimeTransformer)
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
data/lib/hat/model.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "active_support/core_ext/class/attribute"
|
2
|
+
require "active_support/json"
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require_relative '../base_json_serializer'
|
5
|
+
require_relative 'relation_loader'
|
6
|
+
|
7
|
+
module Hat
|
8
|
+
module Nesting
|
9
|
+
module JsonSerializer
|
10
|
+
|
11
|
+
include Hat::BaseJsonSerializer
|
12
|
+
|
13
|
+
def as_json(*args)
|
14
|
+
|
15
|
+
result[root_key] = Array(serializable).map do |serializable_item|
|
16
|
+
serializer = self.class.new(serializable_item, { result: {} })
|
17
|
+
serializer.includes(*relation_includes).serialize
|
18
|
+
end
|
19
|
+
|
20
|
+
result.merge({ meta: meta_data.merge(includes_meta_data) })
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize
|
24
|
+
assert_id_present(serializable)
|
25
|
+
attribute_hash.merge(relation_loader.relation_hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def relation_loader
|
31
|
+
@relation_loader ||= Relationloader.new({
|
32
|
+
serializable: serializable,
|
33
|
+
has_manys: has_manys,
|
34
|
+
has_ones: has_ones,
|
35
|
+
relation_includes: relation_includes
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.included(serializer_class)
|
40
|
+
JsonSerializerDsl.apply_to(serializer_class)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Hat
|
2
|
+
module Nesting
|
3
|
+
class Relationloader
|
4
|
+
|
5
|
+
def initialize(opts = {})
|
6
|
+
@serializable = opts[:serializable]
|
7
|
+
@has_ones = opts[:has_ones]
|
8
|
+
@has_manys = opts[:has_manys]
|
9
|
+
@relation_includes = opts[:relation_includes]
|
10
|
+
end
|
11
|
+
|
12
|
+
def relation_hash
|
13
|
+
has_one_hash.merge(has_many_hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :serializable, :has_ones, :has_manys, :relation_includes
|
19
|
+
|
20
|
+
def has_one_hash
|
21
|
+
has_ones.inject({}) { |has_one_hash, attr_name| serialize_has_one_relation(has_one_hash, attr_name) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def serialize_has_one_relation(has_one_hash, attr_name)
|
25
|
+
|
26
|
+
id_attr = "#{attr_name}_id"
|
27
|
+
|
28
|
+
if related_item = relation_for(attr_name)
|
29
|
+
has_one_hash[attr_name] = serialize_nested_item_with_includes(related_item, includes_for_attr(attr_name))
|
30
|
+
elsif serializable.respond_to?(id_attr)
|
31
|
+
has_one_hash[id_attr] = serializable.send(id_attr)
|
32
|
+
else
|
33
|
+
has_one_hash[id_attr] = serializable.send(attr_name).try(:id)
|
34
|
+
end
|
35
|
+
|
36
|
+
has_one_hash
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def has_many_hash
|
42
|
+
has_manys.inject({}) { |has_many_hash, attr_name| serialize_has_many_relations(has_many_hash, attr_name) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def serialize_has_many_relations(has_many_hash, attr_name)
|
46
|
+
if related_items = relation_for(attr_name)
|
47
|
+
has_many_hash[attr_name] = related_items.map do |related_item|
|
48
|
+
serialize_nested_item_with_includes(related_item, includes_for_attr(attr_name))
|
49
|
+
end
|
50
|
+
else
|
51
|
+
has_many_relation = serializable.send(attr_name) || []
|
52
|
+
has_many_hash["#{attr_name.to_s.singularize}_ids"] = has_many_relation.map(&:id)
|
53
|
+
end
|
54
|
+
|
55
|
+
has_many_hash
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def relation_for(attr_name)
|
60
|
+
serializable.send(attr_name) if relation_includes.includes_relation?(attr_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def serialize_nested_item_with_includes(serializable_item, includes)
|
64
|
+
|
65
|
+
serializer = serializer_for(serializable_item)
|
66
|
+
|
67
|
+
serializer.includes(*includes).serialize
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def includes_for_attr(attr_name)
|
72
|
+
relation_includes.nested_includes_for(attr_name) || []
|
73
|
+
end
|
74
|
+
|
75
|
+
def serializer_for(serializable_item)
|
76
|
+
|
77
|
+
serializer_class_for(serializable_item).new(serializable_item, {
|
78
|
+
result: {}
|
79
|
+
})
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def serializer_class_for(serializable)
|
84
|
+
Hat::SerializerRegistry.instance.get(:nesting, serializable.class)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|