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
@@ -0,0 +1,196 @@
1
+ require 'spec_helper'
2
+ require 'http_api_tools/relation_includes'
3
+
4
+ module HttpApiTools
5
+ describe RelationIncludes do
6
+
7
+ context 'when constructed with no value' do
8
+ let(:includes) { RelationIncludes.new }
9
+
10
+ it 'is empty, not present and blank' do
11
+ expect(includes).to be_empty
12
+ expect(includes).to be_blank
13
+ expect(includes).to_not be_present
14
+ end
15
+ end
16
+
17
+ describe 'equality' do
18
+
19
+ it 'works as expected' do
20
+ one = [ :tags, { images: [:comments] }, { reviews: [:author] } ]
21
+ two = [ :tags, { images: [:comments] }, { reviews: [:author] } ]
22
+ expect(RelationIncludes.new(*one)).to eq RelationIncludes.new(*two)
23
+
24
+ one = [ { reviews: [:author] }, :tags, { images: [:comments] } ]
25
+ two = [ :tags, { images: [:comments] }, { reviews: [:author] } ]
26
+ expect(RelationIncludes.new(*one)).to eq RelationIncludes.new(*two)
27
+
28
+ one = [ :tags, { images: [:comments] }, { reviews: [:author] } ]
29
+ two = [ :tags, { images: [:comments] }, :reviews ]
30
+ expect(RelationIncludes.new(*one)).to_not eq RelationIncludes.new(*two)
31
+ end
32
+ end
33
+
34
+ describe '.from_string' do
35
+
36
+ let(:string) { 'a,a.b,a.b.c,b,c' }
37
+ let(:includes) { RelationIncludes.from_string(string) }
38
+
39
+ it 'creates single level includes' do
40
+ expect(includes).to include :b
41
+ expect(includes).to include :c
42
+ end
43
+
44
+ it 'creates nested includes' do
45
+ expect(includes).to include({ a: [{ b: [:c] }] })
46
+ end
47
+
48
+ it 'creates same structure when implicit parts of the path are removed' do
49
+ simplified_params = 'a.b.c,b,c'
50
+ simplified_includes = RelationIncludes.from_string(simplified_params)
51
+ expect(includes).to eq simplified_includes
52
+ end
53
+
54
+ context 'when a nil or empty string is provided' do
55
+
56
+ it 'returns a new includes' do
57
+ expect(RelationIncludes.from_string(nil)).to eq RelationIncludes.new
58
+ expect(RelationIncludes.from_string('')).to eq RelationIncludes.new
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#to_s" do
64
+
65
+ it "converts to dot-notation specified by the JSON API spec, sorted alphabetically" do
66
+ includes = RelationIncludes.new(:reviews, { images: [{ comments: [:author] }] }, :hashtags)
67
+ expect(includes.to_s).to eq 'hashtags,images,images.comments,images.comments.author,reviews'
68
+
69
+ includes = RelationIncludes.new(:hashtags, { images: [{ comments: [:author, :rating] }] }, :reviews)
70
+ expect(includes.to_s).to eq 'hashtags,images,images.comments,images.comments.author,images.comments.rating,reviews'
71
+ end
72
+ end
73
+
74
+ describe '#&' do
75
+
76
+ let(:relations) { [ :tags, { images: [:comments] }, { reviews: [:author] } ] }
77
+ let(:includes) { RelationIncludes.new(*relations) }
78
+
79
+ let(:scenarios) do
80
+ [
81
+ {
82
+ includes: [ { images: [:comments] } ],
83
+ other: [ { images: [:comments] } ],
84
+ expected: [ { images: [:comments] } ]
85
+ },
86
+ {
87
+ includes: [ { images: [:comments, :hashtags]} ],
88
+ other: [ { images: [:comments] } ],
89
+ expected: [ { images: [:comments] } ]
90
+ },
91
+ {
92
+ includes: [ { images: [:comments] } ],
93
+ other: [ { images: [:comments, :hashtags] } ],
94
+ expected: [ { images: [:comments] } ]
95
+ },
96
+ {
97
+ includes: [ :reviews, { images: [{ comments: [:author] }] }, :hashtags ],
98
+ other: [ :reviews, { images: [{ comments: [:author] }] }, :hashtags ],
99
+ expected: [ :reviews, { images: [{ comments: [:author] }] }, :hashtags ]
100
+ },
101
+ {
102
+ includes: [ :reviews, { images: [{ comments: [:author] }] } ],
103
+ other: [ :reviews, { images: [ :comments ]} ],
104
+ expected: [ :reviews, { images: [ :comments ]} ]
105
+ },
106
+ {
107
+ includes: [ :reviews, { images: [ :comments ]} ],
108
+ other: [ :reviews, { images: [{ comments: [:author] }] } ],
109
+ expected: [ :reviews, { images: [ :comments ]} ]
110
+ },
111
+ {
112
+ includes: [ :reviews, { images: [{ comments: [:author] }] } ],
113
+ other: [ :reviews, :images ],
114
+ expected: [ :reviews, :images ]
115
+ },
116
+ {
117
+ includes: [ :reviews, {images: [{ comments: [:author] }] } ],
118
+ other: [ :reviews, :images, :hashtags ],
119
+ expected: [ :reviews, :images ]
120
+ }
121
+ ]
122
+ end
123
+
124
+ it 'reuturns a new RelationIncludes as a deep intersection between two RelationIncludes' do
125
+ scenarios.each do |scenario|
126
+ includes = scenario[:includes]
127
+ other = scenario[:other]
128
+ expected = scenario[:expected]
129
+
130
+ intersection = RelationIncludes.new(*includes) & RelationIncludes.new(*other)
131
+ expect(intersection).to eq RelationIncludes.new(*expected)
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ describe "#includes_relation?" do
138
+
139
+ let(:includes) { RelationIncludes.new(:a, { b: [:c] }) }
140
+
141
+ it "includes correct relations when a symbol" do
142
+ expect(includes.includes_relation?(:a)).to be_true
143
+ end
144
+
145
+ it "includes relations when key of object" do
146
+ expect(includes.includes_relation?(:b)).to be_true
147
+ end
148
+
149
+ it "does not include unspecified relations" do
150
+ expect(includes.includes_relation?(:x)).to be_false
151
+ end
152
+
153
+ end
154
+
155
+ describe "#include" do
156
+
157
+ let(:includes) { RelationIncludes.new(:a, { b: [:c] }) }
158
+
159
+ it "includes new relations" do
160
+ includes.include([:y, :z])
161
+ expect(includes).to include :y
162
+ expect(includes).to include :z
163
+ end
164
+ end
165
+
166
+ describe "#find" do
167
+
168
+ let(:includes) { RelationIncludes.new(:a, { b: [:c] }) }
169
+
170
+ it "finds include by key" do
171
+ expect(includes.find(:b)).to eq({ b: [:c] })
172
+ end
173
+ end
174
+
175
+ describe "#nested_includes_for" do
176
+
177
+ let(:includes) { RelationIncludes.new(:a, { b: [:c] }) }
178
+
179
+ it "returns nested includes" do
180
+ expect(includes.nested_includes_for(:b)).to eq([:c])
181
+ end
182
+ end
183
+
184
+ describe "#for_query" do
185
+
186
+ let(:includes) { RelationIncludes.new(:employer, { skills: [:person] }).for_query(HttpApiTools::Sideloading::PersonSerializer) }
187
+
188
+ it "creates includes for included relationships and has_many relationships for fetching ids" do
189
+ expect(includes.find(:employer)).to eq({ employer: [:employees] })
190
+ expect(includes.find(:skills)).to eq({ skills: [{ person: [:skills] }] })
191
+ end
192
+
193
+ end
194
+
195
+ end
196
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'http_api_tools/sideloading/json_deserializer'
5
+ require 'http_api_tools/support/company_deserializer_mapping'
6
+ require 'http_api_tools/support/person_deserializer_mapping'
7
+
8
+ module HttpApiTools
9
+
10
+ module Sideloading
11
+
12
+ describe JsonDeserializer do
13
+
14
+ let(:json) do
15
+ {
16
+ 'meta' => {
17
+ 'type' => 'company',
18
+ 'root_key' => 'companies'
19
+ },
20
+ 'companies' => [{
21
+ 'id' => 1,
22
+ 'name' => "Hooroo",
23
+ 'brand' => "We are travellers or something",
24
+ 'links' => {
25
+ 'employees' => [10, 11, 12],
26
+ 'address' => 20,
27
+ 'suppliers' => [30],
28
+ 'parent_company' => 40
29
+ }
30
+ }],
31
+ 'linked' => {
32
+ 'people' => [
33
+ {'id' => 10, 'first_name' => 'Rob', 'links' => { 'employer' => 1 } },
34
+ {'id' => 11, 'first_name' => 'Stu', 'links' => { 'employer' => 1 } },
35
+ {'id' => 12, 'first_name' => 'Dan', 'links' => { 'employer' => 1 } },
36
+ ],
37
+ 'addresses' => [
38
+ {'id' => 20, 'street_address' => "1 Burke Street"}
39
+ ]
40
+ }
41
+
42
+ }
43
+
44
+ end
45
+
46
+ let(:company) do
47
+ JsonDeserializer.new(json).deserialize.first
48
+ end
49
+
50
+ describe "basic deserialization" do
51
+
52
+ it "creates model from the root object" do
53
+ expect(company.id).to eq json['companies'][0]['id']
54
+ expect(company.name).to eq json['companies'][0]['name']
55
+ end
56
+
57
+ it "can set read-only attributes" do
58
+ expect(company.brand).to eq json['companies'][0]['brand']
59
+ end
60
+
61
+ it "includes sideloaded has many relationships" do
62
+ expect(company.employees.size).to eql 3
63
+ expect(company.employees.first.first_name).to eq json['linked']['people'].first['first_name']
64
+ end
65
+
66
+ it "includes sideloaded has_one relationships" do
67
+ expect(company.address.street_address).to eq json['linked']['addresses'].first['street_address']
68
+ end
69
+
70
+ it "includes circular relationships" do
71
+ expect(company.employees.first.employer.name).to eq json['companies'][0]['name']
72
+ end
73
+
74
+ it "has_many relationships without sideloaded data are set to an empty array" do
75
+ expect(company.suppliers).to eql []
76
+ end
77
+
78
+ it "has_many relationships without sideloaded data have relation_ids attribute set to the ids of the relationship" do
79
+ expect(company.supplier_ids).to eql [30]
80
+ end
81
+
82
+ it "has_one relationships without sideloaded data are set to nil" do
83
+ expect(company.parent_company).to eql nil
84
+ end
85
+
86
+ it "has_one relationships without sideloaded data sets the relation_id attribute set to the id" do
87
+ expect(company.parent_company_id).to eql 40
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+ require 'ruby-prof'
3
+
4
+ module HttpApiTools
5
+ module Sideloading
6
+
7
+ # Rudimentary performance profiler - uncomment to run on demand for now
8
+
9
+
10
+ # describe JsonSerializer do
11
+
12
+ # let!(:companies) do
13
+ # start = Time.now
14
+ # companies = (1..100).map do |company_id|
15
+ # company = Company.new(id: company_id, name: "Company #{rand(company_id)}")
16
+ # people = (1..100).map do |person_id|
17
+ # person = Person.new(id: person_id, first_name: "First #{rand(person_id)}", last_name: "Last #{rand(person_id)}", dob: Date.today, email: 'user@example.com', employer: company)
18
+ # skills = (1..20).map do |skill_id|
19
+ # skill = Skill.new(id: skill_id, name: "Skill #{rand(skill_id)}", description: "abc123" * 500, person: person)
20
+ # end
21
+ # person.skills = skills
22
+ # person
23
+ # end
24
+ # company.employees = people
25
+ # company
26
+ # end
27
+ # puts "BUILD MODEL: #{(Time.now - start) * 1000} ms"
28
+ # companies
29
+ # end
30
+
31
+ # it "serializes" do
32
+ # RubyProf.start
33
+ # serialized = CompanySerializer.new(companies).includes(:employer, {employees: [:skills]}).as_json
34
+ # result = RubyProf.stop
35
+ # printer = RubyProf::CallStackPrinter.new(result)
36
+ # report_file = File.open(File.expand_path("../../../reports/profile_report.html", __FILE__), "w")
37
+ # printer.print(report_file)
38
+
39
+ # start = Time.now
40
+ # serialized = CompanySerializer.new(companies).includes(:employer, {employees: [:skills]}).as_json
41
+ # puts "SERIALIZE: #{(Time.now - start) * 1000} ms"
42
+
43
+ # start = Time.now
44
+ # JSON.fast_generate(serialized)
45
+ # puts "TO_JSON: #{(Time.now - start) * 1000} ms"
46
+
47
+ # end
48
+
49
+ # end
50
+ end
51
+ end
@@ -0,0 +1,174 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'http_api_tools/sideloading/json_serializer'
5
+
6
+ module HttpApiTools
7
+ module Sideloading
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
+ expect(serialized_person[:links][:employer]).to eql person.employer.id
48
+ expect(serialized_person[:links][:skills]).to eql person.skills.map(&:id)
49
+ end
50
+
51
+ it "doesn't serialize any relationships" do
52
+ expect(serialized[:linked][:companies]).to be_nil
53
+ expect(serialized[:linked][:skills]).to be_nil
54
+ end
55
+
56
+ end
57
+
58
+ context "with relations specified as includes" do
59
+
60
+ let(:serializer) { PersonSerializer.new(person).includes(:employer, {skills: [:person]}) }
61
+
62
+ let(:serialized) do
63
+ serializer.as_json.with_indifferent_access
64
+ end
65
+
66
+ let(:serialized_person) { serialized[:people].first }
67
+
68
+ it "serializes relationships as ids" do
69
+ expect(serialized_person[:links][:employer]).to eql person.employer.id
70
+ expect(serialized_person[:links][:skills]).to eql person.skills.map(&:id)
71
+ end
72
+
73
+ it "sideloads has_one relationships" do
74
+ expect(serialized[:linked][:companies].first[:name]).to eql person.employer.name
75
+ end
76
+
77
+ it "sideloads has_many relationships" do
78
+ expect(serialized[:linked][:skills].first[:name]).to eql person.skills.first.name
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ context "with an array as the serializable object" do
86
+
87
+ let(:relation) do
88
+ [person, second_person]
89
+ end
90
+
91
+ let(:second_person) { Person.new(id: 5, first_name: 'Stu', last_name: 'Liston') }
92
+ let(:serialized) { JSON.parse(PersonSerializer.new(relation).to_json).with_indifferent_access }
93
+
94
+ before do
95
+ company.employees = [person, second_person]
96
+ person.employer = company
97
+ second_person.employer = company
98
+ person.skills = [skill]
99
+ second_person.skills = []
100
+ skill.person = person
101
+ end
102
+
103
+ it "serializes basic attributes of all items in the array" do
104
+ expect(serialized[:people][0][:id]).to eql person.id
105
+ expect(serialized[:people][0][:first_name]).to eql person.first_name
106
+ expect(serialized[:people][0][:last_name]).to eql person.last_name
107
+ expect(serialized[:people][0][:id]).to eql person.id
108
+
109
+ expect(serialized[:people][1][:id]).to eql second_person.id
110
+ expect(serialized[:people][1][:first_name]).to eql second_person.first_name
111
+ expect(serialized[:people][1][:last_name]).to eql second_person.last_name
112
+ expect(serialized[:people][1][:id]).to eql second_person.id
113
+ end
114
+
115
+ end
116
+
117
+ describe "meta data" do
118
+
119
+ let(:serializer) { PersonSerializer.new(person) }
120
+
121
+ it "adds root key" do
122
+ expect(serializer.as_json[:meta][:root_key]).to eql 'people'
123
+ end
124
+
125
+ it "adds type" do
126
+ expect(serializer.as_json[:meta][:type]).to eql 'person'
127
+ end
128
+
129
+ it "allows meta data to be added" do
130
+ serializer.meta(offset: 0, limit: 10)
131
+ expect(serializer.as_json[:meta][:offset]).to eql 0
132
+ expect(serializer.as_json[:meta][:limit]).to eql 10
133
+ end
134
+
135
+ end
136
+
137
+ describe "limiting sideloadable data" do
138
+
139
+ class LimitedSideloadingPersonSerializer < PersonSerializer
140
+ includable :employer, :skills
141
+ end
142
+
143
+ let(:unlimited_serialized) { PersonSerializer.new(person).includes(:employer, {skills: [:person]}).as_json.with_indifferent_access }
144
+
145
+ let(:limited_serialized) do
146
+ LimitedSideloadingPersonSerializer.new(person).includes(:employer, {skills: [:person]}).as_json.with_indifferent_access
147
+ end
148
+
149
+ it "does not limit sideloading if not limited in serializer" do
150
+ expect(unlimited_serialized[:linked][:people].first[:id]).to eq person.id
151
+ end
152
+
153
+ it "allows sideloading of includable relations" do
154
+ expect(limited_serialized[:linked][:skills].first[:name]).to eql person.skills.first.name
155
+ end
156
+
157
+ it "prevents sideloading of non-includable relations" do
158
+ expect(limited_serialized[:linked][:people]).to be_nil
159
+ end
160
+
161
+ it "includes whttp_api_tools is includables in meta" do
162
+ expect(limited_serialized[:meta][:includable]).to eq 'employer,skills'
163
+ end
164
+
165
+ it "includes whttp_api_tools was included in meta" do
166
+ expect(limited_serialized[:meta][:included]).to eq 'employer,skills'
167
+ expect(unlimited_serialized[:meta][:included]).to eq 'employer,skills,skills.person'
168
+ end
169
+
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end