http_api_tools 0.3.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 +479 -0
- data/Rakefile +4 -0
- data/http_api_tools.gemspec +29 -0
- data/lib/http_api_tools/base_json_serializer.rb +103 -0
- data/lib/http_api_tools/expanded_relation_includes.rb +77 -0
- data/lib/http_api_tools/identity_map.rb +42 -0
- data/lib/http_api_tools/json_serializer_dsl.rb +62 -0
- data/lib/http_api_tools/model/acts_like_active_model.rb +16 -0
- data/lib/http_api_tools/model/attributes.rb +159 -0
- data/lib/http_api_tools/model/has_many_array.rb +47 -0
- data/lib/http_api_tools/model/transformers/date_time_transformer.rb +31 -0
- data/lib/http_api_tools/model/transformers/registry.rb +55 -0
- data/lib/http_api_tools/model.rb +2 -0
- data/lib/http_api_tools/nesting/json_serializer.rb +45 -0
- data/lib/http_api_tools/nesting/relation_loader.rb +89 -0
- data/lib/http_api_tools/relation_includes.rb +146 -0
- data/lib/http_api_tools/serializer_registry.rb +27 -0
- data/lib/http_api_tools/sideloading/json_deserializer.rb +121 -0
- data/lib/http_api_tools/sideloading/json_deserializer_mapping.rb +27 -0
- data/lib/http_api_tools/sideloading/json_serializer.rb +125 -0
- data/lib/http_api_tools/sideloading/relation_sideloader.rb +79 -0
- data/lib/http_api_tools/sideloading/sideload_map.rb +54 -0
- data/lib/http_api_tools/type_key_resolver.rb +27 -0
- data/lib/http_api_tools/version.rb +3 -0
- data/lib/http_api_tools.rb +10 -0
- data/reports/empty.png +0 -0
- data/reports/minus.png +0 -0
- data/reports/plus.png +0 -0
- data/spec/http_api_tools/expanded_relation_includes_spec.rb +31 -0
- data/spec/http_api_tools/identity_map_spec.rb +31 -0
- data/spec/http_api_tools/model/attributes_spec.rb +170 -0
- data/spec/http_api_tools/model/has_many_array_spec.rb +48 -0
- data/spec/http_api_tools/model/transformers/date_time_transformer_spec.rb +36 -0
- data/spec/http_api_tools/model/transformers/registry_spec.rb +53 -0
- data/spec/http_api_tools/nesting/json_serializer_spec.rb +173 -0
- data/spec/http_api_tools/relation_includes_spec.rb +196 -0
- data/spec/http_api_tools/sideloading/json_deserializer_spec.rb +93 -0
- data/spec/http_api_tools/sideloading/json_serializer_performance_spec.rb +51 -0
- data/spec/http_api_tools/sideloading/json_serializer_spec.rb +174 -0
- data/spec/http_api_tools/sideloading/sideload_map_spec.rb +59 -0
- data/spec/http_api_tools/support/company_deserializer_mapping.rb +11 -0
- data/spec/http_api_tools/support/person_deserializer_mapping.rb +9 -0
- data/spec/http_api_tools/support/spec_models.rb +89 -0
- data/spec/http_api_tools/support/spec_nesting_serializers.rb +41 -0
- data/spec/http_api_tools/support/spec_sideloading_serializers.rb +41 -0
- data/spec/http_api_tools/type_key_resolver_spec.rb +19 -0
- data/spec/spec_helper.rb +8 -0
- metadata +214 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#Takes a json response based on the active-model-serializer relationship sideloading pattern
|
4
|
+
#and given a root object key, builds an object graph with cyclic relationships.
|
5
|
+
#See the id based pattern here - https://github.com/rails-api/active_model_serializers
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
require_relative 'sideload_map'
|
8
|
+
require_relative '../identity_map'
|
9
|
+
|
10
|
+
module HttpApiTools
|
11
|
+
module Sideloading
|
12
|
+
class JsonDeserializer
|
13
|
+
|
14
|
+
def initialize(json)
|
15
|
+
@json = json
|
16
|
+
@root_key = json['meta']['root_key'].to_s
|
17
|
+
@identity_map = IdentityMap.new
|
18
|
+
@sideload_map = SideloadMap.new(json, root_key)
|
19
|
+
@key_to_class_mappings = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize
|
23
|
+
json[root_key].map do |json_item|
|
24
|
+
create_from_json_item(target_class_for_key(root_key), json_item)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_accessor :json, :root_key, :sideload_map, :identity_map, :key_to_class_mappings
|
31
|
+
|
32
|
+
def create_from_json_item(target_class, json_item)
|
33
|
+
|
34
|
+
return nil unless target_class
|
35
|
+
|
36
|
+
existing_deserialized = identity_map.get(target_class.name, json_item['id'])
|
37
|
+
return existing_deserialized if existing_deserialized
|
38
|
+
|
39
|
+
relations = {}
|
40
|
+
target_class_name = target_class.name
|
41
|
+
|
42
|
+
#we have to add this before we process subtree or we'll get circular issues
|
43
|
+
target = target_class.new(json_item.with_indifferent_access)
|
44
|
+
identity_map.put(target_class_name, json_item['id'], target)
|
45
|
+
|
46
|
+
links = json_item['links'] || {}
|
47
|
+
apply_linked_relations_to_target(target, links)
|
48
|
+
|
49
|
+
target
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_linked_relations_to_target(target, links)
|
54
|
+
|
55
|
+
target_class_name = target.class.name
|
56
|
+
|
57
|
+
links.each do |relation_name, value|
|
58
|
+
if value.kind_of?(Array)
|
59
|
+
target.send("#{relation_name}=", create_has_manys(target_class_name, relation_name, value))
|
60
|
+
target.send("#{relation_name.singularize}_ids=", value)
|
61
|
+
else
|
62
|
+
target.send("#{relation_name}=", create_belongs_to(target_class_name, relation_name, value))
|
63
|
+
target.send("#{relation_name}_id=", value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_belongs_to(parent_class_name, sideload_key, id)
|
69
|
+
|
70
|
+
sideload_key = mapped_sideload_key_for(parent_class_name, sideload_key)
|
71
|
+
|
72
|
+
if sideloaded_json = sideload_map.get(sideload_key, id)
|
73
|
+
sideloaded_object = create_from_json_item(target_class_for_key(sideload_key), sideloaded_json)
|
74
|
+
else
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_has_manys(parent_class_name, sideload_key, ids)
|
80
|
+
sideload_key = mapped_sideload_key_for(parent_class_name, sideload_key)
|
81
|
+
target_class = target_class_for_key(sideload_key)
|
82
|
+
sideloaded_json_items = sideload_map.get_all(sideload_key, ids)
|
83
|
+
|
84
|
+
sideloaded_json_items.map do |json_item|
|
85
|
+
create_from_json_item(target_class, json_item)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def mapped_sideload_key_for(parent_class_name, sideload_key)
|
90
|
+
|
91
|
+
resolve_class_mappings_for(parent_class_name)
|
92
|
+
class_mapping = key_to_class_mappings[parent_class_name]
|
93
|
+
|
94
|
+
if attribute_mapping = class_mapping[sideload_key.to_sym]
|
95
|
+
return attribute_mapping.name.underscore
|
96
|
+
end
|
97
|
+
|
98
|
+
sideload_key
|
99
|
+
end
|
100
|
+
|
101
|
+
def resolve_class_mappings_for(parent_class_name)
|
102
|
+
unless key_to_class_mappings[parent_class_name]
|
103
|
+
mapping_class_name = "#{parent_class_name}DeserializerMapping"
|
104
|
+
if Object.const_defined?(mapping_class_name)
|
105
|
+
key_to_class_mappings[parent_class_name] = mapping_class_name.constantize.mappings
|
106
|
+
else
|
107
|
+
key_to_class_mappings[parent_class_name] = {}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def target_class_for_key(key)
|
113
|
+
key.to_s.singularize.camelize.constantize
|
114
|
+
rescue NameError
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_support/core_ext/class/attribute"
|
2
|
+
|
3
|
+
module HttpApiTools
|
4
|
+
module Sideloading
|
5
|
+
module JsonDeserializerMapping
|
6
|
+
|
7
|
+
#----Module Inclusion
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_attribute :_mappings
|
11
|
+
base._mappings = {}
|
12
|
+
base.extend(Dsl)
|
13
|
+
end
|
14
|
+
|
15
|
+
module Dsl
|
16
|
+
def map(attr_name, target_class)
|
17
|
+
self._mappings[attr_name] = target_class
|
18
|
+
end
|
19
|
+
|
20
|
+
def mappings
|
21
|
+
self._mappings
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require_relative '../base_json_serializer'
|
2
|
+
require_relative '../json_serializer_dsl'
|
3
|
+
require_relative 'relation_sideloader'
|
4
|
+
|
5
|
+
module HttpApiTools
|
6
|
+
module Sideloading
|
7
|
+
module JsonSerializer
|
8
|
+
|
9
|
+
include HttpApiTools::BaseJsonSerializer
|
10
|
+
|
11
|
+
attr_reader :relation_includes, :result, :attribute_mappings, :has_one_mappings, :has_many_mappings, :cached
|
12
|
+
|
13
|
+
def initialize(serializable, attrs = {})
|
14
|
+
super
|
15
|
+
@identity_map = attrs[:identity_map] || IdentityMap.new
|
16
|
+
@type_key_resolver = attrs[:type_key_resolver] || TypeKeyResolver.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json(*args)
|
20
|
+
|
21
|
+
result[root_key] = []
|
22
|
+
|
23
|
+
Array(serializable).each do |serializable_item|
|
24
|
+
serialize_item_and_cache_relationships(serializable_item)
|
25
|
+
end
|
26
|
+
|
27
|
+
result.merge({ meta: meta_data.merge(includes_meta_data), linked: relation_sideloader.as_json })
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_sideloaded_hash
|
31
|
+
hash = attribute_hash.merge(links: has_one_hash.merge(has_many_hash))
|
32
|
+
relation_sideloader.sideload_relations
|
33
|
+
hash
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :relation_sideloader
|
39
|
+
|
40
|
+
def serialize_item_and_cache_relationships(serializable_item)
|
41
|
+
|
42
|
+
assert_id_present(serializable_item)
|
43
|
+
|
44
|
+
serializer = serializer_for(serializable_item)
|
45
|
+
hashed = { id: serializable_item.id }
|
46
|
+
|
47
|
+
result[root_key] << hashed
|
48
|
+
|
49
|
+
hashed.merge!(serializer.includes(*relation_includes.to_a).as_sideloaded_hash)
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_one_hash
|
54
|
+
|
55
|
+
has_one_hash = {}
|
56
|
+
|
57
|
+
has_ones.each do |attr_name|
|
58
|
+
|
59
|
+
id_attr = "#{attr_name}_id"
|
60
|
+
|
61
|
+
#Use id attr if possible as it's cheaper than referencing the object
|
62
|
+
if serializable.respond_to?(id_attr)
|
63
|
+
related_id = serializable.send(id_attr)
|
64
|
+
else
|
65
|
+
related_id = serializable.send(attr_name).try(:id)
|
66
|
+
end
|
67
|
+
|
68
|
+
has_one_hash[attr_name] = related_id
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
has_one_hash
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def has_many_hash
|
78
|
+
|
79
|
+
has_many_hash = {}
|
80
|
+
|
81
|
+
has_manys.each do |attr_name|
|
82
|
+
has_many_relation = serializable.send(attr_name) || []
|
83
|
+
has_many_hash[attr_name] = has_many_relation.map(&:id)
|
84
|
+
end
|
85
|
+
|
86
|
+
has_many_hash
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
def relation_sideloader
|
91
|
+
@relation_sideloader ||= RelationSideloader.new(
|
92
|
+
serializable: serializable,
|
93
|
+
has_ones: has_ones,
|
94
|
+
has_manys: has_manys,
|
95
|
+
relation_includes: relation_includes,
|
96
|
+
identity_map: identity_map,
|
97
|
+
type_key_resolver: type_key_resolver,
|
98
|
+
result: result
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def serializer_for(serializable_item)
|
103
|
+
|
104
|
+
serializer_class = serializer_class_for(serializable_item)
|
105
|
+
|
106
|
+
serializer_class.new(serializable_item, {
|
107
|
+
result: result,
|
108
|
+
identity_map: identity_map,
|
109
|
+
type_key_resolver: type_key_resolver
|
110
|
+
})
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
def serializer_class_for(serializable)
|
115
|
+
HttpApiTools::SerializerRegistry.instance.get(:sideloading, serializable.class)
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.included(serializer_class)
|
119
|
+
JsonSerializerDsl.apply_to(serializer_class)
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module HttpApiTools
|
2
|
+
module Sideloading
|
3
|
+
class RelationSideloader
|
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
|
+
@result = opts[:result]
|
11
|
+
@identity_map = opts[:identity_map]
|
12
|
+
@type_key_resolver = opts[:type_key_resolver]
|
13
|
+
end
|
14
|
+
|
15
|
+
def sideload_relations
|
16
|
+
sideload_has_ones
|
17
|
+
sideload_has_manys
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json
|
21
|
+
identity_map.to_hash.inject({}) do |sideload_data, (key, type_map)|
|
22
|
+
sideload_data[key] = type_map.values
|
23
|
+
sideload_data
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :serializable, :has_ones, :has_manys, :relation_includes, :identity_map, :type_key_resolver, :result
|
30
|
+
|
31
|
+
def sideload_has_ones
|
32
|
+
|
33
|
+
has_ones.each do |attr_name|
|
34
|
+
if related_item = relation_for(attr_name)
|
35
|
+
type_key = type_key_for(related_item)
|
36
|
+
sideload_item(related_item, attr_name, type_key) unless identity_map.get(type_key, related_item.id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def sideload_has_manys
|
42
|
+
|
43
|
+
has_manys.each do |attr_name|
|
44
|
+
|
45
|
+
if related_items = relation_for(attr_name)
|
46
|
+
type_key = nil
|
47
|
+
related_items.each do |related_item|
|
48
|
+
type_key ||= type_key_for(related_item)
|
49
|
+
sideload_item(related_item, attr_name, type_key) unless identity_map.get(type_key, related_item.id)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def relation_for(attr_name)
|
57
|
+
serializable.send(attr_name) if relation_includes.includes_relation?(attr_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def sideload_item(related, attr_name, type_key)
|
61
|
+
serializer_class = serializer_class_for(related)
|
62
|
+
includes = relation_includes.nested_includes_for(attr_name) || []
|
63
|
+
sideloaded_hash = serializer_class.new(related, { result: result, identity_map: identity_map, type_key_resolver: type_key_resolver }).includes(*includes).as_sideloaded_hash
|
64
|
+
|
65
|
+
identity_map.put(type_key, related.id, sideloaded_hash)
|
66
|
+
end
|
67
|
+
|
68
|
+
def type_key_for(related)
|
69
|
+
type_key_resolver.for_class(related.class)
|
70
|
+
end
|
71
|
+
|
72
|
+
def serializer_class_for(serializable)
|
73
|
+
HttpApiTools::SerializerRegistry.instance.get(:sideloading, serializable.class)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
require 'http_api_tools/identity_map'
|
3
|
+
#Holds a fast access map of all sideloaded json for deserialization
|
4
|
+
|
5
|
+
module HttpApiTools
|
6
|
+
module Sideloading
|
7
|
+
class SideloadMap
|
8
|
+
|
9
|
+
def initialize(json, root_key)
|
10
|
+
|
11
|
+
@root_key = root_key
|
12
|
+
@identity_map = IdentityMap.new
|
13
|
+
|
14
|
+
build_from_json(json)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(type, id)
|
18
|
+
identity_map.get(type.singularize, id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_all(type, ids)
|
22
|
+
ids.map { |id| get(type, id)}.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
identity_map.inspect
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_accessor :root_key, :identity_map
|
32
|
+
|
33
|
+
|
34
|
+
def put(type, id, object)
|
35
|
+
identity_map.put(type.singularize, id, object)
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_from_json(json)
|
39
|
+
|
40
|
+
json[root_key].each do |json_item|
|
41
|
+
put(root_key, json_item['id'], json_item)
|
42
|
+
end
|
43
|
+
|
44
|
+
if json['linked']
|
45
|
+
json['linked'].each do |type_key, sideloaded_json_item|
|
46
|
+
sideloaded_json_item.each { |json_item| put(type_key, json_item['id'], json_item) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#Provides a cache for resolved type keys against classes. When serializing a lot of
|
2
|
+
#relationships this can have a significant improvement on performance.
|
3
|
+
|
4
|
+
module HttpApiTools
|
5
|
+
class TypeKeyResolver
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def for_class(klass)
|
12
|
+
class_name = klass.name
|
13
|
+
cache[class_name] || resolve_and_store_type_key_for(class_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :cache
|
19
|
+
|
20
|
+
def resolve_and_store_type_key_for(class_name)
|
21
|
+
type_key = class_name.underscore.pluralize
|
22
|
+
cache[class_name] = type_key
|
23
|
+
type_key
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "http_api_tools/version"
|
2
|
+
require 'http_api_tools/nesting/json_serializer'
|
3
|
+
require 'http_api_tools/sideloading/json_serializer'
|
4
|
+
require 'http_api_tools/sideloading/json_deserializer'
|
5
|
+
require 'http_api_tools/model'
|
6
|
+
require 'http_api_tools/relation_includes'
|
7
|
+
|
8
|
+
module HttpApiTools
|
9
|
+
# Your code goes here...
|
10
|
+
end
|