castle-her 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +110 -0
  12. data/castle-her.gemspec +30 -0
  13. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  17. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  18. data/lib/castle-her.rb +20 -0
  19. data/lib/castle-her/api.rb +113 -0
  20. data/lib/castle-her/collection.rb +12 -0
  21. data/lib/castle-her/errors.rb +27 -0
  22. data/lib/castle-her/json_api/model.rb +46 -0
  23. data/lib/castle-her/middleware.rb +12 -0
  24. data/lib/castle-her/middleware/accept_json.rb +17 -0
  25. data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/castle-her/middleware/json_api_parser.rb +36 -0
  27. data/lib/castle-her/middleware/parse_json.rb +21 -0
  28. data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/castle-her/model.rb +75 -0
  30. data/lib/castle-her/model/associations.rb +141 -0
  31. data/lib/castle-her/model/associations/association.rb +103 -0
  32. data/lib/castle-her/model/associations/association_proxy.rb +45 -0
  33. data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
  34. data/lib/castle-her/model/associations/has_many_association.rb +100 -0
  35. data/lib/castle-her/model/associations/has_one_association.rb +79 -0
  36. data/lib/castle-her/model/attributes.rb +284 -0
  37. data/lib/castle-her/model/base.rb +33 -0
  38. data/lib/castle-her/model/deprecated_methods.rb +61 -0
  39. data/lib/castle-her/model/http.rb +114 -0
  40. data/lib/castle-her/model/introspection.rb +65 -0
  41. data/lib/castle-her/model/nested_attributes.rb +45 -0
  42. data/lib/castle-her/model/orm.rb +207 -0
  43. data/lib/castle-her/model/parse.rb +216 -0
  44. data/lib/castle-her/model/paths.rb +126 -0
  45. data/lib/castle-her/model/relation.rb +164 -0
  46. data/lib/castle-her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +166 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +290 -0
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Model::Associations::AssociationProxy do
5
+ describe "proxy assignment methods" do
6
+ before do
7
+ Her::API.setup url: "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ builder.adapter :test do |stub|
11
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke" }.to_json ] }
12
+ stub.get("/users/1/fish") { |env| [200, {}, { :id => 1, :name => "Tobias's Fish" }.to_json ] }
13
+ end
14
+ end
15
+ spawn_model "User" do
16
+ has_one :fish
17
+ end
18
+ spawn_model "Fish"
19
+ end
20
+
21
+ subject { User.find(1) }
22
+
23
+ it "should assign value" do
24
+ subject.fish.name = "Fishy"
25
+ expect(subject.fish.name).to eq "Fishy"
26
+ end
27
+ end
28
+ end
29
+
30
+
31
+
@@ -0,0 +1,504 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::Associations do
5
+ context "setting associations without details" do
6
+ before { spawn_model "Foo::User" }
7
+ subject { Foo::User.associations }
8
+
9
+ context "single has_many association" do
10
+ before { Foo::User.has_many :comments }
11
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Comment", :path => "/comments", :inverse_of => nil }] }
12
+ end
13
+
14
+ context "multiple has_many associations" do
15
+ before do
16
+ Foo::User.has_many :comments
17
+ Foo::User.has_many :posts
18
+ end
19
+
20
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Comment", :path => "/comments", :inverse_of => nil }, { :name => :posts, :data_key => :posts, :default => [], :class_name => "Post", :path => "/posts", :inverse_of => nil }] }
21
+ end
22
+
23
+ context "single has_one association" do
24
+ before { Foo::User.has_one :category }
25
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :category, :default => nil, :class_name => "Category", :path => "/category" }] }
26
+ end
27
+
28
+ context "multiple has_one associations" do
29
+ before do
30
+ Foo::User.has_one :category
31
+ Foo::User.has_one :role
32
+ end
33
+
34
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :category, :default => nil, :class_name => "Category", :path => "/category" }, { :name => :role, :data_key => :role, :default => nil, :class_name => "Role", :path => "/role" }] }
35
+ end
36
+
37
+ context "single belongs_to association" do
38
+ before { Foo::User.belongs_to :organization }
39
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :organization, :default => nil, :class_name => "Organization", :foreign_key => "organization_id", :path => "/organizations/:id" }] }
40
+ end
41
+
42
+ context "multiple belongs_to association" do
43
+ before do
44
+ Foo::User.belongs_to :organization
45
+ Foo::User.belongs_to :family
46
+ end
47
+
48
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :organization, :default => nil, :class_name => "Organization", :foreign_key => "organization_id", :path => "/organizations/:id" }, { :name => :family, :data_key => :family, :default => nil, :class_name => "Family", :foreign_key => "family_id", :path => "/families/:id" }] }
49
+ end
50
+ end
51
+
52
+ context "setting associations with details" do
53
+ before { spawn_model "Foo::User" }
54
+ subject { Foo::User.associations }
55
+
56
+ context "in base class" do
57
+ context "single has_many association" do
58
+ before { Foo::User.has_many :comments, :class_name => "Post", :inverse_of => :admin, :data_key => :user_comments, :default => {} }
59
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :user_comments, :default => {}, :class_name => "Post", :path => "/comments", :inverse_of => :admin }] }
60
+ end
61
+
62
+ context "single has_one association" do
63
+ before { Foo::User.has_one :category, :class_name => "Topic", :foreign_key => "topic_id", :data_key => :topic, :default => nil }
64
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :topic, :default => nil, :class_name => "Topic", :foreign_key => "topic_id", :path => "/category" }] }
65
+ end
66
+
67
+ context "single belongs_to association" do
68
+ before { Foo::User.belongs_to :organization, :class_name => "Business", :foreign_key => "org_id", :data_key => :org, :default => true }
69
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :org, :default => true, :class_name => "Business", :foreign_key => "org_id", :path => "/organizations/:id" }] }
70
+ end
71
+ end
72
+
73
+ context "in parent class" do
74
+ before { Foo::User.has_many :comments, :class_name => "Post" }
75
+
76
+ describe "associations accessor" do
77
+ subject { Class.new(Foo::User).associations }
78
+ its(:object_id) { should_not eql Foo::User.associations.object_id }
79
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Post", :path => "/comments", :inverse_of => nil }] }
80
+ end
81
+ end
82
+ end
83
+
84
+ context "handling associations without details" do
85
+ before do
86
+ Her::API.setup :url => "https://api.example.com" do |builder|
87
+ builder.use Her::Middleware::FirstLevelParseJSON
88
+ builder.use Faraday::Request::UrlEncoded
89
+ builder.adapter :test do |stub|
90
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :comments => [{ :comment => { :id => 2, :body => "Tobias, you blow hard!", :user_id => 1 } }, { :comment => { :id => 3, :body => "I wouldn't mind kissing that man between the cheeks, so to speak", :user_id => 1 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 1, :name => "Bluth Company" }, :organization_id => 1 }.to_json] }
91
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 2 }.to_json] }
92
+ stub.get("/users/1/comments") { |env| [200, {}, [{ :comment => { :id => 4, :body => "They're having a FIRESALE?" } }].to_json] }
93
+ stub.get("/users/2/comments") { |env| [200, {}, [{ :comment => { :id => 4, :body => "They're having a FIRESALE?" } }, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }].to_json] }
94
+ stub.get("/users/2/comments/5") { |env| [200, {}, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }.to_json] }
95
+ stub.get("/users/2/role") { |env| [200, {}, { :id => 2, :body => "User" }.to_json] }
96
+ stub.get("/users/1/role") { |env| [200, {}, { :id => 3, :body => "User" }.to_json] }
97
+ stub.get("/users/1/posts") { |env| [200, {}, [{:id => 1, :body => 'blogging stuff', :admin_id => 1 }].to_json] }
98
+ stub.get("/organizations/1") { |env| [200, {}, { :organization => { :id => 1, :name => "Bluth Company Foo" } }.to_json] }
99
+ stub.post("/users") { |env| [200, {}, { :id => 5, :name => "Mr. Krabs", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
100
+ stub.put("/users/5") { |env| [200, {}, { :id => 5, :name => "Clancy Brown", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
101
+ stub.delete("/users/5") { |env| [200, {}, { :id => 5, :name => "Clancy Brown", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
102
+
103
+ stub.get("/organizations/2") do |env|
104
+ if env[:params]["admin"] == "true"
105
+ [200, {}, { :organization => { :id => 2, :name => "Bluth Company (admin)" } }.to_json]
106
+ else
107
+ [200, {}, { :organization => { :id => 2, :name => "Bluth Company" } }.to_json]
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ spawn_model "Foo::User" do
114
+ has_many :comments, class_name: "Foo::Comment"
115
+ has_one :role
116
+ belongs_to :organization
117
+ has_many :posts, :inverse_of => :admin
118
+ end
119
+ spawn_model "Foo::Comment" do
120
+ belongs_to :user
121
+ parse_root_in_json true
122
+ end
123
+ spawn_model "Foo::Post" do
124
+ belongs_to :admin, :class_name => 'Foo::User'
125
+ end
126
+
127
+ spawn_model "Foo::Organization" do
128
+ parse_root_in_json true
129
+ end
130
+
131
+ spawn_model "Foo::Role"
132
+
133
+ @user_with_included_data = Foo::User.find(1)
134
+ @user_without_included_data = Foo::User.find(2)
135
+ @user_without_organization_and_not_persisted = Foo::User.new(organization_id: nil, name: "Katlin Fünke")
136
+ end
137
+
138
+ let(:user_with_included_data_after_create) { Foo::User.create }
139
+ let(:user_with_included_data_after_save_existing) { Foo::User.save_existing(5, :name => "Clancy Brown") }
140
+ let(:user_with_included_data_after_destroy) { Foo::User.new(:id => 5).destroy }
141
+ let(:comment_without_included_parent_data) { Foo::Comment.new(:id => 7, :user_id => 1) }
142
+
143
+ it "maps an array of included data through has_many" do
144
+ expect(@user_with_included_data.comments.first).to be_a(Foo::Comment)
145
+ expect(@user_with_included_data.comments.length).to eq(2)
146
+ expect(@user_with_included_data.comments.first.id).to eq(2)
147
+ expect(@user_with_included_data.comments.first.body).to eq("Tobias, you blow hard!")
148
+ end
149
+
150
+ it "does not refetch the parents models data if they have been fetched before" do
151
+ expect(@user_with_included_data.comments.first.user.object_id).to eq(@user_with_included_data.object_id)
152
+ end
153
+
154
+ it "does fetch the parent models data only once" do
155
+ expect(comment_without_included_parent_data.user.object_id).to eq(comment_without_included_parent_data.user.object_id)
156
+ end
157
+
158
+ it "does fetch the parent models data that was cached if called with parameters" do
159
+ expect(comment_without_included_parent_data.user.object_id).not_to eq(comment_without_included_parent_data.user.where(:a => 2).object_id)
160
+ end
161
+
162
+ it "uses the given inverse_of key to set the parent model" do
163
+ expect(@user_with_included_data.posts.first.admin.object_id).to eq(@user_with_included_data.object_id)
164
+ end
165
+
166
+ it "fetches data that was not included through has_many" do
167
+ expect(@user_without_included_data.comments.first).to be_a(Foo::Comment)
168
+ expect(@user_without_included_data.comments.length).to eq(2)
169
+ expect(@user_without_included_data.comments.first.id).to eq(4)
170
+ expect(@user_without_included_data.comments.first.body).to eq("They're having a FIRESALE?")
171
+ end
172
+
173
+ it "fetches has_many data even if it was included, only if called with parameters" do
174
+ expect(@user_with_included_data.comments.where(:foo_id => 1).length).to eq(1)
175
+ end
176
+
177
+ it "fetches data that was not included through has_many only once" do
178
+ expect(@user_without_included_data.comments.first.object_id).to eq(@user_without_included_data.comments.first.object_id)
179
+ end
180
+
181
+ it "fetches data that was cached through has_many if called with parameters" do
182
+ expect(@user_without_included_data.comments.first.object_id).not_to eq(@user_without_included_data.comments.where(:foo_id => 1).first.object_id)
183
+ end
184
+
185
+ it "maps an array of included data through has_one" do
186
+ expect(@user_with_included_data.role).to be_a(Foo::Role)
187
+ expect(@user_with_included_data.role.object_id).to eq(@user_with_included_data.role.object_id)
188
+ expect(@user_with_included_data.role.id).to eq(1)
189
+ expect(@user_with_included_data.role.body).to eq("Admin")
190
+ end
191
+
192
+ it "fetches data that was not included through has_one" do
193
+ expect(@user_without_included_data.role).to be_a(Foo::Role)
194
+ expect(@user_without_included_data.role.id).to eq(2)
195
+ expect(@user_without_included_data.role.body).to eq("User")
196
+ end
197
+
198
+ it "fetches has_one data even if it was included, only if called with parameters" do
199
+ expect(@user_with_included_data.role.where(:foo_id => 2).id).to eq(3)
200
+ end
201
+
202
+ it "maps an array of included data through belongs_to" do
203
+ expect(@user_with_included_data.organization).to be_a(Foo::Organization)
204
+ expect(@user_with_included_data.organization.id).to eq(1)
205
+ expect(@user_with_included_data.organization.name).to eq("Bluth Company")
206
+ end
207
+
208
+ it "fetches data that was not included through belongs_to" do
209
+ expect(@user_without_included_data.organization).to be_a(Foo::Organization)
210
+ expect(@user_without_included_data.organization.id).to eq(2)
211
+ expect(@user_without_included_data.organization.name).to eq("Bluth Company")
212
+ end
213
+
214
+ it "returns nil if the foreign key is nil" do
215
+ expect(@user_without_organization_and_not_persisted.organization).to eq(nil)
216
+ end
217
+
218
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
219
+ expect(@user_with_included_data.organization.where(:foo_id => 1).name).to eq("Bluth Company Foo")
220
+ end
221
+
222
+ it "can tell if it has a association" do
223
+ expect(@user_without_included_data.has_association?(:unknown_association)).to eq(false)
224
+ expect(@user_without_included_data.has_association?(:organization)).to eq(true)
225
+ end
226
+
227
+ it "fetches the resource corresponding to a named association" do
228
+ expect(@user_without_included_data.get_association(:unknown_association)).to eq(nil)
229
+ expect(@user_without_included_data.get_association(:organization).name).to eq("Bluth Company")
230
+ end
231
+
232
+ it "pass query string parameters when additional arguments are passed" do
233
+ expect(@user_without_included_data.organization.where(:admin => true).name).to eq("Bluth Company (admin)")
234
+ expect(@user_without_included_data.organization.name).to eq("Bluth Company")
235
+ end
236
+
237
+ it "fetches data with the specified id when calling find" do
238
+ comment = @user_without_included_data.comments.find(5)
239
+ expect(comment).to be_a(Foo::Comment)
240
+ expect(comment.id).to eq(5)
241
+ end
242
+
243
+ it "'s associations responds to #empty?" do
244
+ expect(@user_without_included_data.organization.respond_to?(:empty?)).to be_truthy
245
+ expect(@user_without_included_data.organization).not_to be_empty
246
+ end
247
+
248
+ it 'includes has_many relationships in params by default' do
249
+ params = @user_with_included_data.to_params
250
+ expect(params[:comments]).to be_kind_of(Array)
251
+ expect(params[:comments].length).to eq(2)
252
+ end
253
+
254
+ [:create, :save_existing, :destroy].each do |type|
255
+ context "after #{type}" do
256
+ let(:subject) { self.send("user_with_included_data_after_#{type}")}
257
+
258
+ it "maps an array of included data through has_many" do
259
+ expect(subject.comments.first).to be_a(Foo::Comment)
260
+ expect(subject.comments.length).to eq(1)
261
+ expect(subject.comments.first.id).to eq(99)
262
+ expect(subject.comments.first.body).to eq("Rodríguez, nasibisibusi?")
263
+ end
264
+
265
+ it "maps an array of included data through has_one" do
266
+ expect(subject.role).to be_a(Foo::Role)
267
+ expect(subject.role.id).to eq(1)
268
+ expect(subject.role.body).to eq("Admin")
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ context "handling associations with details in active_model_serializers format" do
275
+ before do
276
+ Her::API.setup :url => "https://api.example.com" do |builder|
277
+ builder.use Her::Middleware::FirstLevelParseJSON
278
+ builder.use Faraday::Request::UrlEncoded
279
+ builder.adapter :test do |stub|
280
+ stub.get("/users/1") { |env| [200, {}, { :user => { :id => 1, :name => "Tobias Fünke", :comments => [{ :id => 2, :body => "Tobias, you blow hard!", :user_id => 1 }, { :id => 3, :body => "I wouldn't mind kissing that man between the cheeks, so to speak", :user_id => 1 }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 1, :name => "Bluth Company" }, :organization_id => 1 } }.to_json] }
281
+ stub.get("/users/2") { |env| [200, {}, { :user => { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 } }.to_json] }
282
+ stub.get("/users/1/comments") { |env| [200, {}, { :comments => [{ :id => 4, :body => "They're having a FIRESALE?" }] }.to_json] }
283
+ stub.get("/users/2/comments") { |env| [200, {}, { :comments => [{ :id => 4, :body => "They're having a FIRESALE?" }, { :id => 5, :body => "Is this the tiny town from Footloose?" }] }.to_json] }
284
+ stub.get("/users/2/comments/5") { |env| [200, {}, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }.to_json] }
285
+ stub.get("/organizations/1") { |env| [200, {}, { :organization => { :id => 1, :name => "Bluth Company Foo" } }.to_json] }
286
+ end
287
+ end
288
+ spawn_model "Foo::User" do
289
+ parse_root_in_json true, :format => :active_model_serializers
290
+ has_many :comments, class_name: "Foo::Comment"
291
+ belongs_to :organization
292
+ end
293
+ spawn_model "Foo::Comment" do
294
+ belongs_to :user
295
+ parse_root_in_json true, :format => :active_model_serializers
296
+ end
297
+ spawn_model "Foo::Organization" do
298
+ parse_root_in_json true, :format => :active_model_serializers
299
+ end
300
+
301
+ @user_with_included_data = Foo::User.find(1)
302
+ @user_without_included_data = Foo::User.find(2)
303
+ end
304
+
305
+ it "maps an array of included data through has_many" do
306
+ expect(@user_with_included_data.comments.first).to be_a(Foo::Comment)
307
+ expect(@user_with_included_data.comments.length).to eq(2)
308
+ expect(@user_with_included_data.comments.first.id).to eq(2)
309
+ expect(@user_with_included_data.comments.first.body).to eq("Tobias, you blow hard!")
310
+ end
311
+
312
+ it "does not refetch the parents models data if they have been fetched before" do
313
+ expect(@user_with_included_data.comments.first.user.object_id).to eq(@user_with_included_data.object_id)
314
+ end
315
+
316
+ it "fetches data that was not included through has_many" do
317
+ expect(@user_without_included_data.comments.first).to be_a(Foo::Comment)
318
+ expect(@user_without_included_data.comments.length).to eq(2)
319
+ expect(@user_without_included_data.comments.first.id).to eq(4)
320
+ expect(@user_without_included_data.comments.first.body).to eq("They're having a FIRESALE?")
321
+ end
322
+
323
+ it "fetches has_many data even if it was included, only if called with parameters" do
324
+ expect(@user_with_included_data.comments.where(:foo_id => 1).length).to eq(1)
325
+ end
326
+
327
+ it "maps an array of included data through belongs_to" do
328
+ expect(@user_with_included_data.organization).to be_a(Foo::Organization)
329
+ expect(@user_with_included_data.organization.id).to eq(1)
330
+ expect(@user_with_included_data.organization.name).to eq("Bluth Company")
331
+ end
332
+
333
+ it "fetches data that was not included through belongs_to" do
334
+ expect(@user_without_included_data.organization).to be_a(Foo::Organization)
335
+ expect(@user_without_included_data.organization.id).to eq(1)
336
+ expect(@user_without_included_data.organization.name).to eq("Bluth Company Foo")
337
+ end
338
+
339
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
340
+ expect(@user_with_included_data.organization.where(:foo_id => 1).name).to eq("Bluth Company Foo")
341
+ end
342
+
343
+ it "fetches data with the specified id when calling find" do
344
+ comment = @user_without_included_data.comments.find(5)
345
+ expect(comment).to be_a(Foo::Comment)
346
+ expect(comment.id).to eq(5)
347
+ end
348
+
349
+ it 'includes has_many relationships in params by default' do
350
+ params = @user_with_included_data.to_params
351
+ expect(params[:comments]).to be_kind_of(Array)
352
+ expect(params[:comments].length).to eq(2)
353
+ end
354
+ end
355
+
356
+ context "handling associations with details" do
357
+ before do
358
+ Her::API.setup :url => "https://api.example.com" do |builder|
359
+ builder.use Her::Middleware::FirstLevelParseJSON
360
+ builder.use Faraday::Request::UrlEncoded
361
+ builder.adapter :test do |stub|
362
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." }, :organization_id => 1 }.to_json] }
363
+ stub.get("/users/4") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." } }.to_json] }
364
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 }.to_json] }
365
+ stub.get("/users/3") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :company => nil }.to_json] }
366
+ stub.get("/companies/1") { |env| [200, {}, { :id => 1, :name => "Bluth Company" }.to_json] }
367
+ end
368
+ end
369
+
370
+ spawn_model "Foo::User" do
371
+ belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id, :data_key => :organization
372
+ end
373
+
374
+ spawn_model "Foo::Company"
375
+
376
+ @user_with_included_data = Foo::User.find(1)
377
+ @user_without_included_data = Foo::User.find(2)
378
+ @user_with_included_nil_data = Foo::User.find(3)
379
+ @user_with_included_data_but_no_fk = Foo::User.find(4)
380
+ end
381
+
382
+ it "maps an array of included data through belongs_to" do
383
+ expect(@user_with_included_data.company).to be_a(Foo::Company)
384
+ expect(@user_with_included_data.company.id).to eq(1)
385
+ expect(@user_with_included_data.company.name).to eq("Bluth Company Inc.")
386
+ end
387
+
388
+ it "does not map included data if it’s nil" do
389
+ expect(@user_with_included_nil_data.company).to eq(nil)
390
+ end
391
+
392
+ it "fetches data that was not included through belongs_to" do
393
+ expect(@user_without_included_data.company).to be_a(Foo::Company)
394
+ expect(@user_without_included_data.company.id).to eq(1)
395
+ expect(@user_without_included_data.company.name).to eq("Bluth Company")
396
+ end
397
+
398
+ it "does not require foreugn key to have nested object" do
399
+ expect(@user_with_included_data_but_no_fk.company.name).to eq("Bluth Company Inc.")
400
+ end
401
+ end
402
+
403
+ context "object returned by the association method" do
404
+ before do
405
+ spawn_model "Foo::Role" do
406
+ def present?
407
+ "of_course"
408
+ end
409
+ end
410
+ spawn_model "Foo::User" do
411
+ has_one :role
412
+ end
413
+ end
414
+
415
+ let(:associated_value) { Foo::Role.new }
416
+ let(:user_with_role) do
417
+ Foo::User.new.tap { |user| user.role = associated_value }
418
+ end
419
+
420
+ subject { user_with_role.role }
421
+
422
+ it "doesnt mask the object's basic methods" do
423
+ expect(subject.class).to eq(Foo::Role)
424
+ end
425
+
426
+ it "doesnt mask core methods like extend" do
427
+ committer = Module.new
428
+ subject.extend committer
429
+ expect(associated_value).to be_kind_of committer
430
+ end
431
+
432
+ it "can return the association object" do
433
+ expect(subject.association).to be_kind_of(Her::Model::Associations::Association)
434
+ end
435
+
436
+ it "still can call fetch via the association" do
437
+ expect(subject.association.fetch).to eq(associated_value)
438
+ end
439
+
440
+ it "calls missing methods on associated value" do
441
+ expect(subject.present?).to eq("of_course")
442
+ end
443
+
444
+ it "can use association methods like where" do
445
+ expect(subject.where(role: 'committer').association.
446
+ params).to include(:role)
447
+ end
448
+ end
449
+
450
+ context "building and creating association data" do
451
+ before do
452
+ spawn_model "Foo::Comment"
453
+ spawn_model "Foo::User" do
454
+ has_many :comments
455
+ end
456
+ end
457
+
458
+ context "with #build" do
459
+ it "takes the parent primary key" do
460
+ @comment = Foo::User.new(:id => 10).comments.build(:body => "Hello!")
461
+ expect(@comment.body).to eq("Hello!")
462
+ expect(@comment.user_id).to eq(10)
463
+ end
464
+ end
465
+
466
+ context "with #create" do
467
+ before do
468
+ Her::API.setup :url => "https://api.example.com" do |builder|
469
+ builder.use Her::Middleware::FirstLevelParseJSON
470
+ builder.use Faraday::Request::UrlEncoded
471
+ builder.adapter :test do |stub|
472
+ stub.get("/users/10") { |env| [200, {}, { :id => 10 }.to_json] }
473
+ stub.post("/comments") { |env| [200, {}, { :id => 1, :body => Faraday::Utils.parse_query(env[:body])['body'], :user_id => Faraday::Utils.parse_query(env[:body])['user_id'].to_i }.to_json] }
474
+ end
475
+ end
476
+
477
+ Foo::User.use_api Her::API.default_api
478
+ Foo::Comment.use_api Her::API.default_api
479
+ end
480
+
481
+ it "takes the parent primary key and saves the resource" do
482
+ @user = Foo::User.find(10)
483
+ @comment = @user.comments.create(:body => "Hello!")
484
+ expect(@comment.id).to eq(1)
485
+ expect(@comment.body).to eq("Hello!")
486
+ expect(@comment.user_id).to eq(10)
487
+ expect(@user.comments).to eq([@comment])
488
+ end
489
+ end
490
+
491
+ context "with #new" do
492
+ it "creates nested models from hash attibutes" do
493
+ user = Foo::User.new(:name => "vic", :comments => [{:text => "hello"}])
494
+ expect(user.comments.first.text).to eq("hello")
495
+ end
496
+
497
+ it "assigns nested models if given as already constructed objects" do
498
+ bye = Foo::Comment.new(:text => "goodbye")
499
+ user = Foo::User.new(:name => 'vic', :comments => [bye])
500
+ expect(user.comments.first.text).to eq('goodbye')
501
+ end
502
+ end
503
+ end
504
+ end