him 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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. metadata +201 -0
@@ -0,0 +1,1010 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Him::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_many :feeds, class_name: "Foo::Feed"
283
+ has_one :role, class_name: "Foo::Role"
284
+ belongs_to :organization, class_name: "Foo::Organization"
285
+ has_many :posts, inverse_of: :admin
286
+ end
287
+
288
+ spawn_model "Foo::Comment" do
289
+ belongs_to :user
290
+ parse_root_in_json true
291
+ end
292
+
293
+ spawn_model "Foo::Feed" do
294
+ belongs_to :user
295
+ end
296
+
297
+ spawn_model "Foo::Post" do
298
+ belongs_to :admin, class_name: "Foo::User"
299
+ end
300
+
301
+ spawn_model "Foo::Organization" do
302
+ parse_root_in_json true
303
+ end
304
+
305
+ spawn_model "Foo::Role"
306
+ end
307
+
308
+ context "with included data" do
309
+ before(:context) do
310
+ Him::API.setup url: "https://api.example.com" do |builder|
311
+ builder.use Him::Middleware::FirstLevelParseJSON
312
+ builder.use Faraday::Request::UrlEncoded
313
+ builder.adapter :test do |stub|
314
+ 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] }
315
+ stub.get("/users/1/comments") { [200, {}, [{ comment: { id: 4, body: "They're having a FIRESALE?" } }].to_json] }
316
+ stub.get("/users/1/feeds") { [204, {}, ""] }
317
+ stub.get("/users/1/role") { [200, {}, { id: 3, body: "User" }.to_json] }
318
+ stub.get("/users/1/posts") { [200, {}, [{ id: 1, body: "blogging stuff", admin_id: 1 }].to_json] }
319
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
320
+ end
321
+ end
322
+ end
323
+
324
+ let(:user) { Foo::User.find(1) }
325
+ let(:user_params) { user.to_params }
326
+
327
+ it "maps an array of included data through has_many" do
328
+ expect(user.comments.first).to be_a(Foo::Comment)
329
+ expect(user.comments.length).to eq(2)
330
+ expect(user.comments.first.id).to eq(2)
331
+ expect(user.comments.first.body).to eq("Tobias, you blow hard!")
332
+ end
333
+
334
+ it "handles a 204 empty response on has_many" do
335
+ expect(user.feeds).to eq([])
336
+ expect(user.feeds.first).to eq(nil)
337
+ expect(user.feeds.length).to eq(0)
338
+ end
339
+
340
+ it "does not refetch the parents models data if they have been fetched before" do
341
+ expect(user.comments.first.user.object_id).to eq(user.object_id)
342
+ end
343
+
344
+ it "uses the given inverse_of key to set the parent model" do
345
+ expect(user.posts.first.admin.object_id).to eq(user.object_id)
346
+ end
347
+
348
+ it "does not set the inverse of a has_many as an attribute" do
349
+ first_comment = user.comments.where(foo_id: 1).first
350
+ expect(first_comment.attributes.keys).not_to include("user")
351
+ end
352
+
353
+ it "fetches has_many data even if it was included, only if called with parameters" do
354
+ expect(user.comments.where(foo_id: 1).length).to eq(1)
355
+ end
356
+
357
+ it "maps an array of included data through has_one" do
358
+ expect(user.role).to be_a(Foo::Role)
359
+ expect(user.role.object_id).to eq(user.role.object_id)
360
+ expect(user.role.id).to eq(1)
361
+ expect(user.role.body).to eq("Admin")
362
+ end
363
+
364
+ it "fetches has_one data even if it was included, only if called with parameters" do
365
+ expect(user.role.where(foo_id: 2).id).to eq(3)
366
+ end
367
+
368
+ it "maps an array of included data through belongs_to" do
369
+ expect(user.organization).to be_a(Foo::Organization)
370
+ expect(user.organization.id).to eq(1)
371
+ expect(user.organization.name).to eq("Bluth Company")
372
+ end
373
+
374
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
375
+ expect(user.organization.where(foo_id: 1).name).to eq("Bluth Company Foo")
376
+ end
377
+
378
+ it "includes has_many relationships in params by default" do
379
+ expect(user_params[:comments]).to be_kind_of(Array)
380
+ expect(user_params[:comments].length).to eq(2)
381
+ end
382
+
383
+ it "includes has_one relationship in params by default" do
384
+ expect(user_params[:role]).to be_kind_of(Hash)
385
+ expect(user_params[:role]).not_to be_empty
386
+ end
387
+
388
+ it "includes belongs_to relationship in params by default" do
389
+ expect(user_params[:organization]).to be_kind_of(Hash)
390
+ expect(user_params[:organization]).not_to be_empty
391
+ end
392
+ end
393
+
394
+ context "without included data" do
395
+ before(:context) do
396
+ Him::API.setup url: "https://api.example.com" do |builder|
397
+ builder.use Him::Middleware::FirstLevelParseJSON
398
+ builder.use Faraday::Request::UrlEncoded
399
+ builder.adapter :test do |stub|
400
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
401
+ 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] }
402
+ stub.get("/users/2/comments/5") { [200, {}, { comment: { id: 5, body: "Is this the tiny town from Footloose?" } }.to_json] }
403
+ stub.get("/users/2/role") { [200, {}, { id: 2, body: "User" }.to_json] }
404
+ stub.get("/organizations/2") do |env|
405
+ if env[:params]["admin"] == "true"
406
+ [200, {}, { organization: { id: 2, name: "Bluth Company (admin)" } }.to_json]
407
+ else
408
+ [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json]
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
414
+
415
+ let(:user) { Foo::User.find(2) }
416
+
417
+ it "fetches data that was not included through has_many" do
418
+ expect(user.comments.first).to be_a(Foo::Comment)
419
+ expect(user.comments.length).to eq(2)
420
+ expect(user.comments.first.id).to eq(4)
421
+ expect(user.comments.first.body).to eq("They're having a FIRESALE?")
422
+ end
423
+
424
+ it "fetches data that was not included through has_many only once" do
425
+ expect(user.comments.first.object_id).to eq(user.comments.first.object_id)
426
+ end
427
+
428
+ it "fetches data that was cached through has_many if called with parameters" do
429
+ expect(user.comments.first.object_id).not_to eq(user.comments.where(foo_id: 1).first.object_id)
430
+ end
431
+
432
+ it "fetches data again after being reloaded" do
433
+ expect { user.comments.reload }.to change { user.comments.first.object_id }
434
+ end
435
+
436
+ it "fetches data that was not included through has_one" do
437
+ expect(user.role).to be_a(Foo::Role)
438
+ expect(user.role.id).to eq(2)
439
+ expect(user.role.body).to eq("User")
440
+ end
441
+
442
+ it "fetches data that was not included through belongs_to" do
443
+ expect(user.organization).to be_a(Foo::Organization)
444
+ expect(user.organization.id).to eq(2)
445
+ expect(user.organization.name).to eq("Bluth Company")
446
+ end
447
+
448
+ it "can tell if it has a association" do
449
+ expect(user.has_association?(:unknown_association)).to be false
450
+ expect(user.has_association?(:organization)).to be true
451
+ end
452
+
453
+ it "fetches the resource corresponding to a named association" do
454
+ expect(user.get_association(:unknown_association)).to be_nil
455
+ expect(user.get_association(:organization).name).to eq("Bluth Company")
456
+ end
457
+
458
+ it "pass query string parameters when additional arguments are passed" do
459
+ expect(user.organization.where(admin: true).name).to eq("Bluth Company (admin)")
460
+ expect(user.organization.name).to eq("Bluth Company")
461
+ end
462
+
463
+ it "fetches data with the specified id when calling find" do
464
+ comment = user.comments.find(5)
465
+ expect(comment).to be_a(Foo::Comment)
466
+ expect(comment.id).to eq(5)
467
+ end
468
+
469
+ it "'s associations responds to #empty?" do
470
+ expect(user.organization.respond_to?(:empty?)).to be_truthy
471
+ expect(user.organization).not_to be_empty
472
+ end
473
+ end
474
+
475
+ context "without included parent data" do
476
+ before(:context) do
477
+ Him::API.setup url: "https://api.example.com" do |builder|
478
+ builder.use Him::Middleware::FirstLevelParseJSON
479
+ builder.use Faraday::Request::UrlEncoded
480
+ builder.adapter :test do |stub|
481
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
482
+ end
483
+ end
484
+ end
485
+
486
+ let(:comment) { Foo::Comment.new(id: 7, user_id: 1) }
487
+
488
+ it "does fetch the parent models data only once" do
489
+ expect(comment.user.object_id).to eq(comment.user.object_id)
490
+ end
491
+
492
+ it "does fetch the parent models data that was cached if called with parameters" do
493
+ expect(comment.user.object_id).not_to eq(comment.user.where(a: 2).object_id)
494
+ end
495
+ end
496
+
497
+ context "when resource is new" do
498
+ let(:new_user) { Foo::User.new }
499
+
500
+ it "doesn't attempt to fetch association data" do
501
+ expect(new_user.comments).to eq([])
502
+ expect(new_user.role).to be_nil
503
+ expect(new_user.organization).to be_nil
504
+ end
505
+
506
+ it "reports nil associations as blank" do
507
+ expect(new_user.role.blank?).to be true
508
+ expect(new_user.organization.blank?).to be true
509
+ end
510
+ end
511
+
512
+ context "when foreign_key is nil" do
513
+ before do
514
+ spawn_model "Foo::User" do
515
+ belongs_to :organization, class_name: "Foo::Organization"
516
+ end
517
+
518
+ spawn_model "Foo::Organization" do
519
+ parse_root_in_json true
520
+ end
521
+ end
522
+
523
+ let(:user) { Foo::User.new(organization_id: nil, name: "Katlin Fünke") }
524
+
525
+ it "returns nil" do
526
+ expect(user.organization).to be_nil
527
+ end
528
+ end
529
+
530
+ context "after" do
531
+ before(:context) do
532
+ Him::API.setup url: "https://api.example.com" do |builder|
533
+ builder.use Him::Middleware::FirstLevelParseJSON
534
+ builder.use Faraday::Request::UrlEncoded
535
+ builder.adapter :test do |stub|
536
+ 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] }
537
+ 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] }
538
+ 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] }
539
+ end
540
+ end
541
+ end
542
+
543
+ let(:user_after_create) { Foo::User.create }
544
+ let(:user_after_save_existing) { Foo::User.save_existing(5, name: "Clancy Brown") }
545
+ let(:user_after_destroy) { Foo::User.new(id: 5).destroy }
546
+
547
+ [:create, :save_existing, :destroy].each do |type|
548
+ context "after #{type}" do
549
+ let(:subject) { send("user_after_#{type}") }
550
+
551
+ it "maps an array of included data through has_many" do
552
+ expect(subject.comments.first).to be_a(Foo::Comment)
553
+ expect(subject.comments.length).to eq(1)
554
+ expect(subject.comments.first.id).to eq(99)
555
+ expect(subject.comments.first.body).to eq("Rodríguez, nasibisibusi?")
556
+ end
557
+
558
+ it "maps an array of included data through has_one" do
559
+ expect(subject.role).to be_a(Foo::Role)
560
+ expect(subject.role.id).to eq(1)
561
+ expect(subject.role.body).to eq("Admin")
562
+ end
563
+ end
564
+ end
565
+ end
566
+ end
567
+
568
+ context "handling associations with collection_path" do
569
+ before do
570
+ spawn_model "Foo::Organization" do
571
+ has_many :users
572
+ parse_root_in_json true
573
+ collection_path '/special/organizations'
574
+ end
575
+ spawn_model "Foo::User" do
576
+ belongs_to :organization
577
+ end
578
+ end
579
+
580
+ context "without included data" do
581
+ before(:context) do
582
+ Him::API.setup url: "https://api.example.com" do |builder|
583
+ builder.use Him::Middleware::FirstLevelParseJSON
584
+ builder.use Faraday::Request::UrlEncoded
585
+ builder.adapter :test do |stub|
586
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
587
+ stub.get("/special/organizations/2") { [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json] }
588
+ end
589
+ end
590
+ end
591
+
592
+ let(:user) { Foo::User.find(2) }
593
+
594
+ it "fetches data that was not included through belongs_to" do
595
+ expect(user.organization).to be_a(Foo::Organization)
596
+ expect(user.organization.id).to eq(2)
597
+ expect(user.organization.name).to eq("Bluth Company")
598
+ end
599
+ end
600
+ end
601
+
602
+ context "handling associations with path_prefix" do
603
+ before do
604
+ spawn_model "Foo::Organization" do
605
+ has_many :users
606
+ parse_root_in_json true
607
+ end
608
+ spawn_model "Foo::User" do
609
+ belongs_to :organization
610
+ end
611
+ end
612
+
613
+ context "without included data" do
614
+ before(:context) do
615
+ Him::API.setup url: "https://api.example.com" do |builder|
616
+ builder.use Him::Middleware::FirstLevelParseJSON
617
+ builder.use Faraday::Request::UrlEncoded
618
+ builder.path_prefix = 'special'
619
+ builder.adapter :test do |stub|
620
+ stub.get("/special/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 2 }.to_json] }
621
+ stub.get("/special/organizations/2") { [200, {}, { organization: { id: 2, name: "Bluth Company" } }.to_json] }
622
+ end
623
+ end
624
+ end
625
+
626
+ let(:user) { Foo::User.find(2) }
627
+
628
+ it "fetches data that was not included through belongs_to" do
629
+ expect(user.organization).to be_a(Foo::Organization)
630
+ expect(user.organization.id).to eq(2)
631
+ expect(user.organization.name).to eq("Bluth Company")
632
+ end
633
+ end
634
+ end
635
+
636
+ context "handling associations with details in active_model_serializers format" do
637
+ before do
638
+ spawn_model "Foo::User" do
639
+ parse_root_in_json true, format: :active_model_serializers
640
+ has_many :comments, class_name: "Foo::Comment"
641
+ has_one :role, class_name: "Foo::Role"
642
+ belongs_to :organization, class_name: "Foo::Organization"
643
+ end
644
+
645
+ spawn_model "Foo::Role" do
646
+ belongs_to :user
647
+ parse_root_in_json true, format: :active_model_serializers
648
+ end
649
+
650
+ spawn_model "Foo::Comment" do
651
+ belongs_to :user
652
+ parse_root_in_json true, format: :active_model_serializers
653
+ end
654
+
655
+ spawn_model "Foo::Organization" do
656
+ parse_root_in_json true, format: :active_model_serializers
657
+ end
658
+ end
659
+
660
+ context "with included data" do
661
+ before(:context) do
662
+ Him::API.setup url: "https://api.example.com" do |builder|
663
+ builder.use Him::Middleware::FirstLevelParseJSON
664
+ builder.use Faraday::Request::UrlEncoded
665
+ builder.adapter :test do |stub|
666
+ 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] }
667
+ stub.get("/users/1/comments") { [200, {}, { comments: [{ id: 4, body: "They're having a FIRESALE?" }] }.to_json] }
668
+ stub.get("/users/1/role") { [200, {}, { role: { id: 3, body: "User" } }.to_json] }
669
+ stub.get("/users/2") { [200, {}, { user: { id: 2, name: "Lindsay Fünke", organization_id: 1 } }.to_json] }
670
+ stub.get("/users/2/role") { [200, {}, { role: { id: 4, body: "Viewer" } }.to_json] }
671
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
672
+ end
673
+ end
674
+ end
675
+
676
+ let(:user) { Foo::User.find(1) }
677
+ let(:user_params) { user.to_params }
678
+
679
+ it "maps an array of included data through has_many" do
680
+ expect(user.comments.first).to be_a(Foo::Comment)
681
+ expect(user.comments.length).to eq(2)
682
+ expect(user.comments.first.id).to eq(2)
683
+ expect(user.comments.first.body).to eq("Tobias, you blow hard!")
684
+ end
685
+
686
+ it "does not refetch the parents models data if they have been fetched before" do
687
+ expect(user.comments.first.user.object_id).to eq(user.object_id)
688
+ end
689
+
690
+ it "fetches has_many data even if it was included, only if called with parameters" do
691
+ expect(user.comments.where(foo_id: 1).length).to eq(1)
692
+ end
693
+
694
+ it "maps an array of included data through belongs_to" do
695
+ expect(user.organization).to be_a(Foo::Organization)
696
+ expect(user.organization.id).to eq(1)
697
+ expect(user.organization.name).to eq("Bluth Company")
698
+ end
699
+
700
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
701
+ expect(user.organization.where(foo_id: 1).name).to eq("Bluth Company Foo")
702
+ end
703
+
704
+ it "maps included data through has_one with AMS format" do
705
+ expect(user.role).to be_a(Foo::Role)
706
+ expect(user.role.id).to eq(1)
707
+ expect(user.role.body).to eq("Admin")
708
+ end
709
+
710
+ it "fetches has_one data not included with AMS format" do
711
+ user2 = Foo::User.find(2)
712
+ expect(user2.role).to be_a(Foo::Role)
713
+ expect(user2.role.id).to eq(4)
714
+ expect(user2.role.body).to eq("Viewer")
715
+ end
716
+
717
+ it "includes has_many relationships in params by default" do
718
+ expect(user_params[:comments]).to be_kind_of(Array)
719
+ expect(user_params[:comments].length).to eq(2)
720
+ end
721
+
722
+ it "includes has_one relationships in params by default" do
723
+ expect(user_params[:role]).to be_kind_of(Hash)
724
+ expect(user_params[:role]).not_to be_empty
725
+ end
726
+
727
+ it "includes belongs_to relationship in params by default" do
728
+ expect(user_params[:organization]).to be_kind_of(Hash)
729
+ expect(user_params[:organization]).not_to be_empty
730
+ end
731
+ end
732
+
733
+ context "without included data" do
734
+ before(:context) do
735
+ Him::API.setup url: "https://api.example.com" do |builder|
736
+ builder.use Him::Middleware::FirstLevelParseJSON
737
+ builder.use Faraday::Request::UrlEncoded
738
+ builder.adapter :test do |stub|
739
+ stub.get("/users/2") { [200, {}, { user: { id: 2, name: "Lindsay Fünke", organization_id: 1 } }.to_json] }
740
+ 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] }
741
+ stub.get("/users/2/comments/5") { [200, {}, { comment: { id: 5, body: "Is this the tiny town from Footloose?" } }.to_json] }
742
+ stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
743
+ end
744
+ end
745
+ end
746
+
747
+ let(:user) { Foo::User.find(2) }
748
+
749
+ it "fetches data that was not included through has_many" do
750
+ expect(user.comments.first).to be_a(Foo::Comment)
751
+ expect(user.comments.length).to eq(2)
752
+ expect(user.comments.first.id).to eq(4)
753
+ expect(user.comments.first.body).to eq("They're having a FIRESALE?")
754
+ end
755
+
756
+ it "fetches data that was not included through belongs_to" do
757
+ expect(user.organization).to be_a(Foo::Organization)
758
+ expect(user.organization.id).to eq(1)
759
+ expect(user.organization.name).to eq("Bluth Company Foo")
760
+ end
761
+
762
+ it "fetches data with the specified id when calling find" do
763
+ comment = user.comments.find(5)
764
+ expect(comment).to be_a(Foo::Comment)
765
+ expect(comment.id).to eq(5)
766
+ end
767
+ end
768
+ end
769
+
770
+ context "handling associations with details" do
771
+ before do
772
+ spawn_model "Foo::User" do
773
+ belongs_to :company, path: "/organizations/:id", foreign_key: :organization_id, data_key: :organization
774
+ end
775
+
776
+ spawn_model "Foo::Company"
777
+ end
778
+
779
+ context "with included data" do
780
+ before(:context) do
781
+ Him::API.setup url: "https://api.example.com" do |builder|
782
+ builder.use Him::Middleware::FirstLevelParseJSON
783
+ builder.use Faraday::Request::UrlEncoded
784
+ builder.adapter :test do |stub|
785
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Tobias Fünke", organization: { id: 1, name: "Bluth Company Inc." }, organization_id: 1 }.to_json] }
786
+ stub.get("/users/4") { [200, {}, { id: 1, name: "Tobias Fünke", organization: { id: 1, name: "Bluth Company Inc." } }.to_json] }
787
+ stub.get("/users/3") { [200, {}, { id: 2, name: "Lindsay Fünke", organization: nil }.to_json] }
788
+ end
789
+ end
790
+ end
791
+
792
+ let(:user) { Foo::User.find(1) }
793
+
794
+ it "maps an array of included data through belongs_to" do
795
+ expect(user.company).to be_a(Foo::Company)
796
+ expect(user.company.id).to eq(1)
797
+ expect(user.company.name).to eq("Bluth Company Inc.")
798
+ end
799
+
800
+ context "when included data is nil" do
801
+ let(:user) { Foo::User.find(3) }
802
+
803
+ it "does not map included data" do
804
+ expect(user.company).to be_nil
805
+ end
806
+ end
807
+
808
+ context "when included data has no foreign_key" do
809
+ let(:user) { Foo::User.find(4) }
810
+
811
+ it "maps included data anyway" do
812
+ expect(user.company.name).to eq("Bluth Company Inc.")
813
+ end
814
+ end
815
+ end
816
+
817
+ context "without included data" do
818
+ before(:context) do
819
+ Him::API.setup url: "https://api.example.com" do |builder|
820
+ builder.use Him::Middleware::FirstLevelParseJSON
821
+ builder.use Faraday::Request::UrlEncoded
822
+ builder.adapter :test do |stub|
823
+ stub.get("/users/2") { [200, {}, { id: 2, name: "Lindsay Fünke", organization_id: 1 }.to_json] }
824
+ stub.get("/organizations/1") { [200, {}, { id: 1, name: "Bluth Company" }.to_json] }
825
+ end
826
+ end
827
+ end
828
+
829
+ let(:user) { Foo::User.find(2) }
830
+
831
+ it "fetches data that was not included through belongs_to" do
832
+ expect(user.company).to be_a(Foo::Company)
833
+ expect(user.company.id).to eq(1)
834
+ expect(user.company.name).to eq("Bluth Company")
835
+ end
836
+ end
837
+ end
838
+
839
+ context "object returned by the association method" do
840
+ before do
841
+ spawn_model "Foo::Role" do
842
+ def present?
843
+ "of_course"
844
+ end
845
+ end
846
+ spawn_model "Foo::User" do
847
+ has_one :role
848
+ end
849
+ end
850
+
851
+ let(:associated_value) { Foo::Role.new }
852
+ let(:user_with_role) do
853
+ Foo::User.new.tap { |user| user.role = associated_value }
854
+ end
855
+
856
+ subject { user_with_role.role }
857
+
858
+ it "doesnt mask the object's basic methods" do
859
+ expect(subject.class).to eq(Foo::Role)
860
+ end
861
+
862
+ it "doesnt mask core methods like extend" do
863
+ committer = Module.new
864
+ subject.extend committer
865
+ expect(associated_value).to be_kind_of committer
866
+ end
867
+
868
+ it "can return the association object" do
869
+ expect(subject.association).to be_kind_of Him::Model::Associations::Association
870
+ end
871
+
872
+ it "still can call fetch via the association" do
873
+ expect(subject.association.fetch).to eq associated_value
874
+ end
875
+
876
+ it "calls missing methods on associated value" do
877
+ expect(subject.present?).to eq("of_course")
878
+ end
879
+
880
+ it "can use association methods like where" do
881
+ expect(subject.where(role: "committer").association
882
+ .params).to include :role
883
+ end
884
+ end
885
+
886
+ context "building and creating association data" do
887
+ before do
888
+ spawn_model "Foo::Comment"
889
+ spawn_model "Foo::User" do
890
+ has_many :comments
891
+ end
892
+ end
893
+
894
+ context "with #build" do
895
+ let(:comment) { Foo::User.new(id: 10).comments.build(body: "Hello!") }
896
+
897
+ it "takes the parent primary key" do
898
+ expect(comment.body).to eq("Hello!")
899
+ expect(comment.user_id).to eq(10)
900
+ end
901
+ end
902
+
903
+ context "with #build using custom foreign_key" do
904
+ before do
905
+ spawn_model "Foo::Article"
906
+ spawn_model "Foo::User" do
907
+ has_many :articles, foreign_key: :creator_id
908
+ end
909
+ end
910
+
911
+ it "uses the declared foreign_key instead of convention" do
912
+ article = Foo::User.new(id: 5).articles.build(title: "Hello")
913
+ expect(article.creator_id).to eq(5)
914
+ end
915
+ end
916
+
917
+ context "with #create" do
918
+ let(:user) { Foo::User.find(10) }
919
+ let(:comment) { user.comments.create(body: "Hello!") }
920
+
921
+ before do
922
+ Him::API.setup url: "https://api.example.com" do |builder|
923
+ builder.use Him::Middleware::FirstLevelParseJSON
924
+ builder.use Faraday::Request::UrlEncoded
925
+ builder.adapter :test do |stub|
926
+ stub.get("/users/10") { [200, {}, { id: 10 }.to_json] }
927
+ 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] }
928
+ end
929
+ end
930
+
931
+ Foo::User.use_api Him::API.default_api
932
+ Foo::Comment.use_api Him::API.default_api
933
+ end
934
+
935
+ it "takes the parent primary key and saves the resource" do
936
+ expect(comment.id).to eq(1)
937
+ expect(comment.body).to eq("Hello!")
938
+ expect(comment.user_id).to eq(10)
939
+ expect(user.comments).to eq([comment])
940
+ end
941
+ end
942
+
943
+ context "with #build appending to existing collection" do
944
+ before do
945
+ Him::API.setup url: "https://api.example.com" do |builder|
946
+ builder.use Him::Middleware::FirstLevelParseJSON
947
+ builder.use Faraday::Request::UrlEncoded
948
+ builder.adapter :test do |stub|
949
+ stub.get("/users/1") { [200, {}, { id: 1 }.to_json] }
950
+ stub.get("/users/1/comments") { [200, {}, [{ id: 1, body: "Existing", user_id: 1 }].to_json] }
951
+ end
952
+ end
953
+
954
+ Foo::User.use_api Him::API.default_api
955
+ Foo::Comment.use_api Him::API.default_api
956
+ end
957
+
958
+ it "appends built resource to the existing collection" do
959
+ user = Foo::User.find(1)
960
+ expect(user.comments.length).to eq(1)
961
+ user.comments.build(body: "New!")
962
+ expect(user.comments.length).to eq(2)
963
+ end
964
+ end
965
+
966
+ context "with #create when collection already has data" do
967
+ before do
968
+ Him::API.setup url: "https://api.example.com" do |builder|
969
+ builder.use Him::Middleware::FirstLevelParseJSON
970
+ builder.use Faraday::Request::UrlEncoded
971
+ builder.adapter :test do |stub|
972
+ stub.get("/users/10") { [200, {}, { id: 10 }.to_json] }
973
+ stub.get("/users/10/comments") { [200, {}, [{ id: 1, body: "Existing", user_id: 10 }].to_json] }
974
+ stub.post("/comments") { |env| [200, {}, { id: 2, body: Faraday::Utils.parse_query(env[:body])["body"], user_id: 10 }.to_json] }
975
+ end
976
+ end
977
+
978
+ Foo::User.use_api Him::API.default_api
979
+ Foo::Comment.use_api Him::API.default_api
980
+ end
981
+
982
+ it "appends to the existing collection" do
983
+ user = Foo::User.find(10)
984
+ expect(user.comments.length).to eq(1)
985
+ user.comments.create(body: "New!")
986
+ expect(user.comments.length).to eq(2)
987
+ end
988
+ end
989
+
990
+ context "with #new" do
991
+ let(:user) { Foo::User.new(name: "vic", comments: [comment]) }
992
+
993
+ context "using hash attributes" do
994
+ let(:comment) { { text: "hello" } }
995
+
996
+ it "assigns nested models" do
997
+ expect(user.comments.first.text).to eq("hello")
998
+ end
999
+ end
1000
+
1001
+ context "using constructed objects" do
1002
+ let(:comment) { Foo::Comment.new(text: "goodbye") }
1003
+
1004
+ it "assigns nested models" do
1005
+ expect(user.comments.first.text).to eq("goodbye")
1006
+ end
1007
+ end
1008
+ end
1009
+ end
1010
+ end