http-api-tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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'