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
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ task :default => :spec
4
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'http_api_tools/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "http_api_tools"
8
+ spec.version = HttpApiTools::VERSION
9
+ spec.authors = ["Rob Monie"]
10
+ spec.email = ["robmonie@gmail.com"]
11
+ spec.description = %q{Http API Tools}
12
+ spec.summary = %q{Provides JSON serialization/deserialization and basic model attribute definition for client apps}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "activesupport"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "pry-debugger"
27
+ spec.add_development_dependency "ruby-prof"
28
+
29
+ end
@@ -0,0 +1,103 @@
1
+
2
+ require "active_support/core_ext/class/attribute"
3
+ require "active_support/json"
4
+ require 'active_support/core_ext/string/inflections'
5
+ require_relative 'relation_includes'
6
+ require_relative 'identity_map'
7
+ require_relative 'type_key_resolver'
8
+
9
+ module HttpApiTools
10
+ module BaseJsonSerializer
11
+
12
+ attr_reader :serializable, :relation_includes, :result, :meta_data
13
+
14
+ def initialize(serializable, attrs = {})
15
+ @serializable = serializable
16
+ @relation_includes = attrs[:relation_includes] || RelationIncludes.new
17
+ @result = attrs[:result] || {}
18
+ @meta_data = { type: root_key.to_s.singularize, root_key: root_key.to_s }
19
+ end
20
+
21
+
22
+ def attribute_hash
23
+
24
+ attribute_hash = {}
25
+
26
+ attributes.each do |attr_name|
27
+ if self.respond_to?(attr_name)
28
+ attribute_hash[attr_name] = self.send(attr_name)
29
+ else
30
+ attribute_hash[attr_name] = serializable.send(attr_name)
31
+ end
32
+ end
33
+
34
+ attribute_hash
35
+
36
+ end
37
+
38
+
39
+ def to_json(*args)
40
+ JSON.fast_generate(as_json)
41
+ end
42
+
43
+ def includes(*includes)
44
+
45
+ if includable
46
+ allowable_includes_to_add = RelationIncludes.new(*includes) & includable
47
+ else
48
+ allowable_includes_to_add = includes
49
+ end
50
+
51
+ relation_includes.include(allowable_includes_to_add)
52
+
53
+ self
54
+ end
55
+
56
+ def meta(data)
57
+ meta_data.merge!(data)
58
+ self
59
+ end
60
+
61
+ def attributes
62
+ self.class._attributes
63
+ end
64
+
65
+ def has_ones
66
+ self.class.has_ones
67
+ end
68
+
69
+ def has_manys
70
+ self.class.has_manys
71
+ end
72
+
73
+ def includable
74
+ self.class._includable
75
+ end
76
+
77
+ protected
78
+
79
+ attr_accessor :identity_map
80
+
81
+ private
82
+
83
+ attr_writer :relation_includes
84
+ attr_reader :type_key_resolver
85
+ attr_accessor :serializer_map, :meta_data
86
+
87
+ def root_key
88
+ @_root_key ||= self.class._serializes.name.split("::").last.underscore.pluralize.to_sym
89
+ end
90
+
91
+ def includes_meta_data
92
+ {
93
+ includable: includable.to_s.blank? ? '*' : includable.to_s,
94
+ included: relation_includes.to_s
95
+ }
96
+ end
97
+
98
+ def assert_id_present(serializable_item)
99
+ raise "serializable items must have an id attribute" unless serializable_item.respond_to?(:id)
100
+ end
101
+
102
+ end
103
+ end
@@ -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 HttpApiTools
4
+ class ExpandedRelationIncludes
5
+
6
+ def initialize(relation_includes, serializer_class)
7
+ @relation_includes = relation_includes
8
+ @serializer_class = serializer_class
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_class, :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 HttpApiTools
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 HttpApiTools
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
+ HttpApiTools::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 == HttpApiTools::Sideloading::JsonSerializer }
53
+ :sideloading
54
+ elsif self.ancestors.any? { |klass| klass == HttpApiTools::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 thttp_api_tools 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 HttpApiTools
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 HttpApiTools
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