restorm 1.0.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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +232 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +55 -0
  8. data/.yardopts +2 -0
  9. data/CONTRIBUTING.md +26 -0
  10. data/Gemfile +10 -0
  11. data/HER_README.md +1065 -0
  12. data/LICENSE +7 -0
  13. data/README.md +7 -0
  14. data/Rakefile +11 -0
  15. data/UPGRADE.md +101 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +6 -0
  17. data/gemfiles/Gemfile.activemodel-5.0 +6 -0
  18. data/gemfiles/Gemfile.activemodel-5.1 +6 -0
  19. data/gemfiles/Gemfile.activemodel-5.2 +6 -0
  20. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  21. data/lib/restorm/api.rb +121 -0
  22. data/lib/restorm/collection.rb +13 -0
  23. data/lib/restorm/errors.rb +29 -0
  24. data/lib/restorm/json_api/model.rb +42 -0
  25. data/lib/restorm/middleware/accept_json.rb +18 -0
  26. data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
  27. data/lib/restorm/middleware/json_api_parser.rb +37 -0
  28. data/lib/restorm/middleware/parse_json.rb +22 -0
  29. data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
  30. data/lib/restorm/middleware.rb +12 -0
  31. data/lib/restorm/model/associations/association.rb +128 -0
  32. data/lib/restorm/model/associations/association_proxy.rb +44 -0
  33. data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
  34. data/lib/restorm/model/associations/has_many_association.rb +100 -0
  35. data/lib/restorm/model/associations/has_one_association.rb +79 -0
  36. data/lib/restorm/model/associations.rb +141 -0
  37. data/lib/restorm/model/attributes.rb +322 -0
  38. data/lib/restorm/model/base.rb +33 -0
  39. data/lib/restorm/model/deprecated_methods.rb +61 -0
  40. data/lib/restorm/model/http.rb +119 -0
  41. data/lib/restorm/model/introspection.rb +67 -0
  42. data/lib/restorm/model/nested_attributes.rb +45 -0
  43. data/lib/restorm/model/orm.rb +299 -0
  44. data/lib/restorm/model/parse.rb +223 -0
  45. data/lib/restorm/model/paths.rb +125 -0
  46. data/lib/restorm/model/relation.rb +209 -0
  47. data/lib/restorm/model.rb +75 -0
  48. data/lib/restorm/version.rb +3 -0
  49. data/lib/restorm.rb +19 -0
  50. data/restorm.gemspec +29 -0
  51. data/spec/api_spec.rb +120 -0
  52. data/spec/collection_spec.rb +41 -0
  53. data/spec/json_api/model_spec.rb +169 -0
  54. data/spec/middleware/accept_json_spec.rb +11 -0
  55. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  56. data/spec/middleware/json_api_parser_spec.rb +52 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/spec/model/associations/association_proxy_spec.rb +29 -0
  59. data/spec/model/associations_spec.rb +911 -0
  60. data/spec/model/attributes_spec.rb +354 -0
  61. data/spec/model/callbacks_spec.rb +176 -0
  62. data/spec/model/dirty_spec.rb +133 -0
  63. data/spec/model/http_spec.rb +201 -0
  64. data/spec/model/introspection_spec.rb +81 -0
  65. data/spec/model/nested_attributes_spec.rb +135 -0
  66. data/spec/model/orm_spec.rb +704 -0
  67. data/spec/model/parse_spec.rb +520 -0
  68. data/spec/model/paths_spec.rb +348 -0
  69. data/spec/model/relation_spec.rb +247 -0
  70. data/spec/model/validations_spec.rb +43 -0
  71. data/spec/model_spec.rb +45 -0
  72. data/spec/spec_helper.rb +25 -0
  73. data/spec/support/macros/her_macros.rb +17 -0
  74. data/spec/support/macros/model_macros.rb +36 -0
  75. data/spec/support/macros/request_macros.rb +27 -0
  76. metadata +203 -0
@@ -0,0 +1,911 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Restorm::Model::Associations do
6
+ context "setting associations without details" do
7
+ before { spawn_model "Foo::User" }
8
+ subject(:associations) { Foo::User.associations }
9
+
10
+ describe "has_many associations" do
11
+ subject { associations[:has_many] }
12
+
13
+ context "single" do
14
+ let(:comments_association) do
15
+ {
16
+ name: :comments,
17
+ data_key: :comments,
18
+ default: [],
19
+ class_name: "Comment",
20
+ path: "/comments",
21
+ inverse_of: nil
22
+ }
23
+ end
24
+ before { Foo::User.has_many :comments }
25
+
26
+ it { is_expected.to eql [comments_association] }
27
+ end
28
+
29
+ context "multiple" do
30
+ let(:comments_association) do
31
+ {
32
+ name: :comments,
33
+ data_key: :comments,
34
+ default: [],
35
+ class_name: "Comment",
36
+ path: "/comments",
37
+ inverse_of: nil
38
+ }
39
+ end
40
+ let(:posts_association) do
41
+ {
42
+ name: :posts,
43
+ data_key: :posts,
44
+ default: [],
45
+ class_name: "Post",
46
+ path: "/posts",
47
+ inverse_of: nil
48
+ }
49
+ end
50
+ before do
51
+ Foo::User.has_many :comments
52
+ Foo::User.has_many :posts
53
+ end
54
+
55
+ it { is_expected.to eql [comments_association, posts_association] }
56
+ end
57
+ end
58
+
59
+ describe "has_one associations" do
60
+ subject { associations[:has_one] }
61
+
62
+ context "single" do
63
+ let(:category_association) do
64
+ {
65
+ name: :category,
66
+ data_key: :category,
67
+ default: nil,
68
+ class_name: "Category",
69
+ path: "/category"
70
+ }
71
+ end
72
+ before { Foo::User.has_one :category }
73
+
74
+ it { is_expected.to eql [category_association] }
75
+ end
76
+
77
+ context "multiple" do
78
+ let(:category_association) do
79
+ {
80
+ name: :category,
81
+ data_key: :category,
82
+ default: nil,
83
+ class_name: "Category",
84
+ path: "/category"
85
+ }
86
+ end
87
+ let(:role_association) do
88
+ {
89
+ name: :role,
90
+ data_key: :role,
91
+ default: nil,
92
+ class_name: "Role",
93
+ path: "/role"
94
+ }
95
+ end
96
+ before do
97
+ Foo::User.has_one :category
98
+ Foo::User.has_one :role
99
+ end
100
+
101
+ it { is_expected.to eql [category_association, role_association] }
102
+ end
103
+ end
104
+
105
+ describe "belongs_to associations" do
106
+ subject { associations[:belongs_to] }
107
+
108
+ context "single" do
109
+ let(:organization_association) do
110
+ {
111
+ name: :organization,
112
+ data_key: :organization,
113
+ default: nil,
114
+ class_name: "Organization",
115
+ foreign_key: "organization_id"
116
+ }
117
+ end
118
+ before { Foo::User.belongs_to :organization }
119
+
120
+ it { is_expected.to eql [organization_association] }
121
+ end
122
+
123
+ context "specifying non-default path" do
124
+ let(:path) { 'my_special_path' }
125
+ let(:organization_association) do
126
+ {
127
+ name: :organization,
128
+ data_key: :organization,
129
+ default: nil,
130
+ class_name: "Organization",
131
+ foreign_key: "organization_id",
132
+ path: path
133
+ }
134
+ end
135
+ before { Foo::User.belongs_to :organization, path: path }
136
+
137
+ it { is_expected.to eql [organization_association] }
138
+ end
139
+
140
+ context "multiple" do
141
+ let(:organization_association) do
142
+ {
143
+ name: :organization,
144
+ data_key: :organization,
145
+ default: nil,
146
+ class_name: "Organization",
147
+ foreign_key: "organization_id"
148
+ }
149
+ end
150
+ let(:family_association) do
151
+ {
152
+ name: :family,
153
+ data_key: :family,
154
+ default: nil,
155
+ class_name: "Family",
156
+ foreign_key: "family_id"
157
+ }
158
+ end
159
+ before do
160
+ Foo::User.belongs_to :organization
161
+ Foo::User.belongs_to :family
162
+ end
163
+
164
+ it { is_expected.to eql [organization_association, family_association] }
165
+ end
166
+ end
167
+ end
168
+
169
+ context "setting associations with details" do
170
+ before { spawn_model "Foo::User" }
171
+ subject(:associations) { Foo::User.associations }
172
+
173
+ context "in base class" do
174
+ describe "has_many associations" do
175
+ subject { associations[:has_many] }
176
+
177
+ context "single" do
178
+ let(:comments_association) do
179
+ {
180
+ name: :comments,
181
+ data_key: :user_comments,
182
+ default: {},
183
+ class_name: "Post",
184
+ path: "/comments",
185
+ inverse_of: :admin
186
+ }
187
+ end
188
+ before do
189
+ Foo::User.has_many :comments, class_name: "Post",
190
+ inverse_of: :admin,
191
+ data_key: :user_comments,
192
+ default: {}
193
+ end
194
+
195
+ it { is_expected.to eql [comments_association] }
196
+ end
197
+ end
198
+
199
+ describe "has_one associations" do
200
+ subject { associations[:has_one] }
201
+
202
+ context "single" do
203
+ let(:category_association) do
204
+ {
205
+ name: :category,
206
+ data_key: :topic,
207
+ default: nil,
208
+ class_name: "Topic",
209
+ foreign_key: "topic_id",
210
+ path: "/category"
211
+ }
212
+ end
213
+ before do
214
+ Foo::User.has_one :category, class_name: "Topic",
215
+ foreign_key: "topic_id",
216
+ data_key: :topic, default: nil
217
+ end
218
+
219
+ it { is_expected.to eql [category_association] }
220
+ end
221
+ end
222
+
223
+ describe "belongs_to associations" do
224
+ subject { associations[:belongs_to] }
225
+
226
+ context "single" do
227
+ let(:organization_association) do
228
+ {
229
+ name: :organization,
230
+ data_key: :org,
231
+ default: true,
232
+ class_name: "Business",
233
+ foreign_key: "org_id"
234
+ }
235
+ end
236
+ before do
237
+ Foo::User.belongs_to :organization, class_name: "Business",
238
+ foreign_key: "org_id",
239
+ data_key: :org,
240
+ default: true
241
+ end
242
+
243
+ it { is_expected.to eql [organization_association] }
244
+ end
245
+ end
246
+ end
247
+
248
+ context "in parent class" do
249
+ before { Foo::User.has_many :comments, class_name: "Post" }
250
+
251
+ describe "associations accessor" do
252
+ subject(:associations) { Class.new(Foo::User).associations }
253
+
254
+ describe "#object_id" do
255
+ subject { associations.object_id }
256
+ it { is_expected.not_to eql Foo::User.associations.object_id }
257
+ end
258
+
259
+ describe "[:has_many]" do
260
+ subject { associations[:has_many] }
261
+ let(:association) do
262
+ {
263
+ name: :comments,
264
+ data_key: :comments,
265
+ default: [],
266
+ class_name: "Post",
267
+ path: "/comments",
268
+ inverse_of: nil
269
+ }
270
+ end
271
+
272
+ it { is_expected.to eql [association] }
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ context "handling associations without details" do
279
+ before do
280
+ spawn_model "Foo::User" do
281
+ has_many :comments, class_name: "Foo::Comment"
282
+ has_one :role, class_name: "Foo::Role"
283
+ belongs_to :organization, class_name: "Foo::Organization"
284
+ has_many :posts, inverse_of: :admin
285
+ end
286
+
287
+ spawn_model "Foo::Comment" do
288
+ belongs_to :user
289
+ parse_root_in_json true
290
+ end
291
+
292
+ spawn_model "Foo::Post" do
293
+ belongs_to :admin, class_name: "Foo::User"
294
+ end
295
+
296
+ spawn_model "Foo::Organization" do
297
+ parse_root_in_json true
298
+ end
299
+
300
+ spawn_model "Foo::Role"
301
+ end
302
+
303
+ context "with included data" do
304
+ before(:context) do
305
+ Restorm::API.setup url: "https://api.example.com" do |builder|
306
+ builder.use Restorm::Middleware::FirstLevelParseJSON
307
+ builder.use Faraday::Request::UrlEncoded
308
+ builder.adapter :test do |stub|
309
+ stub.get("/users/1") { [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] }
310
+ stub.get("/users/1/comments") { [200, {}, [{ comment: { id: 4, body: "They're having a FIRESALE?" } }].to_json] }
311
+ stub.get("/users/1/role") { [200, {}, { id: 3, body: "User" }.to_json] }
312
+ stub.get("/users/1/posts") { [200, {}, [{ id: 1, body: "blogging stuff", admin_id: 1 }].to_json] }
313
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
314
+ end
315
+ end
316
+ end
317
+
318
+ let(:user) { Foo::User.find(1) }
319
+ let(:user_params) { user.to_params }
320
+
321
+ it "maps an array of included data through has_many" do
322
+ expect(user.comments.first).to be_a(Foo::Comment)
323
+ expect(user.comments.length).to eq(2)
324
+ expect(user.comments.first.id).to eq(2)
325
+ expect(user.comments.first.body).to eq("Tobias, you blow hard!")
326
+ end
327
+
328
+ it "does not refetch the parents models data if they have been fetched before" do
329
+ expect(user.comments.first.user.object_id).to eq(user.object_id)
330
+ end
331
+
332
+ it "uses the given inverse_of key to set the parent model" do
333
+ expect(user.posts.first.admin.object_id).to eq(user.object_id)
334
+ end
335
+
336
+ it "fetches has_many data even if it was included, only if called with parameters" do
337
+ expect(user.comments.where(foo_id: 1).length).to eq(1)
338
+ end
339
+
340
+ it "maps an array of included data through has_one" do
341
+ expect(user.role).to be_a(Foo::Role)
342
+ expect(user.role.object_id).to eq(user.role.object_id)
343
+ expect(user.role.id).to eq(1)
344
+ expect(user.role.body).to eq("Admin")
345
+ end
346
+
347
+ it "fetches has_one data even if it was included, only if called with parameters" do
348
+ expect(user.role.where(foo_id: 2).id).to eq(3)
349
+ end
350
+
351
+ it "maps an array of included data through belongs_to" do
352
+ expect(user.organization).to be_a(Foo::Organization)
353
+ expect(user.organization.id).to eq(1)
354
+ expect(user.organization.name).to eq("Bluth Company")
355
+ end
356
+
357
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
358
+ expect(user.organization.where(foo_id: 1).name).to eq("Bluth Company Foo")
359
+ end
360
+
361
+ it "includes has_many relationships in params by default" do
362
+ expect(user_params[:comments]).to be_kind_of(Array)
363
+ expect(user_params[:comments].length).to eq(2)
364
+ end
365
+
366
+ it "includes has_one relationship in params by default" do
367
+ expect(user_params[:role]).to be_kind_of(Hash)
368
+ expect(user_params[:role]).not_to be_empty
369
+ end
370
+
371
+ it "includes belongs_to relationship in params by default" do
372
+ expect(user_params[:organization]).to be_kind_of(Hash)
373
+ expect(user_params[:organization]).not_to be_empty
374
+ end
375
+ end
376
+
377
+ context "without included data" do
378
+ before(:context) do
379
+ Restorm::API.setup url: "https://api.example.com" do |builder|
380
+ builder.use Restorm::Middleware::FirstLevelParseJSON
381
+ builder.use Faraday::Request::UrlEncoded
382
+ builder.adapter :test do |stub|
383
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
384
+ stub.get("/users/2/comments") { [200, {}, [{ comment: { id: 4, body: "They're having a FIRESALE?" } }, { comment: { id: 5, body: "Is this the tiny town from Footloose?" } }].to_json] }
385
+ stub.get("/users/2/comments/5") { [200, {}, { comment: { id: 5, body: "Is this the tiny town from Footloose?" } }.to_json] }
386
+ stub.get("/users/2/role") { [200, {}, { id: 2, body: "User" }.to_json] }
387
+ stub.get("/organizations/2") do |env|
388
+ if env[:params]["admin"] == "true"
389
+ [200, {}, { organization: { id: 2, name: "Bluth Company (admin)" } }.to_json]
390
+ else
391
+ [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json]
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ let(:user) { Foo::User.find(2) }
399
+
400
+ it "fetches data that was not included through has_many" do
401
+ expect(user.comments.first).to be_a(Foo::Comment)
402
+ expect(user.comments.length).to eq(2)
403
+ expect(user.comments.first.id).to eq(4)
404
+ expect(user.comments.first.body).to eq("They're having a FIRESALE?")
405
+ end
406
+
407
+ it "fetches data that was not included through has_many only once" do
408
+ expect(user.comments.first.object_id).to eq(user.comments.first.object_id)
409
+ end
410
+
411
+ it "fetches data that was cached through has_many if called with parameters" do
412
+ expect(user.comments.first.object_id).not_to eq(user.comments.where(foo_id: 1).first.object_id)
413
+ end
414
+
415
+ it "fetches data again after being reloaded" do
416
+ expect { user.comments.reload }.to change { user.comments.first.object_id }
417
+ end
418
+
419
+ it "fetches data that was not included through has_one" do
420
+ expect(user.role).to be_a(Foo::Role)
421
+ expect(user.role.id).to eq(2)
422
+ expect(user.role.body).to eq("User")
423
+ end
424
+
425
+ it "fetches data that was not included through belongs_to" do
426
+ expect(user.organization).to be_a(Foo::Organization)
427
+ expect(user.organization.id).to eq(2)
428
+ expect(user.organization.name).to eq("Bluth Company")
429
+ end
430
+
431
+ it "can tell if it has a association" do
432
+ expect(user.has_association?(:unknown_association)).to be false
433
+ expect(user.has_association?(:organization)).to be true
434
+ end
435
+
436
+ it "fetches the resource corresponding to a named association" do
437
+ expect(user.get_association(:unknown_association)).to be_nil
438
+ expect(user.get_association(:organization).name).to eq("Bluth Company")
439
+ end
440
+
441
+ it "pass query string parameters when additional arguments are passed" do
442
+ expect(user.organization.where(admin: true).name).to eq("Bluth Company (admin)")
443
+ expect(user.organization.name).to eq("Bluth Company")
444
+ end
445
+
446
+ it "fetches data with the specified id when calling find" do
447
+ comment = user.comments.find(5)
448
+ expect(comment).to be_a(Foo::Comment)
449
+ expect(comment.id).to eq(5)
450
+ end
451
+
452
+ it "'s associations responds to #empty?" do
453
+ expect(user.organization.respond_to?(:empty?)).to be_truthy
454
+ expect(user.organization).not_to be_empty
455
+ end
456
+ end
457
+
458
+ context "without included parent data" do
459
+ before(:context) do
460
+ Restorm::API.setup url: "https://api.example.com" do |builder|
461
+ builder.use Restorm::Middleware::FirstLevelParseJSON
462
+ builder.use Faraday::Request::UrlEncoded
463
+ builder.adapter :test do |stub|
464
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
465
+ end
466
+ end
467
+ end
468
+
469
+ let(:comment) { Foo::Comment.new(id: 7, user_id: 1) }
470
+
471
+ it "does fetch the parent models data only once" do
472
+ expect(comment.user.object_id).to eq(comment.user.object_id)
473
+ end
474
+
475
+ it "does fetch the parent models data that was cached if called with parameters" do
476
+ expect(comment.user.object_id).not_to eq(comment.user.where(a: 2).object_id)
477
+ end
478
+ end
479
+
480
+ context "when resource is new" do
481
+ let(:new_user) { Foo::User.new }
482
+
483
+ it "doesn't attempt to fetch association data" do
484
+ expect(new_user.comments).to eq([])
485
+ expect(new_user.role).to be_nil
486
+ expect(new_user.organization).to be_nil
487
+ end
488
+ end
489
+
490
+ context "when foreign_key is nil" do
491
+ before do
492
+ spawn_model "Foo::User" do
493
+ belongs_to :organization, class_name: "Foo::Organization"
494
+ end
495
+
496
+ spawn_model "Foo::Organization" do
497
+ parse_root_in_json true
498
+ end
499
+ end
500
+
501
+ let(:user) { Foo::User.new(organization_id: nil, name: "Katlin Fünke") }
502
+
503
+ it "returns nil" do
504
+ expect(user.organization).to be_nil
505
+ end
506
+ end
507
+
508
+ context "after" do
509
+ before(:context) do
510
+ Restorm::API.setup url: "https://api.example.com" do |builder|
511
+ builder.use Restorm::Middleware::FirstLevelParseJSON
512
+ builder.use Faraday::Request::UrlEncoded
513
+ builder.adapter :test do |stub|
514
+ stub.post("/users") { [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] }
515
+ stub.put("/users/5") { [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] }
516
+ stub.delete("/users/5") { [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] }
517
+ end
518
+ end
519
+ end
520
+
521
+ let(:user_after_create) { Foo::User.create }
522
+ let(:user_after_save_existing) { Foo::User.save_existing(5, name: "Clancy Brown") }
523
+ let(:user_after_destroy) { Foo::User.new(id: 5).destroy }
524
+
525
+ [:create, :save_existing, :destroy].each do |type|
526
+ context "after #{type}" do
527
+ let(:subject) { send("user_after_#{type}") }
528
+
529
+ it "maps an array of included data through has_many" do
530
+ expect(subject.comments.first).to be_a(Foo::Comment)
531
+ expect(subject.comments.length).to eq(1)
532
+ expect(subject.comments.first.id).to eq(99)
533
+ expect(subject.comments.first.body).to eq("Rodríguez, nasibisibusi?")
534
+ end
535
+
536
+ it "maps an array of included data through has_one" do
537
+ expect(subject.role).to be_a(Foo::Role)
538
+ expect(subject.role.id).to eq(1)
539
+ expect(subject.role.body).to eq("Admin")
540
+ end
541
+ end
542
+ end
543
+ end
544
+ end
545
+
546
+ context "handling associations with collection_path" do
547
+ before do
548
+ spawn_model "Foo::Organization" do
549
+ has_many :users
550
+ parse_root_in_json true
551
+ collection_path '/special/organizations'
552
+ end
553
+ spawn_model "Foo::User" do
554
+ belongs_to :organization
555
+ end
556
+ end
557
+
558
+ context "without included data" do
559
+ before(:context) do
560
+ Restorm::API.setup url: "https://api.example.com" do |builder|
561
+ builder.use Restorm::Middleware::FirstLevelParseJSON
562
+ builder.use Faraday::Request::UrlEncoded
563
+ builder.adapter :test do |stub|
564
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
565
+ stub.get("/special/organizations/2") { [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json] }
566
+ end
567
+ end
568
+ end
569
+
570
+ let(:user) { Foo::User.find(2) }
571
+
572
+ it "fetches data that was not included through belongs_to" do
573
+ expect(user.organization).to be_a(Foo::Organization)
574
+ expect(user.organization.id).to eq(2)
575
+ expect(user.organization.name).to eq("Bluth Company")
576
+ end
577
+ end
578
+ end
579
+
580
+ context "handling associations with path_prefix" do
581
+ before do
582
+ spawn_model "Foo::Organization" do
583
+ has_many :users
584
+ parse_root_in_json true
585
+ end
586
+ spawn_model "Foo::User" do
587
+ belongs_to :organization
588
+ end
589
+ end
590
+
591
+ context "without included data" do
592
+ before(:context) do
593
+ Restorm::API.setup url: "https://api.example.com" do |builder|
594
+ builder.use Restorm::Middleware::FirstLevelParseJSON
595
+ builder.use Faraday::Request::UrlEncoded
596
+ builder.path_prefix = 'special'
597
+ builder.adapter :test do |stub|
598
+ stub.get("/special/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
599
+ stub.get("/special/organizations/2") { [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json] }
600
+ end
601
+ end
602
+ end
603
+
604
+ let(:user) { Foo::User.find(2) }
605
+
606
+ it "fetches data that was not included through belongs_to" do
607
+ expect(user.organization).to be_a(Foo::Organization)
608
+ expect(user.organization.id).to eq(2)
609
+ expect(user.organization.name).to eq("Bluth Company")
610
+ end
611
+ end
612
+ end
613
+
614
+ context "handling associations with details in active_model_serializers format" do
615
+ before do
616
+ spawn_model "Foo::User" do
617
+ parse_root_in_json true, format: :active_model_serializers
618
+ has_many :comments, class_name: "Foo::Comment"
619
+ has_one :role, class_name: "Foo::Role"
620
+ belongs_to :organization, class_name: "Foo::Organization"
621
+ end
622
+
623
+ spawn_model "Foo::Role" do
624
+ belongs_to :user
625
+ parse_root_in_json true, format: :active_model_serializers
626
+ end
627
+
628
+ spawn_model "Foo::Comment" do
629
+ belongs_to :user
630
+ parse_root_in_json true, format: :active_model_serializers
631
+ end
632
+
633
+ spawn_model "Foo::Organization" do
634
+ parse_root_in_json true, format: :active_model_serializers
635
+ end
636
+ end
637
+
638
+ context "with included data" do
639
+ before(:context) do
640
+ Restorm::API.setup url: "https://api.example.com" do |builder|
641
+ builder.use Restorm::Middleware::FirstLevelParseJSON
642
+ builder.use Faraday::Request::UrlEncoded
643
+ builder.adapter :test do |stub|
644
+ stub.get("/users/1") { [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] }
645
+ stub.get("/users/1/comments") { [200, {}, { comments: [{ id: 4, body: "They're having a FIRESALE?" }] }.to_json] }
646
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
647
+ end
648
+ end
649
+ end
650
+
651
+ let(:user) { Foo::User.find(1) }
652
+ let(:user_params) { user.to_params }
653
+
654
+ it "maps an array of included data through has_many" do
655
+ expect(user.comments.first).to be_a(Foo::Comment)
656
+ expect(user.comments.length).to eq(2)
657
+ expect(user.comments.first.id).to eq(2)
658
+ expect(user.comments.first.body).to eq("Tobias, you blow hard!")
659
+ end
660
+
661
+ it "does not refetch the parents models data if they have been fetched before" do
662
+ expect(user.comments.first.user.object_id).to eq(user.object_id)
663
+ end
664
+
665
+ it "fetches has_many data even if it was included, only if called with parameters" do
666
+ expect(user.comments.where(foo_id: 1).length).to eq(1)
667
+ end
668
+
669
+ it "maps an array of included data through belongs_to" do
670
+ expect(user.organization).to be_a(Foo::Organization)
671
+ expect(user.organization.id).to eq(1)
672
+ expect(user.organization.name).to eq("Bluth Company")
673
+ end
674
+
675
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
676
+ expect(user.organization.where(foo_id: 1).name).to eq("Bluth Company Foo")
677
+ end
678
+
679
+ it "includes has_many relationships in params by default" do
680
+ expect(user_params[:comments]).to be_kind_of(Array)
681
+ expect(user_params[:comments].length).to eq(2)
682
+ end
683
+
684
+ it "includes has_one relationships in params by default" do
685
+ expect(user_params[:role]).to be_kind_of(Hash)
686
+ expect(user_params[:role]).not_to be_empty
687
+ end
688
+
689
+ it "includes belongs_to relationship in params by default" do
690
+ expect(user_params[:organization]).to be_kind_of(Hash)
691
+ expect(user_params[:organization]).not_to be_empty
692
+ end
693
+ end
694
+
695
+ context "without included data" do
696
+ before(:context) do
697
+ Restorm::API.setup url: "https://api.example.com" do |builder|
698
+ builder.use Restorm::Middleware::FirstLevelParseJSON
699
+ builder.use Faraday::Request::UrlEncoded
700
+ builder.adapter :test do |stub|
701
+ stub.get("/users/2") { [200, {}, { user: { id: 2, name: "Lindsay Fünke", organization_id: 1 } }.to_json] }
702
+ stub.get("/users/2/comments") { [200, {}, { comments: [{ id: 4, body: "They're having a FIRESALE?" }, { id: 5, body: "Is this the tiny town from Footloose?" }] }.to_json] }
703
+ stub.get("/users/2/comments/5") { [200, {}, { comment: { id: 5, body: "Is this the tiny town from Footloose?" } }.to_json] }
704
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
705
+ end
706
+ end
707
+ end
708
+
709
+ let(:user) { Foo::User.find(2) }
710
+
711
+ it "fetches data that was not included through has_many" do
712
+ expect(user.comments.first).to be_a(Foo::Comment)
713
+ expect(user.comments.length).to eq(2)
714
+ expect(user.comments.first.id).to eq(4)
715
+ expect(user.comments.first.body).to eq("They're having a FIRESALE?")
716
+ end
717
+
718
+ it "fetches data that was not included through belongs_to" do
719
+ expect(user.organization).to be_a(Foo::Organization)
720
+ expect(user.organization.id).to eq(1)
721
+ expect(user.organization.name).to eq("Bluth Company Foo")
722
+ end
723
+
724
+ it "fetches data with the specified id when calling find" do
725
+ comment = user.comments.find(5)
726
+ expect(comment).to be_a(Foo::Comment)
727
+ expect(comment.id).to eq(5)
728
+ end
729
+ end
730
+ end
731
+
732
+ context "handling associations with details" do
733
+ before do
734
+ spawn_model "Foo::User" do
735
+ belongs_to :company, path: "/organizations/:id", foreign_key: :organization_id, data_key: :organization
736
+ end
737
+
738
+ spawn_model "Foo::Company"
739
+ end
740
+
741
+ context "with included data" do
742
+ before(:context) do
743
+ Restorm::API.setup url: "https://api.example.com" do |builder|
744
+ builder.use Restorm::Middleware::FirstLevelParseJSON
745
+ builder.use Faraday::Request::UrlEncoded
746
+ builder.adapter :test do |stub|
747
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Tobias Fünke", organization: { id: 1, name: "Bluth Company Inc." }, organization_id: 1 }.to_json] }
748
+ stub.get("/users/4") { [200, {}, { id: 1, name: "Tobias Fünke", organization: { id: 1, name: "Bluth Company Inc." } }.to_json] }
749
+ stub.get("/users/3") { [200, {}, { id: 2, name: "Lindsay Fünke", organization: nil }.to_json] }
750
+ end
751
+ end
752
+ end
753
+
754
+ let(:user) { Foo::User.find(1) }
755
+
756
+ it "maps an array of included data through belongs_to" do
757
+ expect(user.company).to be_a(Foo::Company)
758
+ expect(user.company.id).to eq(1)
759
+ expect(user.company.name).to eq("Bluth Company Inc.")
760
+ end
761
+
762
+ context "when included data is nil" do
763
+ let(:user) { Foo::User.find(3) }
764
+
765
+ it "does not map included data" do
766
+ expect(user.company).to be_nil
767
+ end
768
+ end
769
+
770
+ context "when included data has no foreign_key" do
771
+ let(:user) { Foo::User.find(4) }
772
+
773
+ it "maps included data anyway" do
774
+ expect(user.company.name).to eq("Bluth Company Inc.")
775
+ end
776
+ end
777
+ end
778
+
779
+ context "without included data" do
780
+ before(:context) do
781
+ Restorm::API.setup url: "https://api.example.com" do |builder|
782
+ builder.use Restorm::Middleware::FirstLevelParseJSON
783
+ builder.use Faraday::Request::UrlEncoded
784
+ builder.adapter :test do |stub|
785
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 1 }.to_json] }
786
+ stub.get("/organizations/1") { [200, {}, { id: 1, name: "Bluth Company" }.to_json] }
787
+ end
788
+ end
789
+ end
790
+
791
+ let(:user) { Foo::User.find(2) }
792
+
793
+ it "fetches data that was not included through belongs_to" do
794
+ expect(user.company).to be_a(Foo::Company)
795
+ expect(user.company.id).to eq(1)
796
+ expect(user.company.name).to eq("Bluth Company")
797
+ end
798
+ end
799
+ end
800
+
801
+ context "object returned by the association method" do
802
+ before do
803
+ spawn_model "Foo::Role" do
804
+ def present?
805
+ "of_course"
806
+ end
807
+ end
808
+ spawn_model "Foo::User" do
809
+ has_one :role
810
+ end
811
+ end
812
+
813
+ let(:associated_value) { Foo::Role.new }
814
+ let(:user_with_role) do
815
+ Foo::User.new.tap { |user| user.role = associated_value }
816
+ end
817
+
818
+ subject { user_with_role.role }
819
+
820
+ it "doesnt mask the object's basic methods" do
821
+ expect(subject.class).to eq(Foo::Role)
822
+ end
823
+
824
+ it "doesnt mask core methods like extend" do
825
+ committer = Module.new
826
+ subject.extend committer
827
+ expect(associated_value).to be_kind_of committer
828
+ end
829
+
830
+ it "can return the association object" do
831
+ expect(subject.association).to be_kind_of Restorm::Model::Associations::Association
832
+ end
833
+
834
+ it "still can call fetch via the association" do
835
+ expect(subject.association.fetch).to eq associated_value
836
+ end
837
+
838
+ it "calls missing methods on associated value" do
839
+ expect(subject.present?).to eq("of_course")
840
+ end
841
+
842
+ it "can use association methods like where" do
843
+ expect(subject.where(role: "committer").association
844
+ .params).to include :role
845
+ end
846
+ end
847
+
848
+ context "building and creating association data" do
849
+ before do
850
+ spawn_model "Foo::Comment"
851
+ spawn_model "Foo::User" do
852
+ has_many :comments
853
+ end
854
+ end
855
+
856
+ context "with #build" do
857
+ let(:comment) { Foo::User.new(id: 10).comments.build(body: "Hello!") }
858
+
859
+ it "takes the parent primary key" do
860
+ expect(comment.body).to eq("Hello!")
861
+ expect(comment.user_id).to eq(10)
862
+ end
863
+ end
864
+
865
+ context "with #create" do
866
+ let(:user) { Foo::User.find(10) }
867
+ let(:comment) { user.comments.create(body: "Hello!") }
868
+
869
+ before do
870
+ Restorm::API.setup url: "https://api.example.com" do |builder|
871
+ builder.use Restorm::Middleware::FirstLevelParseJSON
872
+ builder.use Faraday::Request::UrlEncoded
873
+ builder.adapter :test do |stub|
874
+ stub.get("/users/10") { [200, {}, { id: 10 }.to_json] }
875
+ 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] }
876
+ end
877
+ end
878
+
879
+ Foo::User.use_api Restorm::API.default_api
880
+ Foo::Comment.use_api Restorm::API.default_api
881
+ end
882
+
883
+ it "takes the parent primary key and saves the resource" do
884
+ expect(comment.id).to eq(1)
885
+ expect(comment.body).to eq("Hello!")
886
+ expect(comment.user_id).to eq(10)
887
+ expect(user.comments).to eq([comment])
888
+ end
889
+ end
890
+
891
+ context "with #new" do
892
+ let(:user) { Foo::User.new(name: "vic", comments: [comment]) }
893
+
894
+ context "using hash attributes" do
895
+ let(:comment) { { text: "hello" } }
896
+
897
+ it "assigns nested models" do
898
+ expect(user.comments.first.text).to eq("hello")
899
+ end
900
+ end
901
+
902
+ context "using constructed objects" do
903
+ let(:comment) { Foo::Comment.new(text: "goodbye") }
904
+
905
+ it "assigns nested models" do
906
+ expect(user.comments.first.text).to eq("goodbye")
907
+ end
908
+ end
909
+ end
910
+ end
911
+ end