http-api-tools 0.1.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 +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,93 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'hat/sideloading/json_deserializer'
5
+ require 'hat/support/company_deserializer_mapping'
6
+ require 'hat/support/person_deserializer_mapping'
7
+
8
+ module Hat
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 Hat
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,185 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'hat/sideloading/json_serializer'
5
+
6
+ module Hat
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 that 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
+ describe "#includes_for_query" do
82
+
83
+ let(:includes_for_query) { serializer.includes_for_query }
84
+
85
+ it "expands includes for included relationships and has_many relationships for fetching ids" do
86
+ expect(includes_for_query.find(:employer)).to eq({ employer: [:employees] })
87
+ expect(includes_for_query.find(:skills)).to eq({ skills: [{ person: [:skills] }] })
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+
96
+ context "with an array as the serializable object" do
97
+
98
+ let(:relation) do
99
+ [person, second_person]
100
+ end
101
+
102
+ let(:second_person) { Person.new(id: 5, first_name: 'Stu', last_name: 'Liston') }
103
+ let(:serialized) { JSON.parse(PersonSerializer.new(relation).to_json).with_indifferent_access }
104
+
105
+ before do
106
+ company.employees = [person, second_person]
107
+ person.employer = company
108
+ second_person.employer = company
109
+ person.skills = [skill]
110
+ second_person.skills = []
111
+ skill.person = person
112
+ end
113
+
114
+ it "serializes basic attributes of all items in the array" do
115
+ expect(serialized[:people][0][:id]).to eql person.id
116
+ expect(serialized[:people][0][:first_name]).to eql person.first_name
117
+ expect(serialized[:people][0][:last_name]).to eql person.last_name
118
+ expect(serialized[:people][0][:id]).to eql person.id
119
+
120
+ expect(serialized[:people][1][:id]).to eql second_person.id
121
+ expect(serialized[:people][1][:first_name]).to eql second_person.first_name
122
+ expect(serialized[:people][1][:last_name]).to eql second_person.last_name
123
+ expect(serialized[:people][1][:id]).to eql second_person.id
124
+ end
125
+
126
+ end
127
+
128
+ describe "meta data" do
129
+
130
+ let(:serializer) { PersonSerializer.new(person) }
131
+
132
+ it "adds root key" do
133
+ expect(serializer.as_json[:meta][:root_key]).to eql 'people'
134
+ end
135
+
136
+ it "adds type" do
137
+ expect(serializer.as_json[:meta][:type]).to eql 'person'
138
+ end
139
+
140
+ it "allows meta data to be added" do
141
+ serializer.meta(offset: 0, limit: 10)
142
+ expect(serializer.as_json[:meta][:offset]).to eql 0
143
+ expect(serializer.as_json[:meta][:limit]).to eql 10
144
+ end
145
+
146
+ end
147
+
148
+ describe "limiting sideloadable data" do
149
+
150
+ class LimitedSideloadingPersonSerializer < PersonSerializer
151
+ includable :employer, :skills
152
+ end
153
+
154
+ let(:unlimited_serialized) { PersonSerializer.new(person).includes(:employer, {skills: [:person]}).as_json.with_indifferent_access }
155
+
156
+ let(:limited_serialized) do
157
+ LimitedSideloadingPersonSerializer.new(person).includes(:employer, {skills: [:person]}).as_json.with_indifferent_access
158
+ end
159
+
160
+ it "does not limit sideloading if not limited in serializer" do
161
+ expect(unlimited_serialized[:linked][:people].first[:id]).to eq person.id
162
+ end
163
+
164
+ it "allows sideloading of includable relations" do
165
+ expect(limited_serialized[:linked][:skills].first[:name]).to eql person.skills.first.name
166
+ end
167
+
168
+ it "prevents sideloading of non-includable relations" do
169
+ expect(limited_serialized[:linked][:people]).to be_nil
170
+ end
171
+
172
+ it "includes what is includables in meta" do
173
+ expect(limited_serialized[:meta][:includable]).to eq 'employer,skills'
174
+ end
175
+
176
+ it "includes what was included in meta" do
177
+ expect(limited_serialized[:meta][:included]).to eq 'employer,skills'
178
+ expect(unlimited_serialized[:meta][:included]).to eq 'employer,skills,skills.person'
179
+ end
180
+
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+ require 'hat/sideloading/sideload_map'
3
+
4
+ module Hat
5
+ module Sideloading
6
+
7
+ describe SideloadMap do
8
+
9
+ let(:json) do
10
+ {
11
+ 'meta' => {
12
+ 'root_key' => 'posts'
13
+ },
14
+ 'posts' => [{
15
+ 'id' => 1,
16
+ 'title' => 'Post Title'
17
+ }],
18
+ 'linked' => {
19
+ 'images' => [
20
+ {'id' => 10, 'url' => '1.png' },
21
+ {'id' => 11, 'url' => '2.png' }
22
+ ],
23
+ 'comments' => [
24
+ {'id' => 20, 'text' => 'Comment 1'}
25
+ ]
26
+ }
27
+ }
28
+
29
+ end
30
+
31
+ let(:sideload_map) { SideloadMap.new(json, 'posts') }
32
+
33
+ describe 'getting sideloaded json from the map' do
34
+
35
+ it 'returns object at root key' do
36
+ expect(sideload_map.get('post', 1)['id']).to eql 1
37
+ end
38
+
39
+ it 'retrieves by singular type' do
40
+ expect(sideload_map.get('image', 10)['id']).to eql 10
41
+ end
42
+
43
+ it 'retrieves by plural type' do
44
+ expect(sideload_map.get('images', 11)['id']).to eql 11
45
+ end
46
+
47
+ it 'retrieves with string type' do
48
+ expect(sideload_map.get('comment', 20)['id']).to eql 20
49
+ end
50
+
51
+ it 'returns nil if object not present' do
52
+ expect(sideload_map.get('foo', 100)).to be_nil
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require 'hat/sideloading/json_deserializer_mapping'
4
+
5
+ class CompanyDeserializerMapping
6
+
7
+ include Hat::Sideloading::JsonDeserializerMapping
8
+
9
+ map :employees, Person
10
+
11
+ end
@@ -0,0 +1,9 @@
1
+ require 'hat/sideloading/json_deserializer_mapping'
2
+
3
+ class PersonDeserializerMapping
4
+
5
+ include Hat::Sideloading::JsonDeserializerMapping
6
+
7
+ map :employer, Company
8
+
9
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ require 'hat/model'
4
+ require 'ostruct'
5
+
6
+
7
+ class Person
8
+
9
+ include Hat::Model::Attributes
10
+
11
+ attribute :id
12
+ attribute :first_name
13
+ attribute :last_name
14
+ attribute :dob
15
+ attribute :email
16
+ belongs_to :employer
17
+ has_many :skills
18
+
19
+ def employer_id
20
+ employer.try(:id)
21
+ end
22
+
23
+ #Act like active record for reflectively interogating type info
24
+ def self.reflections
25
+ {
26
+ employer: OpenStruct.new(class_name: 'Company'),
27
+ skills: OpenStruct.new(class_name: 'Skill')
28
+ }
29
+ end
30
+
31
+ end
32
+
33
+ class Company
34
+
35
+ include Hat::Model::Attributes
36
+
37
+ attribute :id
38
+ attribute :name
39
+ attribute :brand, read_only: true
40
+ has_many :employees
41
+ has_many :suppliers
42
+ belongs_to :parent_company
43
+ belongs_to :address
44
+
45
+
46
+ #Act like active record for reflectively interogating type info
47
+ def self.reflections
48
+ {
49
+ employees: OpenStruct.new(class_name: 'Person'),
50
+ suppliers: OpenStruct.new(class_name: 'Company'),
51
+ parent_company: OpenStruct.new(class_name: 'Company'),
52
+ address: OpenStruct.new(class_name: 'Address')
53
+ }
54
+ end
55
+
56
+ end
57
+
58
+ class Skill
59
+
60
+ include Hat::Model::Attributes
61
+
62
+ attribute :id
63
+ attribute :name
64
+ attribute :description
65
+ belongs_to :person
66
+
67
+ def person_id
68
+ person.try(:id)
69
+ end
70
+
71
+ #Act like active record for reflectively interogating type info
72
+ def self.reflections
73
+ {
74
+ person: OpenStruct.new(class_name: 'Person')
75
+ }
76
+ end
77
+
78
+ end
79
+
80
+ class Address
81
+
82
+ attr_accessor :id, :street_address
83
+
84
+ def initialize(attrs)
85
+ @id = attrs[:id]
86
+ @street_address = attrs[:street_address]
87
+ end
88
+
89
+ end
@@ -0,0 +1,41 @@
1
+ require 'hat/nesting/json_serializer'
2
+
3
+ module Hat
4
+ module Nesting
5
+ class PersonSerializer
6
+
7
+ include Hat::Nesting::JsonSerializer
8
+
9
+ serializes Person
10
+ attributes :id, :first_name, :last_name, :full_name, :dob, :email
11
+ has_one :employer
12
+ has_many :skills
13
+
14
+ def full_name
15
+ "#{serializable.first_name} #{serializable.last_name}"
16
+ end
17
+
18
+ end
19
+
20
+ class CompanySerializer
21
+
22
+ include Hat::Nesting::JsonSerializer
23
+
24
+ serializes Company
25
+ attributes :id, :name
26
+ has_many :employees
27
+
28
+ end
29
+
30
+
31
+ class SkillSerializer
32
+
33
+ include Hat::Nesting::JsonSerializer
34
+
35
+ serializes Skill
36
+ attributes :id, :name, :description
37
+ has_one :person
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'hat/sideloading/json_serializer'
2
+
3
+ module Hat
4
+ module Sideloading
5
+ class PersonSerializer
6
+
7
+ include Hat::Sideloading::JsonSerializer
8
+
9
+ serializes Person
10
+ attributes :id, :first_name, :last_name, :full_name, :dob, :email
11
+ has_one :employer
12
+ has_many :skills
13
+
14
+ def full_name
15
+ "#{serializable.first_name} #{serializable.last_name}"
16
+ end
17
+
18
+ end
19
+
20
+ class CompanySerializer
21
+
22
+ include Hat::Sideloading::JsonSerializer
23
+
24
+ serializes Company
25
+ attributes :id, :name
26
+ has_many :employees
27
+
28
+ end
29
+
30
+
31
+ class SkillSerializer
32
+
33
+ include Hat::Sideloading::JsonSerializer
34
+
35
+ serializes Skill
36
+ attributes :id, :name, :description
37
+ has_one :person
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'hat/type_key_resolver'
3
+
4
+ module Hat
5
+
6
+ describe TypeKeyResolver do
7
+
8
+ let(:resolver) { TypeKeyResolver.new }
9
+
10
+ describe 'resolving class names' do
11
+
12
+ it 'correctly resolves the type key multiple times' do
13
+ expect(resolver.for_class(String)).to eq 'strings'
14
+ expect(resolver.for_class(String)).to eq 'strings'
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ require 'hat/support/spec_models'
4
+ require 'hat/support/spec_sideloading_serializers'
5
+ require 'hat/support/spec_nesting_serializers'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'rubygems'
8
+ require 'pry'