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.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +479 -0
  7. data/Rakefile +4 -0
  8. data/http_api_tools.gemspec +29 -0
  9. data/lib/http_api_tools/base_json_serializer.rb +103 -0
  10. data/lib/http_api_tools/expanded_relation_includes.rb +77 -0
  11. data/lib/http_api_tools/identity_map.rb +42 -0
  12. data/lib/http_api_tools/json_serializer_dsl.rb +62 -0
  13. data/lib/http_api_tools/model/acts_like_active_model.rb +16 -0
  14. data/lib/http_api_tools/model/attributes.rb +159 -0
  15. data/lib/http_api_tools/model/has_many_array.rb +47 -0
  16. data/lib/http_api_tools/model/transformers/date_time_transformer.rb +31 -0
  17. data/lib/http_api_tools/model/transformers/registry.rb +55 -0
  18. data/lib/http_api_tools/model.rb +2 -0
  19. data/lib/http_api_tools/nesting/json_serializer.rb +45 -0
  20. data/lib/http_api_tools/nesting/relation_loader.rb +89 -0
  21. data/lib/http_api_tools/relation_includes.rb +146 -0
  22. data/lib/http_api_tools/serializer_registry.rb +27 -0
  23. data/lib/http_api_tools/sideloading/json_deserializer.rb +121 -0
  24. data/lib/http_api_tools/sideloading/json_deserializer_mapping.rb +27 -0
  25. data/lib/http_api_tools/sideloading/json_serializer.rb +125 -0
  26. data/lib/http_api_tools/sideloading/relation_sideloader.rb +79 -0
  27. data/lib/http_api_tools/sideloading/sideload_map.rb +54 -0
  28. data/lib/http_api_tools/type_key_resolver.rb +27 -0
  29. data/lib/http_api_tools/version.rb +3 -0
  30. data/lib/http_api_tools.rb +10 -0
  31. data/reports/empty.png +0 -0
  32. data/reports/minus.png +0 -0
  33. data/reports/plus.png +0 -0
  34. data/spec/http_api_tools/expanded_relation_includes_spec.rb +31 -0
  35. data/spec/http_api_tools/identity_map_spec.rb +31 -0
  36. data/spec/http_api_tools/model/attributes_spec.rb +170 -0
  37. data/spec/http_api_tools/model/has_many_array_spec.rb +48 -0
  38. data/spec/http_api_tools/model/transformers/date_time_transformer_spec.rb +36 -0
  39. data/spec/http_api_tools/model/transformers/registry_spec.rb +53 -0
  40. data/spec/http_api_tools/nesting/json_serializer_spec.rb +173 -0
  41. data/spec/http_api_tools/relation_includes_spec.rb +196 -0
  42. data/spec/http_api_tools/sideloading/json_deserializer_spec.rb +93 -0
  43. data/spec/http_api_tools/sideloading/json_serializer_performance_spec.rb +51 -0
  44. data/spec/http_api_tools/sideloading/json_serializer_spec.rb +174 -0
  45. data/spec/http_api_tools/sideloading/sideload_map_spec.rb +59 -0
  46. data/spec/http_api_tools/support/company_deserializer_mapping.rb +11 -0
  47. data/spec/http_api_tools/support/person_deserializer_mapping.rb +9 -0
  48. data/spec/http_api_tools/support/spec_models.rb +89 -0
  49. data/spec/http_api_tools/support/spec_nesting_serializers.rb +41 -0
  50. data/spec/http_api_tools/support/spec_sideloading_serializers.rb +41 -0
  51. data/spec/http_api_tools/type_key_resolver_spec.rb +19 -0
  52. data/spec/spec_helper.rb +8 -0
  53. 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,2 @@
1
+ require_relative "model/attributes"
2
+ require_relative "model/acts_like_active_model"
@@ -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