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,717 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Him::Model::ORM do
6
+ context "mapping data to Ruby objects" do
7
+ before do
8
+ api = Him::API.new
9
+ api.setup url: "https://api.example.com" do |builder|
10
+ builder.use Him::Middleware::FirstLevelParseJSON
11
+ builder.use Faraday::Request::UrlEncoded
12
+ builder.adapter :test do |stub|
13
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Tobias Fünke" }.to_json] }
14
+ stub.get("/users") { [200, {}, [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }].to_json] }
15
+ stub.get("/admin_users") { [200, {}, [{ admin_id: 1, name: "Tobias Fünke" }, { admin_id: 2, name: "Lindsay Fünke" }].to_json] }
16
+ stub.get("/admin_users/1") { [200, {}, { admin_id: 1, name: "Tobias Fünke" }.to_json] }
17
+ end
18
+ end
19
+
20
+ spawn_model "Foo::User" do
21
+ uses_api api
22
+ end
23
+
24
+ spawn_model "Foo::AdminUser" do
25
+ uses_api api
26
+ primary_key :admin_id
27
+ end
28
+ end
29
+
30
+ it "maps a single resource to a Ruby object" do
31
+ @user = Foo::User.find(1)
32
+ expect(@user.id).to eq(1)
33
+ expect(@user.name).to eq("Tobias Fünke")
34
+
35
+ @admin = Foo::AdminUser.find(1)
36
+ expect(@admin.id).to eq(1)
37
+ expect(@admin.name).to eq("Tobias Fünke")
38
+ end
39
+
40
+ it "maps a collection of resources to an array of Ruby objects" do
41
+ @users = Foo::User.all
42
+ expect(@users.length).to eq(2)
43
+ expect(@users.first.name).to eq("Tobias Fünke")
44
+
45
+ @users = Foo::AdminUser.all
46
+ expect(@users.length).to eq(2)
47
+ expect(@users.first.name).to eq("Tobias Fünke")
48
+ end
49
+
50
+ it "handles new resource" do
51
+ @new_user = Foo::User.new(fullname: "Tobias Fünke")
52
+ expect(@new_user.new?).to be_truthy
53
+ expect(@new_user.new_record?).to be_truthy
54
+ expect(@new_user.fullname).to eq("Tobias Fünke")
55
+
56
+ @existing_user = Foo::User.find(1)
57
+ expect(@existing_user.new?).to be_falsey
58
+ expect(@existing_user.new_record?).to be_falsey
59
+ end
60
+
61
+ it "treats new resource with pre-assigned id as new" do
62
+ @user = Foo::User.new(id: 5, fullname: "Tobias Fünke")
63
+ expect(@user.new?).to be_truthy
64
+ expect(@user.persisted?).to be_falsey
65
+ end
66
+
67
+ it "handles new resource with custom primary key" do
68
+ @new_user = Foo::AdminUser.new(fullname: "Lindsay Fünke", id: -1)
69
+ expect(@new_user).to be_new
70
+
71
+ @existing_user = Foo::AdminUser.find(1)
72
+ expect(@existing_user).not_to be_new
73
+ end
74
+
75
+ end
76
+
77
+ context "mapping data, metadata and error data to Ruby objects" do
78
+ before do
79
+ api = Him::API.new
80
+ api.setup url: "https://api.example.com" do |builder|
81
+ builder.use Him::Middleware::SecondLevelParseJSON
82
+ builder.use Faraday::Request::UrlEncoded
83
+ builder.adapter :test do |stub|
84
+ stub.get("/users") { [200, {}, { data: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }], metadata: { total_pages: 10, next_page: 2 }, errors: %w[Oh My God] }.to_json] }
85
+ stub.get("/users") { [200, {}, { :data => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], :metadata => { :total_pages => 10, :next_page => 2 }, :errors => ["Oh", "My", "God"] }.to_json] }
86
+ stub.post("/users") { [200, {}, { :data => { :name => "George Michael Bluth" }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
87
+ stub.delete("/users/1") { [200, {}, { :data => { :id => 1 }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
88
+ end
89
+ end
90
+
91
+ spawn_model :User do
92
+ uses_api api
93
+ end
94
+ end
95
+
96
+ it "handles metadata on a collection" do
97
+ @users = User.all
98
+ expect(@users.metadata[:total_pages]).to eq(10)
99
+ end
100
+
101
+ it "handles error data on a collection" do
102
+ @users = User.all
103
+ expect(@users.errors.length).to eq(3)
104
+ end
105
+
106
+ it "handles metadata on a resource" do
107
+ @user = User.create(name: "George Michael Bluth")
108
+ expect(@user.metadata[:foo]).to eq("bar")
109
+ end
110
+
111
+ it "handles error data on a resource" do
112
+ @user = User.create(name: "George Michael Bluth")
113
+ expect(@user.response_errors).to eq(%w[Yes Sir])
114
+ end
115
+
116
+ it "handles metadata on a destroyed resource" do
117
+ @user = User.destroy_existing(1)
118
+ expect(@user.metadata[:foo]).to eq("bar")
119
+ end
120
+
121
+ it "handles error data on a destroyed resource" do
122
+ @user = User.destroy_existing(1)
123
+ expect(@user.response_errors).to eq(%w[Yes Sir])
124
+ end
125
+ end
126
+
127
+ context "mapping data, metadata and error data in string keys to Ruby objects" do
128
+ before do
129
+ api = Him::API.new
130
+ api.setup url: "https://api.example.com" do |builder|
131
+ builder.use Him::Middleware::SecondLevelParseJSON
132
+ builder.use Faraday::Request::UrlEncoded
133
+ builder.adapter :test do |stub|
134
+ stub.get("/users") { [200, {}, { data: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }], metadata: { total_pages: 10, next_page: 2 }, errors: %w[Oh My God] }.to_json] }
135
+ stub.post("/users") { [200, {}, { data: { name: "George Michael Bluth" }, metadata: { foo: "bar" }, errors: %w[Yes Sir] }.to_json] }
136
+ end
137
+ end
138
+
139
+ spawn_model :User do
140
+ uses_api api
141
+ end
142
+ end
143
+
144
+ it "handles metadata on a collection" do
145
+ @users = User.all
146
+ expect(@users.metadata[:total_pages]).to eq(10)
147
+ end
148
+
149
+ it "handles error data on a collection" do
150
+ @users = User.all
151
+ expect(@users.errors.length).to eq(3)
152
+ end
153
+
154
+ it "handles metadata on a resource" do
155
+ @user = User.create(name: "George Michael Bluth")
156
+ expect(@user.metadata[:foo]).to eq("bar")
157
+ end
158
+
159
+ it "handles error data on a resource" do
160
+ @user = User.create(name: "George Michael Bluth")
161
+ expect(@user.response_errors).to eq(%w[Yes Sir])
162
+ end
163
+ end
164
+
165
+ context "defining custom getters and setters" do
166
+ before do
167
+ api = Him::API.new
168
+ api.setup url: "https://api.example.com" do |builder|
169
+ builder.use Him::Middleware::FirstLevelParseJSON
170
+ builder.use Faraday::Request::UrlEncoded
171
+ builder.adapter :test do |stub|
172
+ stub.get("/users/1") { [200, {}, { id: 1, friends: %w[Maeby GOB Anne] }.to_json] }
173
+ stub.get("/users/2") { [200, {}, { id: 1 }.to_json] }
174
+ end
175
+ end
176
+
177
+ spawn_model :User do
178
+ uses_api api
179
+ belongs_to :organization
180
+
181
+ def friends=(val)
182
+ val = val.delete("\r").split("\n").map { |friend| friend.gsub(/^\s*\*\s*/, "") } if val && val.is_a?(String)
183
+ @_her_attributes[:friends] = val
184
+ end
185
+
186
+ def friends
187
+ @_her_attributes[:friends].map { |friend| "* #{friend}" }.join("\n")
188
+ end
189
+ end
190
+ end
191
+
192
+ it "handles custom setters" do
193
+ @user = User.find(1)
194
+ expect(@user.friends).to eq("* Maeby\n* GOB\n* Anne")
195
+ @user.instance_eval do
196
+ @_her_attributes[:friends] = %w[Maeby GOB Anne]
197
+ end
198
+ end
199
+
200
+ it "handles custom getters" do
201
+ @user = User.new
202
+ @user.friends = "* George\n* Oscar\n* Lucille"
203
+ expect(@user.friends).to eq("* George\n* Oscar\n* Lucille")
204
+ @user.instance_eval do
205
+ @_her_attributes[:friends] = %w[George Oscar Lucille]
206
+ end
207
+ end
208
+ end
209
+
210
+ context "finding resources" do
211
+ before do
212
+ api = Him::API.new
213
+ api.setup url: "https://api.example.com" do |builder|
214
+ builder.use Him::Middleware::FirstLevelParseJSON
215
+ builder.use Faraday::Request::UrlEncoded
216
+ builder.adapter :test do |stub|
217
+ stub.get("/users/1") { [200, {}, { id: 1, age: 42 }.to_json] }
218
+ stub.get("/users/2") { [200, {}, { id: 2, age: 34 }.to_json] }
219
+ stub.get("/users?id[]=1&id[]=2") { [200, {}, [{ id: 1, age: 42 }, { id: 2, age: 34 }].to_json] }
220
+ stub.get("/users?age=42&foo=bar") { [200, {}, [{ id: 3, age: 42 }].to_json] }
221
+ stub.get("/users?age=42") { [200, {}, [{ id: 1, age: 42 }].to_json] }
222
+ stub.get("/users?age=40") { [200, {}, [{ id: 1, age: 40 }].to_json] }
223
+ stub.get("/users?name=baz") { [200, {}, [].to_json] }
224
+ stub.post("/users") { [200, {}, { id: 5, name: "baz" }.to_json] }
225
+ end
226
+ end
227
+
228
+ spawn_model :User do
229
+ uses_api api
230
+ end
231
+ end
232
+
233
+ it "handles finding by a single id" do
234
+ @user = User.find(1)
235
+ expect(@user.id).to eq(1)
236
+ end
237
+
238
+ it "handles finding by multiple ids" do
239
+ @users = User.find(1, 2)
240
+ expect(@users).to be_kind_of(Array)
241
+ expect(@users.length).to eq(2)
242
+ expect(@users[0].id).to eq(1)
243
+ expect(@users[1].id).to eq(2)
244
+ end
245
+
246
+ it "handles finding by an array of ids" do
247
+ @users = User.find([1, 2])
248
+ expect(@users).to be_kind_of(Array)
249
+ expect(@users.length).to eq(2)
250
+ expect(@users[0].id).to eq(1)
251
+ expect(@users[1].id).to eq(2)
252
+ end
253
+
254
+ it "handles finding by an array of ids of length 1" do
255
+ @users = User.find([1])
256
+ expect(@users).to be_kind_of(Array)
257
+ expect(@users.length).to eq(1)
258
+ expect(@users[0].id).to eq(1)
259
+ end
260
+
261
+ it "handles finding by an array id param of length 2" do
262
+ @users = User.find(id: [1, 2])
263
+ expect(@users).to be_kind_of(Array)
264
+ expect(@users.length).to eq(2)
265
+ expect(@users[0].id).to eq(1)
266
+ expect(@users[1].id).to eq(2)
267
+ end
268
+
269
+ it "handles finding with id parameter as an array" do
270
+ @users = User.where(id: [1, 2])
271
+ expect(@users).to be_kind_of(Array)
272
+ expect(@users.length).to eq(2)
273
+ expect(@users[0].id).to eq(1)
274
+ expect(@users[1].id).to eq(2)
275
+ end
276
+
277
+ it "handles finding by attributes" do
278
+ @user = User.find_by(age: 42)
279
+ expect(@user).to be_a(User)
280
+ expect(@user.id).to eq(1)
281
+ end
282
+
283
+ it "handles find or create by attributes" do
284
+ @user = User.find_or_create_by(name: "baz")
285
+ expect(@user).to be_a(User)
286
+ expect(@user.id).to eq(5)
287
+ end
288
+
289
+ it "handles find or initialize by attributes" do
290
+ @user = User.find_or_initialize_by(name: "baz")
291
+ expect(@user).to be_a(User)
292
+ expect(@user).to_not be_persisted
293
+ end
294
+
295
+ it "handles finding with other parameters" do
296
+ @users = User.where(age: 42, foo: "bar").all
297
+ expect(@users).to be_kind_of(Array)
298
+ expect(@users.first.id).to eq(3)
299
+ end
300
+
301
+ it "handles finding with other parameters and scoped" do
302
+ @users = User.scoped
303
+ expect(@users.where(age: 42)).to be_all { |u| u.age == 42 }
304
+ expect(@users.where(age: 40)).to be_all { |u| u.age == 40 }
305
+ end
306
+
307
+ it "handles reloading a resource" do
308
+ @user = User.find(1)
309
+ @user.age = "Oops"
310
+ @user.reload
311
+ expect(@user.age).to eq 42
312
+ expect(@user).to be_persisted
313
+ end
314
+ end
315
+
316
+ context "building resources" do
317
+ context "when request_new_object_on_build is not set (default)" do
318
+ before do
319
+ spawn_model("Foo::User")
320
+ end
321
+
322
+ it "builds a new resource without requesting it" do
323
+ expect(Foo::User).not_to receive(:request)
324
+ @new_user = Foo::User.build(fullname: "Tobias Fünke")
325
+ expect(@new_user.new?).to be_truthy
326
+ expect(@new_user.fullname).to eq("Tobias Fünke")
327
+ end
328
+ end
329
+
330
+ context "when request_new_object_on_build is set" do
331
+ before do
332
+ Him::API.setup url: "https://api.example.com" do |builder|
333
+ builder.use Him::Middleware::FirstLevelParseJSON
334
+ builder.use Faraday::Request::UrlEncoded
335
+ builder.adapter :test do |stub|
336
+ stub.get("/users/new") { |env| ok! id: nil, fullname: params(env)[:fullname], email: "tobias@bluthcompany.com" }
337
+ end
338
+ end
339
+
340
+ spawn_model("Foo::User") { request_new_object_on_build true }
341
+ end
342
+
343
+ it "requests a new resource" do
344
+ expect(Foo::User).to receive(:request).once.and_call_original
345
+ @new_user = Foo::User.build(fullname: "Tobias Fünke")
346
+ expect(@new_user.new?).to be_truthy
347
+ expect(@new_user.fullname).to eq("Tobias Fünke")
348
+ expect(@new_user.email).to eq("tobias@bluthcompany.com")
349
+ end
350
+ end
351
+ end
352
+
353
+ context "creating resources" do
354
+ before do
355
+ Him::API.setup url: "https://api.example.com" do |builder|
356
+ builder.use Him::Middleware::FirstLevelParseJSON
357
+ builder.use Faraday::Request::UrlEncoded
358
+ builder.adapter :test do |stub|
359
+ stub.post("/users") { |env| [200, {}, { id: 1, fullname: Faraday::Utils.parse_query(env[:body])["fullname"], email: Faraday::Utils.parse_query(env[:body])["email"] }.to_json] }
360
+ stub.post("/companies") { [200, {}, { errors: ["name is required"] }.to_json] }
361
+ end
362
+ end
363
+
364
+ spawn_model "Foo::User"
365
+ spawn_model "Foo::Company"
366
+ end
367
+
368
+ it "handle one-line resource creation" do
369
+ @user = Foo::User.create(fullname: "Tobias Fünke", email: "tobias@bluth.com")
370
+ expect(@user.id).to eq(1)
371
+ expect(@user.fullname).to eq("Tobias Fünke")
372
+ expect(@user.email).to eq("tobias@bluth.com")
373
+ end
374
+
375
+ it "handle resource creation through Model.new + #save" do
376
+ @user = Foo::User.new(fullname: "Tobias Fünke")
377
+ expect(@user.save).to be_truthy
378
+ expect(@user.fullname).to eq("Tobias Fünke")
379
+ end
380
+
381
+ it "handle resource creation through Model.new + #save!" do
382
+ @user = Foo::User.new(fullname: "Tobias Fünke")
383
+ expect(@user.save!).to be_truthy
384
+ expect(@user.fullname).to eq("Tobias Fünke")
385
+ end
386
+
387
+ it "returns false when #save gets errors" do
388
+ @company = Foo::Company.new
389
+ expect(@company.save).to be_falsey
390
+ end
391
+
392
+ it "raises ResourceInvalid when #save! gets errors" do
393
+ @company = Foo::Company.new
394
+ expect { @company.save! }.to raise_error Him::Errors::ResourceInvalid, "Remote validation failed: name is required"
395
+ end
396
+
397
+ it "don't overwrite data if response is empty" do
398
+ @company = Foo::Company.new(name: "Company Inc.")
399
+ expect(@company.save).to be_falsey
400
+ expect(@company.name).to eq("Company Inc.")
401
+ end
402
+
403
+ it "POSTs to collection path even with a pre-assigned id" do
404
+ @user = Foo::User.create(id: 1234, fullname: "Tobias Fünke")
405
+ expect(@user.fullname).to eq("Tobias Fünke")
406
+ expect(@user).to be_persisted
407
+ end
408
+ end
409
+
410
+ context "updating resources" do
411
+ before do
412
+ Him::API.setup url: "https://api.example.com" do |builder|
413
+ builder.use Him::Middleware::FirstLevelParseJSON
414
+ builder.use Faraday::Request::UrlEncoded
415
+ builder.adapter :test do |stub|
416
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke", admin: false }.to_json] }
417
+ stub.put("/users/1") { [200, {}, { id: 1, fullname: "Lindsay Fünke", admin: true }.to_json] }
418
+ stub.put("/users/2") { [200, {}, { id: 2, errors: ["fullname has already been taken"] }.to_json] }
419
+ stub.get("/pages/1") { [200, {}, { id: 1, views: 1, unique_visitors: 4 }.to_json] }
420
+ stub.put("/pages/1") { [200, {}, { id: 1, views: 2, unique_visitors: 3 }.to_json] }
421
+ end
422
+ end
423
+
424
+ spawn_model "Foo::User"
425
+ spawn_model "Foo::Page"
426
+ end
427
+
428
+ it "handle resource data update without saving it" do
429
+ @user = Foo::User.find(1)
430
+ expect(@user.fullname).to eq("Tobias Fünke")
431
+ @user.fullname = "Kittie Sanchez"
432
+ expect(@user.fullname).to eq("Kittie Sanchez")
433
+ end
434
+
435
+ it "handle resource update through the .update class method" do
436
+ @user = Foo::User.save_existing(1, fullname: "Lindsay Fünke")
437
+ expect(@user.fullname).to eq("Lindsay Fünke")
438
+ end
439
+
440
+ it "handle resource update through .save_existing!" do
441
+ @user = Foo::User.save_existing!(1, fullname: "Lindsay Fünke")
442
+ expect(@user.fullname).to eq("Lindsay Fünke")
443
+ end
444
+
445
+ it "raises ResourceInvalid when .save_existing! has errors" do
446
+ expect { Foo::User.save_existing!(2, fullname: "Lindsay Fünke") }.to raise_error(Him::Errors::ResourceInvalid)
447
+ end
448
+
449
+ it "handle resource update through #save on an existing resource" do
450
+ @user = Foo::User.find(1)
451
+ @user.fullname = "Lindsay Fünke"
452
+ @user.save
453
+ expect(@user.fullname).to eq("Lindsay Fünke")
454
+ end
455
+
456
+ it "handle resource update through #update_attributes" do
457
+ @user = Foo::User.find(1)
458
+ expect(@user).to receive(:save).and_return(true)
459
+ @user.update_attributes(fullname: "Lindsay Fünke")
460
+ expect(@user.fullname).to eq("Lindsay Fünke")
461
+ end
462
+
463
+ it "handles resource update through #toggle without saving it" do
464
+ @user = Foo::User.find(1)
465
+ expect(@user.admin).to be_falsey
466
+ expect(@user).to_not receive(:save)
467
+ @user.toggle(:admin)
468
+ expect(@user.admin).to be_truthy
469
+ end
470
+
471
+ it "handles resource update through #toggle!" do
472
+ @user = Foo::User.find(1)
473
+ expect(@user.admin).to be_falsey
474
+ expect(@user).to receive(:save).and_return(true)
475
+ @user.toggle!(:admin)
476
+ expect(@user.admin).to be_truthy
477
+ end
478
+
479
+ it "handles resource update through #increment without saving it" do
480
+ page = Foo::Page.find(1)
481
+ expect(page.views).to be 1
482
+ expect(page).to_not receive(:save)
483
+ page.increment(:views)
484
+ expect(page.views).to be 2
485
+ page.increment(:views, 2)
486
+ expect(page.views).to be 4
487
+ end
488
+
489
+ it "handles resource update through #increment!" do
490
+ page = Foo::Page.find(1)
491
+ expect(page.views).to be 1
492
+ expect(page).to receive(:save).and_return(true)
493
+ page.increment!(:views)
494
+ expect(page.views).to be 2
495
+ end
496
+
497
+ it "handles resource update through #decrement without saving it" do
498
+ page = Foo::Page.find(1)
499
+ expect(page.unique_visitors).to be 4
500
+ expect(page).to_not receive(:save)
501
+ page.decrement(:unique_visitors)
502
+ expect(page.unique_visitors).to be 3
503
+ page.decrement(:unique_visitors, 2)
504
+ expect(page.unique_visitors).to be 1
505
+ end
506
+
507
+ it "handles resource update through #decrement!" do
508
+ page = Foo::Page.find(1)
509
+ expect(page.unique_visitors).to be 4
510
+ expect(page).to receive(:save).and_return(true)
511
+ page.decrement!(:unique_visitors)
512
+ expect(page.unique_visitors).to be 3
513
+ end
514
+ end
515
+
516
+ context "deleting resources" do
517
+ let(:status) { 200 }
518
+ before do
519
+ Him::API.setup url: "https://api.example.com" do |builder|
520
+ builder.use Him::Middleware::FirstLevelParseJSON
521
+ builder.use Faraday::Request::UrlEncoded
522
+ builder.adapter :test do |stub|
523
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke", active: true }.to_json] }
524
+ stub.delete("/users/1") { [status, {}, { id: 1, fullname: "Lindsay Fünke", active: false }.to_json] }
525
+
526
+ stub.get("/child_users") { [200, {}, { data: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }], metadata: { total_pages: 10, next_page: 2 }, errors: %w[Oh My God] }.to_json] }
527
+ stub.get("/child_users") { [200, {}, { :data => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], :metadata => { :total_pages => 10, :next_page => 2 }, :errors => ["Oh", "My", "God"] }.to_json] }
528
+ stub.post("/child_users") { [200, {}, { :data => { :name => "George Michael Bluth" }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
529
+ stub.delete("/child_users/1") { [200, {}, { :data => { :id => 1 }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
530
+ end
531
+ end
532
+
533
+ spawn_model "Foo::User"
534
+ end
535
+
536
+ it "handles proper resource deletion on a child model class" do
537
+ child_user_klass = Class.new(Foo::User) do
538
+ def self.name
539
+ "ChildUser"
540
+ end
541
+ end
542
+
543
+ child_user_klass.destroy_existing(1)
544
+ @child_user = child_user_klass.create(name: "George Michael Bluth")
545
+
546
+ expect { @child_user.save! }.to raise_error(Him::Errors::ResourceInvalid)
547
+ end
548
+
549
+ it "handle resource deletion through the .destroy class method" do
550
+ @user = Foo::User.destroy_existing(1)
551
+ expect(@user.active).to be_falsey
552
+ expect(@user).to be_destroyed
553
+ end
554
+
555
+ it "handle resource deletion through #destroy on an existing resource" do
556
+ @user = Foo::User.find(1)
557
+ @user.destroy
558
+ expect(@user.active).to be_falsey
559
+ expect(@user).to be_destroyed
560
+ end
561
+
562
+ context "with response_errors" do
563
+ let(:status) { 422 }
564
+ it "set user.destroyed to false if errors are present through the .destroy class method" do
565
+ @user = Foo::User.destroy_existing(1)
566
+ expect(@user).not_to be_destroyed
567
+ end
568
+
569
+ it "set user.destroyed to false if errors are present through #destroy on an existing resource" do
570
+ @user = Foo::User.find(1)
571
+ @user.destroy
572
+ expect(@user).not_to be_destroyed
573
+ end
574
+ end
575
+
576
+ context "with params" do
577
+ before do
578
+ Him::API.setup url: "https://api.example.com" do |builder|
579
+ builder.use Him::Middleware::FirstLevelParseJSON
580
+ builder.use Faraday::Request::UrlEncoded
581
+ builder.adapter :test do |stub|
582
+ stub.delete("/users/1?delete_type=soft") { [200, {}, { id: 1, fullname: "Lindsay Fünke", active: false }.to_json] }
583
+ end
584
+ end
585
+ end
586
+
587
+ it "handle resource deletion through the .destroy class method" do
588
+ @user = Foo::User.destroy_existing(1, delete_type: "soft")
589
+ expect(@user.active).to be_falsey
590
+ expect(@user).to be_destroyed
591
+ end
592
+
593
+ it "handle resource deletion through #destroy on an existing resource" do
594
+ @user = Foo::User.find(1)
595
+ @user.destroy(delete_type: "soft")
596
+ expect(@user.active).to be_falsey
597
+ expect(@user).to be_destroyed
598
+ end
599
+ end
600
+ end
601
+
602
+ context "customizing HTTP methods" do
603
+ before do
604
+ Him::API.setup url: "https://api.example.com" do |builder|
605
+ builder.use Him::Middleware::FirstLevelParseJSON
606
+ builder.use Faraday::Request::UrlEncoded
607
+ end
608
+ end
609
+
610
+ context "create" do
611
+ before do
612
+ Him::API.default_api.connection.adapter :test do |stub|
613
+ stub.put("/users") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
614
+ end
615
+ spawn_model "Foo::User" do
616
+ attributes :fullname, :email
617
+ method_for :create, "PUT"
618
+ end
619
+ end
620
+
621
+ context "for top-level class" do
622
+ it "uses the custom method (PUT) instead of default method (POST)" do
623
+ user = Foo::User.new(fullname: "Tobias Fünke")
624
+ expect(user).to be_new
625
+ expect(user.save).to be_truthy
626
+ end
627
+ end
628
+
629
+ context "for children class" do
630
+ before do
631
+ class User < Foo::User; end
632
+ @spawned_models << :User
633
+ end
634
+
635
+ it "uses the custom method (PUT) instead of default method (POST)" do
636
+ user = User.new(fullname: "Tobias Fünke")
637
+ expect(user).to be_new
638
+ expect(user.save).to be_truthy
639
+ end
640
+ end
641
+ end
642
+
643
+ context "update" do
644
+ before do
645
+ Him::API.default_api.connection.adapter :test do |stub|
646
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Lindsay Fünke" }.to_json] }
647
+ stub.post("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
648
+ end
649
+
650
+ spawn_model "Foo::User" do
651
+ attributes :fullname, :email
652
+ method_for :update, :post
653
+ end
654
+ end
655
+
656
+ it "uses the custom method (POST) instead of default method (PUT)" do
657
+ user = Foo::User.find(1)
658
+ expect(user.fullname).to eq "Lindsay Fünke"
659
+ user.fullname = "Toby Fünke"
660
+ user.save
661
+ expect(user.fullname).to eq "Tobias Fünke"
662
+ end
663
+ end
664
+ end
665
+
666
+ context "registering callbacks" do
667
+ before do
668
+ Him::API.setup url: "https://api.example.com" do |builder|
669
+ builder.use Him::Middleware::FirstLevelParseJSON
670
+ builder.use Faraday::Request::UrlEncoded
671
+ builder.adapter :test do |stub|
672
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
673
+ stub.put("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
674
+ stub.post("/users") { [200, {}, { id: 2, fullname: "Lindsay Fünke" }.to_json] }
675
+ end
676
+ end
677
+
678
+ spawn_model "User" do
679
+ before_save :before_save_callback
680
+ before_create :before_create_callback
681
+ before_update :before_update_callback
682
+ after_update :after_update_callback
683
+ after_create :after_create_callback
684
+ after_save :after_save_callback
685
+ def before_save_callback; end
686
+
687
+ def before_create_callback; end
688
+
689
+ def before_update_callback; end
690
+
691
+ def after_update_callback; end
692
+
693
+ def after_create_callback; end
694
+
695
+ def after_save_callback; end
696
+ end
697
+ end
698
+
699
+ it "runs create callbacks in the correct order" do
700
+ @user = User.new(fullname: "Tobias Fünke")
701
+ expect(@user).to receive(:before_save_callback).ordered
702
+ expect(@user).to receive(:before_create_callback).ordered
703
+ expect(@user).to receive(:after_create_callback).ordered
704
+ expect(@user).to receive(:after_save_callback).ordered
705
+ @user.save
706
+ end
707
+
708
+ it "runs update callbacks in the correct order" do
709
+ @user = User.find(1)
710
+ expect(@user).to receive(:before_save_callback).ordered
711
+ expect(@user).to receive(:before_update_callback).ordered
712
+ expect(@user).to receive(:after_update_callback).ordered
713
+ expect(@user).to receive(:after_save_callback).ordered
714
+ @user.save
715
+ end
716
+ end
717
+ end