http_api_tools 0.3.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 +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,47 @@
|
|
1
|
+
module HttpApiTools
|
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 HttpApiTools
|
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 HttpApiTools
|
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
|
+
|
@@ -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 HttpApiTools
|
8
|
+
module Nesting
|
9
|
+
module JsonSerializer
|
10
|
+
|
11
|
+
include HttpApiTools::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 HttpApiTools
|
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
|
+
HttpApiTools::SerializerRegistry.instance.get(:nesting, serializable.class)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'http_api_tools/expanded_relation_includes'
|
3
|
+
|
4
|
+
# Hopefully the spec is robust enough thttp_api_tools we can
|
5
|
+
# break this down and refactor as we go. I'm not
|
6
|
+
# happy with the complexity of it, but it's a
|
7
|
+
# reasonably complex problem
|
8
|
+
# ~ Stu
|
9
|
+
|
10
|
+
module HttpApiTools
|
11
|
+
class RelationIncludes < SimpleDelegator
|
12
|
+
include Comparable
|
13
|
+
|
14
|
+
def initialize(*includes)
|
15
|
+
@includes = includes.compact
|
16
|
+
super(@includes)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_params(params)
|
20
|
+
from_string(params[:include])
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_string(string)
|
24
|
+
return new if string.blank?
|
25
|
+
|
26
|
+
includes_hash = build_hash_from_string(string)
|
27
|
+
new(*flatten(includes_hash))
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
@to_s ||= begin
|
32
|
+
paths = []
|
33
|
+
includes.each do |item|
|
34
|
+
if item.is_a? Hash
|
35
|
+
stringify_keys(paths, item)
|
36
|
+
else
|
37
|
+
paths << [item]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
paths = paths.map { |p| p.join('.') }
|
41
|
+
paths.sort.join(',')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def &(other_includes)
|
46
|
+
hash = self.class.build_hash_from_string(to_s)
|
47
|
+
other_hash = self.class.build_hash_from_string(other_includes.to_s)
|
48
|
+
|
49
|
+
intersected_paths = (to_s.split(',') & other_includes.to_s.split(','))
|
50
|
+
self.class.from_string(intersected_paths.join(','))
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(other_includes)
|
54
|
+
to_s <=> other_includes.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
def include(additional_includes)
|
58
|
+
includes.concat(additional_includes)
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def includes_relation?(attr_name)
|
63
|
+
find(attr_name).present?
|
64
|
+
end
|
65
|
+
|
66
|
+
def find(attr_name)
|
67
|
+
includes.find do |relation|
|
68
|
+
(relation.kind_of?(Symbol) && attr_name == relation) ||
|
69
|
+
(relation.kind_of?(Hash) && relation.keys.include?(attr_name))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def nested_includes_for(attr_name)
|
74
|
+
nested = find(attr_name)
|
75
|
+
if nested.kind_of?(Hash)
|
76
|
+
nested[attr_name]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#Return an expanded version of the includes for use in a query.
|
81
|
+
#This api is still pretty rough and likely to change
|
82
|
+
def for_query(serializer_class)
|
83
|
+
RelationIncludes.new(*ExpandedRelationIncludes.new(self, serializer_class))
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
attr_accessor :includes
|
89
|
+
|
90
|
+
def self.build_hash_from_string(string)
|
91
|
+
includes_hash = {}
|
92
|
+
include_paths = string.split(',').map { |path| path.split('.') }
|
93
|
+
|
94
|
+
include_paths.each do |path|
|
95
|
+
current_hash = includes_hash
|
96
|
+
path.each do |token|
|
97
|
+
current_hash[token] ||= {}
|
98
|
+
current_hash = current_hash[token]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
includes_hash
|
102
|
+
end
|
103
|
+
|
104
|
+
def stringify_keys(top_level_paths, hash, path_attrs = [])
|
105
|
+
current_key = hash.keys.first
|
106
|
+
path_attrs << current_key
|
107
|
+
top_level_paths << path_attrs.dup
|
108
|
+
|
109
|
+
hash[current_key].each do |path_value|
|
110
|
+
if path_value.is_a? Hash
|
111
|
+
path_attrs << stringify_keys(top_level_paths, path_value, path_attrs)
|
112
|
+
else
|
113
|
+
top_level_paths << (path_attrs.dup << path_value)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Turns this:
|
119
|
+
#
|
120
|
+
# [ :tags, {images: [:comments]}, {reviews: [:author]} ]
|
121
|
+
#
|
122
|
+
# Into this:
|
123
|
+
#
|
124
|
+
# {
|
125
|
+
# tags: {},
|
126
|
+
# images: {
|
127
|
+
# comments: {}
|
128
|
+
# }
|
129
|
+
# reviews: {}
|
130
|
+
# }
|
131
|
+
# }
|
132
|
+
#
|
133
|
+
def self.flatten(hash)
|
134
|
+
result = []
|
135
|
+
hash.each do |k, v|
|
136
|
+
if v.keys.size == 0
|
137
|
+
result << k.to_sym
|
138
|
+
else
|
139
|
+
result << { "#{k}".to_sym => flatten(v) }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
result
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module HttpApiTools
|
4
|
+
class SerializerRegistry
|
5
|
+
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def get(type, class_name)
|
9
|
+
registry.fetch(type.to_sym, {})[class_name]
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(type, class_name, serializer)
|
13
|
+
if existing_serializer = get(type, class_name)
|
14
|
+
raise "A '#{type}' serializer for '#{class_name}' instances has already been registered as #{existing_serializer.name}"
|
15
|
+
else
|
16
|
+
registry[type.to_sym][class_name] = serializer
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def registry
|
23
|
+
@registry ||= { sideloading: {}, nesting: {} }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|