http-api-tools 0.1.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 +485 -0
  7. data/Rakefile +4 -0
  8. data/http-api-tools.gemspec +29 -0
  9. data/lib/hat/base_json_serializer.rb +107 -0
  10. data/lib/hat/expanded_relation_includes.rb +77 -0
  11. data/lib/hat/identity_map.rb +42 -0
  12. data/lib/hat/json_serializer_dsl.rb +62 -0
  13. data/lib/hat/model/acts_like_active_model.rb +16 -0
  14. data/lib/hat/model/attributes.rb +159 -0
  15. data/lib/hat/model/has_many_array.rb +47 -0
  16. data/lib/hat/model/transformers/date_time_transformer.rb +31 -0
  17. data/lib/hat/model/transformers/registry.rb +55 -0
  18. data/lib/hat/model.rb +2 -0
  19. data/lib/hat/nesting/json_serializer.rb +45 -0
  20. data/lib/hat/nesting/relation_loader.rb +89 -0
  21. data/lib/hat/relation_includes.rb +140 -0
  22. data/lib/hat/serializer_registry.rb +27 -0
  23. data/lib/hat/sideloading/json_deserializer.rb +121 -0
  24. data/lib/hat/sideloading/json_deserializer_mapping.rb +27 -0
  25. data/lib/hat/sideloading/json_serializer.rb +125 -0
  26. data/lib/hat/sideloading/relation_sideloader.rb +79 -0
  27. data/lib/hat/sideloading/sideload_map.rb +54 -0
  28. data/lib/hat/type_key_resolver.rb +27 -0
  29. data/lib/hat/version.rb +3 -0
  30. data/lib/hat.rb +9 -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/hat/expanded_relation_includes_spec.rb +32 -0
  35. data/spec/hat/identity_map_spec.rb +31 -0
  36. data/spec/hat/model/attributes_spec.rb +170 -0
  37. data/spec/hat/model/has_many_array_spec.rb +48 -0
  38. data/spec/hat/model/transformers/date_time_transformer_spec.rb +36 -0
  39. data/spec/hat/model/transformers/registry_spec.rb +53 -0
  40. data/spec/hat/nesting/json_serializer_spec.rb +173 -0
  41. data/spec/hat/relation_includes_spec.rb +185 -0
  42. data/spec/hat/sideloading/json_deserializer_spec.rb +93 -0
  43. data/spec/hat/sideloading/json_serializer_performance_spec.rb +51 -0
  44. data/spec/hat/sideloading/json_serializer_spec.rb +185 -0
  45. data/spec/hat/sideloading/sideload_map_spec.rb +59 -0
  46. data/spec/hat/support/company_deserializer_mapping.rb +11 -0
  47. data/spec/hat/support/person_deserializer_mapping.rb +9 -0
  48. data/spec/hat/support/spec_models.rb +89 -0
  49. data/spec/hat/support/spec_nesting_serializers.rb +41 -0
  50. data/spec/hat/support/spec_sideloading_serializers.rb +41 -0
  51. data/spec/hat/type_key_resolver_spec.rb +19 -0
  52. data/spec/spec_helper.rb +8 -0
  53. 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,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 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