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.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +479 -0
- data/Rakefile +4 -0
- data/http_api_tools.gemspec +29 -0
- data/lib/http_api_tools/base_json_serializer.rb +103 -0
- data/lib/http_api_tools/expanded_relation_includes.rb +77 -0
- data/lib/http_api_tools/identity_map.rb +42 -0
- data/lib/http_api_tools/json_serializer_dsl.rb +62 -0
- data/lib/http_api_tools/model/acts_like_active_model.rb +16 -0
- data/lib/http_api_tools/model/attributes.rb +159 -0
- data/lib/http_api_tools/model/has_many_array.rb +47 -0
- data/lib/http_api_tools/model/transformers/date_time_transformer.rb +31 -0
- data/lib/http_api_tools/model/transformers/registry.rb +55 -0
- data/lib/http_api_tools/model.rb +2 -0
- data/lib/http_api_tools/nesting/json_serializer.rb +45 -0
- data/lib/http_api_tools/nesting/relation_loader.rb +89 -0
- data/lib/http_api_tools/relation_includes.rb +146 -0
- data/lib/http_api_tools/serializer_registry.rb +27 -0
- data/lib/http_api_tools/sideloading/json_deserializer.rb +121 -0
- data/lib/http_api_tools/sideloading/json_deserializer_mapping.rb +27 -0
- data/lib/http_api_tools/sideloading/json_serializer.rb +125 -0
- data/lib/http_api_tools/sideloading/relation_sideloader.rb +79 -0
- data/lib/http_api_tools/sideloading/sideload_map.rb +54 -0
- data/lib/http_api_tools/type_key_resolver.rb +27 -0
- data/lib/http_api_tools/version.rb +3 -0
- data/lib/http_api_tools.rb +10 -0
- data/reports/empty.png +0 -0
- data/reports/minus.png +0 -0
- data/reports/plus.png +0 -0
- data/spec/http_api_tools/expanded_relation_includes_spec.rb +31 -0
- data/spec/http_api_tools/identity_map_spec.rb +31 -0
- data/spec/http_api_tools/model/attributes_spec.rb +170 -0
- data/spec/http_api_tools/model/has_many_array_spec.rb +48 -0
- data/spec/http_api_tools/model/transformers/date_time_transformer_spec.rb +36 -0
- data/spec/http_api_tools/model/transformers/registry_spec.rb +53 -0
- data/spec/http_api_tools/nesting/json_serializer_spec.rb +173 -0
- data/spec/http_api_tools/relation_includes_spec.rb +196 -0
- data/spec/http_api_tools/sideloading/json_deserializer_spec.rb +93 -0
- data/spec/http_api_tools/sideloading/json_serializer_performance_spec.rb +51 -0
- data/spec/http_api_tools/sideloading/json_serializer_spec.rb +174 -0
- data/spec/http_api_tools/sideloading/sideload_map_spec.rb +59 -0
- data/spec/http_api_tools/support/company_deserializer_mapping.rb +11 -0
- data/spec/http_api_tools/support/person_deserializer_mapping.rb +9 -0
- data/spec/http_api_tools/support/spec_models.rb +89 -0
- data/spec/http_api_tools/support/spec_nesting_serializers.rb +41 -0
- data/spec/http_api_tools/support/spec_sideloading_serializers.rb +41 -0
- data/spec/http_api_tools/type_key_resolver_spec.rb +19 -0
- data/spec/spec_helper.rb +8 -0
- metadata +214 -0
data/Rakefile
ADDED
@@ -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
|