http-api-tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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