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/reports/empty.png ADDED
Binary file
data/reports/minus.png ADDED
Binary file
data/reports/plus.png ADDED
Binary file
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'http_api_tools/expanded_relation_includes'
3
+
4
+ module HttpApiTools
5
+ describe ExpandedRelationIncludes do
6
+
7
+ describe "#to_a" do
8
+
9
+ let(:expanded_includes) { ExpandedRelationIncludes.new(includes, HttpApiTools::Sideloading::PersonSerializer) }
10
+
11
+ context 'with single-level includes' do
12
+
13
+ let(:includes) { [:employer, :skills] }
14
+
15
+ it "expands includes to include has_many relationships defined by serializers but not in original includes" do
16
+ expect(expanded_includes.to_a).to eq([{ employer: [:employees] }, :skills])
17
+ end
18
+ end
19
+
20
+ context 'with multi-level includes' do
21
+
22
+ let(:includes) { [:employer, { skills: [:person] }] }
23
+
24
+ it "expands includes to include has_many relationships defined by serializers but not in original includes" do
25
+ expect(expanded_includes.to_a).to eq([{ employer: [:employees] }, { skills: [{ person: [:skills] }] }])
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'http_api_tools/identity_map'
3
+
4
+ module HttpApiTools
5
+ describe IdentityMap do
6
+
7
+ let(:identity_map) { IdentityMap.new }
8
+ let(:thing) { 'a thing' }
9
+
10
+ describe "putting/getting items" do
11
+ it "puts and revieves same object with string type key" do
12
+
13
+ identity_map.put('thing', 1, thing)
14
+ expect(identity_map.get('thing', 1)).to eql thing
15
+ end
16
+
17
+ it "puts and revieves same object with symbol type key" do
18
+ identity_map.put(:thing, 1, thing)
19
+ expect(identity_map.get(:thing, 1)).to eql thing
20
+ end
21
+
22
+ it "puts and revieves same object with mixed type key" do
23
+ identity_map.put(:thing, 1, thing)
24
+ expect(identity_map.get('thing', 1)).to eql thing
25
+ end
26
+
27
+ end
28
+ end
29
+ class IdentityMapThing ; end
30
+ end
31
+
@@ -0,0 +1,170 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'http_api_tools/model/attributes'
5
+
6
+ module HttpApiTools
7
+ module Model
8
+
9
+ describe Attributes do
10
+
11
+ let(:test_model_class) do
12
+ Class.new do
13
+ include Attributes
14
+ attribute :name
15
+ attribute :dob, type: :date_time
16
+ attribute :tags, default: []
17
+ attribute :qualifications, default: { thing: 1 }
18
+ attribute :source, default: 'internal'
19
+ attribute :active
20
+ belongs_to :parent
21
+ has_many :children
22
+ end
23
+ end
24
+
25
+ describe "attribute definition" do
26
+
27
+ it "initializes attributes from constructor with coercions" do
28
+ test_model = test_model_class.new(name: "Moonunit", tags: ["Musician", "Guitarist"])
29
+ expect(test_model.name).to eq "Moonunit"
30
+ expect(test_model.tags).to eq ["Musician", "Guitarist"]
31
+ end
32
+
33
+ it "transforms date value when date_time type is defined" do
34
+ date_time_string = "2013-01-01T12:00:00.000Z"
35
+ test_model = test_model_class.new(dob: date_time_string)
36
+ expect(test_model.dob).to eq DateTime.parse(date_time_string)
37
+ end
38
+
39
+ it "sets default array if provided" do
40
+ expect(test_model_class.new.tags).to eq []
41
+ end
42
+
43
+ it "sets default hash if provided" do
44
+ expect(test_model_class.new.qualifications).to eq(thing: 1)
45
+ end
46
+
47
+ it "sets default primitive if provided" do
48
+ expect(test_model_class.new.source).to eq 'internal'
49
+ end
50
+
51
+ it "multiple objects with default array don't share the same default reference" do
52
+ a = test_model_class.new.tags
53
+ b = test_model_class.new.tags
54
+ expect(a).to_not be b
55
+ end
56
+
57
+ it "allows setting of values defined by attribute" do
58
+ test_model = test_model_class.new
59
+ test_model.name = 'New Name'
60
+ expect(test_model.name).to eql 'New Name'
61
+ end
62
+
63
+ it "prevents default values not catered to default_for" do
64
+ test_model_with_invalid_default = Class.new do
65
+ include Attributes
66
+ attribute :created_at, default: DateTime.now
67
+ end
68
+ expect{ test_model_with_invalid_default.new }.to raise_error
69
+
70
+ end
71
+
72
+ it 'sets a false value as false' do
73
+ test_model = test_model_class.new(active: false)
74
+ expect(test_model.active).to eq false
75
+ end
76
+
77
+ context "when given read_only as an option" do
78
+
79
+ let(:test_model_with_read_only_attribute_class) do
80
+ Class.new do
81
+ include Attributes
82
+ attribute :created_at, read_only: true
83
+ attribute :some_cool_value
84
+ end
85
+ end
86
+
87
+
88
+ let(:test_model) { test_model_with_read_only_attribute_class.new }
89
+
90
+ context "when setting the read-only variable" do
91
+ describe "initialize" do
92
+ it "sets the read only value" do
93
+ now = Time.now
94
+ test_object = test_model_with_read_only_attribute_class.new(created_at: now)
95
+ expect(test_object.created_at).to eq now
96
+ end
97
+
98
+ end
99
+
100
+ it "only allows reading of thttp_api_tools attribute" do
101
+ expect{ test_model.created_at = Time.now }.to raise_error(NoMethodError)
102
+ expect(test_model.created_at).to be_nil
103
+ end
104
+ end
105
+
106
+ context "when setting the non read-only variable" do
107
+ it "allows reading and writing" do
108
+ value = 'value'
109
+ test_model.some_cool_value = value
110
+ expect(test_model.some_cool_value).to eql value
111
+ end
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+ describe 'belongs_to' do
118
+
119
+ let(:test_model) { test_model_class.new }
120
+ let(:parent) { OpenStruct.new(id: 1) }
121
+
122
+ it 'creates accessor for attribute name' do
123
+ test_model.parent = parent
124
+ expect(test_model.parent).to eq parent
125
+ end
126
+
127
+ it 'creates accessor for attribute_id' do
128
+ test_model.parent_id = 1
129
+ expect(test_model.parent_id).to eq 1
130
+ end
131
+
132
+ it 'updates the id attribute when the belongs_to attribute is updated' do
133
+ test_model.parent = parent
134
+ expect(test_model.parent_id).to eq 1
135
+ end
136
+
137
+ end
138
+
139
+ describe 'has_many' do
140
+
141
+ let(:test_model) { test_model_class.new }
142
+ let(:child) { OpenStruct.new(id: 1) }
143
+
144
+ it 'creates accessor for attribute name' do
145
+ test_model.children = [child]
146
+ expect(test_model.children).to eq [child]
147
+ end
148
+
149
+ it 'creates accessor for attributes_id' do
150
+ test_model.child_ids = [1]
151
+ expect(test_model.child_ids).to eq [1]
152
+ end
153
+
154
+ it 'updates the ids attribute when the has_many attribute is updated' do
155
+ test_model.children = [OpenStruct.new(id: 3), OpenStruct.new(id: 4)]
156
+ expect(test_model.child_ids).to eq [3, 4]
157
+ end
158
+
159
+ end
160
+
161
+ describe "#errors" do
162
+ it "adds accessors for errors" do
163
+ test_model = test_model_class.new(errors: 'errors')
164
+ expect(test_model.errors).to eql "errors"
165
+ end
166
+ end
167
+
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'http_api_tools/model/has_many_array'
5
+
6
+ module HttpApiTools
7
+ module Model
8
+ describe HasManyArray do
9
+
10
+ let(:has_many_attr_name) { 'things' }
11
+ let(:owner) { double('owner', has_many_changed: nil) }
12
+ let(:array) { HasManyArray.new([1], owner, has_many_attr_name) }
13
+
14
+ describe "observing mutation" do
15
+
16
+ before do
17
+ owner.should_receive(:has_many_changed).with(array, has_many_attr_name)
18
+ end
19
+
20
+ it "notifies on <<" do
21
+ array << 2
22
+ end
23
+
24
+ it "notifies on push" do
25
+ array.push(2)
26
+ end
27
+
28
+ it "notifies on delete" do
29
+ array.delete(1)
30
+ end
31
+
32
+ it "notifies on delete_at" do
33
+ array.delete_at(0)
34
+ end
35
+
36
+ it "notifies on clear" do
37
+ array.clear
38
+ end
39
+
40
+ it "notifies on shift" do
41
+ array.shift
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'http_api_tools/model/transformers/date_time_transformer'
3
+
4
+
5
+ module HttpApiTools
6
+ module Model
7
+ module Transformers
8
+
9
+ describe DateTimeTransformer do
10
+
11
+ let(:transformer) { DateTimeTransformer }
12
+
13
+ describe '.from_raw_value' do
14
+
15
+ it "transforms a date_time string" do
16
+ date_time_string = "2013-01-01T12:00:00.000Z"
17
+ expect(transformer.from_raw(date_time_string)).to eq DateTime.parse(date_time_string)
18
+ end
19
+
20
+ it "passes through date_time instances" do
21
+ now = DateTime.now
22
+ expect(transformer.from_raw(now)).to eql now
23
+ end
24
+
25
+ it "passes through nil" do
26
+ expect(transformer.from_raw(nil)).to eql nil
27
+ end
28
+
29
+ it "raises transform error on untransformable types" do
30
+ lambda { transformer.from_raw(11.11) }.should raise_error TransformError
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require 'http_api_tools/model/transformers/registry'
3
+
4
+
5
+ module HttpApiTools
6
+ module Model
7
+ module Transformers
8
+
9
+ describe Registry do
10
+
11
+ let(:registry) { Registry.instance }
12
+
13
+ describe 'registering and retrieving a transformer' do
14
+ it 'adds to the registry' do
15
+ registry.register(:foo, Object)
16
+ expect(registry.get(:foo)).to eq Object
17
+ end
18
+
19
+ it 'raises an exception if a transformer is registered more than once' do
20
+ registry.register(:xyz, Object)
21
+ lambda { registry.register(:xyz, Object) }.should raise_error
22
+ end
23
+ end
24
+
25
+ context 'with a registered transformer' do
26
+
27
+ it 'transforms a raw value with the transformer' do
28
+ date_string = '2000-10-10'
29
+ date_result = Date.today
30
+ registry.get(:date_time).should_receive(:from_raw).with(date_string).and_return(date_result)
31
+ expect(registry.from_raw(:date_time, date_string)).to eq date_result
32
+
33
+ end
34
+
35
+ end
36
+
37
+ context 'without a registered transformer' do
38
+ it 'passes through a raw value' do
39
+ raw_bar = 'bar'
40
+ expect(registry.get(:bar)).to be_nil
41
+ expect(registry.from_raw(:bar, raw_bar)).to eq raw_bar
42
+ end
43
+ end
44
+
45
+ describe 'pre-registered transformers' do
46
+ it 'has date_time transformer registered' do
47
+ expect(registry.get(:date_time)).to eq DateTimeTransformer
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,173 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'http_api_tools/nesting/json_serializer'
5
+
6
+ module HttpApiTools
7
+ module Nesting
8
+
9
+ describe JsonSerializer do
10
+
11
+ let(:company) { Company.new(id: 1, name: 'Hooroo') }
12
+ let(:person) { Person.new(id: 2, first_name: 'Rob', last_name: 'Monie') }
13
+ let(:skill) { Skill.new(id: 3, name: "JSON Serialization") }
14
+ let(:skill2) { Skill.new(id: 4, name: "JSON Serialization 2") }
15
+
16
+ before do
17
+ company.employees = [person]
18
+ person.employer = company
19
+ person.skills = [skill, skill2]
20
+ skill.person = person
21
+ skill2.person = person
22
+ end
23
+
24
+ describe "serialization of data" do
25
+ context "with a single top-level serializable object thttp_api_tools has relationship names different to model class" do
26
+
27
+ context "without any includes" do
28
+
29
+ let(:serialized) { PersonSerializer.new(person).as_json.with_indifferent_access }
30
+ let(:serialized_person) { serialized[:people].first }
31
+
32
+ it "serializes basic attributes" do
33
+ expect(serialized_person[:id]).to eql person.id
34
+ expect(serialized_person[:first_name]).to eql person.first_name
35
+ expect(serialized_person[:last_name]).to eql person.last_name
36
+ end
37
+
38
+ it 'expect basic attributes with no value' do
39
+ expect(serialized_person.has_key?(:dob)).to be_true
40
+ end
41
+
42
+ it "serializes attributes defined as methods on the serializer" do
43
+ expect(serialized_person[:full_name]).to eql "#{person.first_name} #{person.last_name}"
44
+ end
45
+
46
+ it "serializes relationships as ids" do
47
+
48
+ expect(serialized_person[:employer_id]).to eql person.employer.id
49
+ expect(serialized_person[:skill_ids]).to eql person.skills.map(&:id)
50
+ end
51
+
52
+ it "doesn't serialize any relationships" do
53
+ expect(serialized[:companies]).to be_nil
54
+ expect(serialized[:skills]).to be_nil
55
+ end
56
+
57
+ end
58
+
59
+ context "with relations specified as includes" do
60
+
61
+ let(:serialized) do
62
+ PersonSerializer.new(person).includes(:employer, { skills: [:person] }).as_json.with_indifferent_access
63
+ end
64
+
65
+ let(:serialized_person) { serialized[:people].first }
66
+
67
+ it "serializes nested relationships" do
68
+ expect(serialized_person[:employer][:id]).to eql person.employer.id
69
+ expect(serialized_person[:skills].first[:id]).to eql person.skills.first.id
70
+ expect(serialized_person[:skills].first[:person][:id]).to eql person.skills.first.person.id
71
+ end
72
+
73
+ it "includes wildcard in includable when no explicit includables have been defined" do
74
+ expect(serialized[:meta][:includable]).to eq '*'
75
+ end
76
+
77
+ it "includes whttp_api_tools was included in meta" do
78
+ expect(serialized[:meta][:included]).to eq 'employer,skills,skills.person'
79
+ end
80
+
81
+ end
82
+ end
83
+
84
+ context "with an array as the serializable object" do
85
+
86
+ let(:relation) do
87
+ [person, second_person]
88
+ end
89
+
90
+ let(:second_person) { Person.new(id: 5, first_name: 'Stu', last_name: 'Liston') }
91
+ let(:serialized) { JSON.parse(PersonSerializer.new(relation).to_json).with_indifferent_access }
92
+
93
+ before do
94
+ company.employees = [person, second_person]
95
+ person.employer = company
96
+ second_person.employer = company
97
+ person.skills = [skill]
98
+ second_person.skills = []
99
+ skill.person = person
100
+ end
101
+
102
+ it "serializes basic attributes of all items in the array" do
103
+ expect(serialized[:people][0][:id]).to eql person.id
104
+ expect(serialized[:people][0][:first_name]).to eql person.first_name
105
+ expect(serialized[:people][0][:last_name]).to eql person.last_name
106
+ expect(serialized[:people][0][:id]).to eql person.id
107
+
108
+ expect(serialized[:people][1][:id]).to eql second_person.id
109
+ expect(serialized[:people][1][:first_name]).to eql second_person.first_name
110
+ expect(serialized[:people][1][:last_name]).to eql second_person.last_name
111
+ expect(serialized[:people][1][:id]).to eql second_person.id
112
+ end
113
+
114
+ end
115
+
116
+ describe "meta data" do
117
+
118
+ let(:serializer) { PersonSerializer.new(person) }
119
+
120
+ it "adds root key" do
121
+ expect(serializer.as_json[:meta][:root_key]).to eql 'people'
122
+ end
123
+
124
+ it "adds type" do
125
+ expect(serializer.as_json[:meta][:type]).to eql 'person'
126
+ end
127
+
128
+ it "allows meta data to be added" do
129
+ serializer.meta(offset: 0, limit: 10)
130
+ expect(serializer.as_json[:meta][:offset]).to eql 0
131
+ expect(serializer.as_json[:meta][:limit]).to eql 10
132
+ end
133
+
134
+ end
135
+
136
+ describe "limiting nested data" do
137
+
138
+ class LimitedNestingPersonSerializer < PersonSerializer
139
+ includable :skills
140
+ end
141
+
142
+ let(:unlimited_serialized) { PersonSerializer.new(person).includes(:employer, { skills: [:person] }).as_json.with_indifferent_access }
143
+
144
+ let(:limited_serialized) do
145
+ LimitedNestingPersonSerializer.new(person).includes(:employer, { skills: [:person] }).as_json.with_indifferent_access
146
+ end
147
+
148
+ it "does not limit nesting if not limited in serializer" do
149
+ expect(unlimited_serialized[:people][0][:id]).to eq person.id
150
+ end
151
+
152
+ it "allows nesting of includable relations" do
153
+ expect(limited_serialized[:people][0][:skills].first[:name]).to eql person.skills.first.name
154
+ end
155
+
156
+ it "prevents nesting of non-includable relations" do
157
+ expect(limited_serialized[:skills]).to be_nil
158
+ end
159
+
160
+ it "includes whttp_api_tools is includable in meta" do
161
+ expect(limited_serialized[:meta][:includable]).to eq 'skills'
162
+ end
163
+
164
+ it "includes whttp_api_tools was included in meta" do
165
+ expect(limited_serialized[:meta][:included]).to eq 'skills'
166
+ expect(unlimited_serialized[:meta][:included]).to eq 'employer,skills,skills.person'
167
+ end
168
+
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end