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