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,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,3 @@
1
+ module HttpApiTools
2
+ VERSION = "0.3.0"
3
+ 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