easy_command 1.0.0.pre.rc1
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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +5 -0
- data/.github/workflows/ci.yaml +52 -0
- data/.github/workflows/lint.yaml +38 -0
- data/.github/workflows/release.yml +43 -0
- data/.gitignore +14 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +1 -0
- data/.rubocop.yml +14 -0
- data/.rubocop_maintainer_style.yml +34 -0
- data/.rubocop_style.yml +142 -0
- data/.rubocop_todo.yml +453 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +89 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +736 -0
- data/easy_command.gemspec +26 -0
- data/lib/easy_command/chainable.rb +16 -0
- data/lib/easy_command/errors.rb +85 -0
- data/lib/easy_command/result.rb +53 -0
- data/lib/easy_command/ruby-2-7-specific.rb +49 -0
- data/lib/easy_command/ruby-2-specific.rb +53 -0
- data/lib/easy_command/ruby-3-specific.rb +49 -0
- data/lib/easy_command/spec_helpers/command_matchers.rb +89 -0
- data/lib/easy_command/spec_helpers/mock_command_helper.rb +89 -0
- data/lib/easy_command/spec_helpers.rb +2 -0
- data/lib/easy_command/version.rb +3 -0
- data/lib/easy_command.rb +94 -0
- data/locales/en.yml +2 -0
- data/release-please-config.json +11 -0
- data/spec/easy_command/errors_spec.rb +121 -0
- data/spec/easy_command/result_spec.rb +176 -0
- data/spec/easy_command_spec.rb +298 -0
- data/spec/factories/addition_command.rb +12 -0
- data/spec/factories/callback_command.rb +20 -0
- data/spec/factories/failure_command.rb +12 -0
- data/spec/factories/missed_call_command.rb +7 -0
- data/spec/factories/multiplication_command.rb +12 -0
- data/spec/factories/sub_command.rb +19 -0
- data/spec/factories/subcommand_command.rb +14 -0
- data/spec/factories/success_command.rb +11 -0
- data/spec/spec_helper.rb +16 -0
- metadata +102 -0
data/README.md
ADDED
@@ -0,0 +1,736 @@
|
|
1
|
+
# EasyCommand
|
2
|
+
|
3
|
+
A simple, standardized way to build and use _Service Objects_ in Ruby.
|
4
|
+
|
5
|
+
Table of Contents
|
6
|
+
=================
|
7
|
+
|
8
|
+
- [EasyCommand](#easycommand)
|
9
|
+
- [Table of Contents](#table-of-contents)
|
10
|
+
- [Requirements](#requirements)
|
11
|
+
- [Installation](#installation)
|
12
|
+
- [Contributing](#contributing)
|
13
|
+
- [Publication](#publication)
|
14
|
+
- [Automated](#automated)
|
15
|
+
- [Deprecated](#deprecated)
|
16
|
+
- [Config (ONCE)](#config-once)
|
17
|
+
- [Instructions](#instructions)
|
18
|
+
- [Usage](#usage)
|
19
|
+
- [Returned objects](#returned-objects)
|
20
|
+
- [Subcommand](#subcommand)
|
21
|
+
- [Command chaining](#command-chaining)
|
22
|
+
- [Flow success callbacks](#flow-success-callbacks)
|
23
|
+
- [Merge errors from ActiveRecord instance](#merge-errors-from-activerecord-instance)
|
24
|
+
- [Stopping execution of the command](#stopping-execution-of-the-command)
|
25
|
+
- [abort](#abort)
|
26
|
+
- [assert](#assert)
|
27
|
+
- [ExitError](#exiterror)
|
28
|
+
- [Callback](#callback)
|
29
|
+
- [#on\_success](#on_success)
|
30
|
+
- [Error message](#error-message)
|
31
|
+
- [Default scope](#default-scope)
|
32
|
+
- [Example](#example)
|
33
|
+
- [Test with Rspec](#test-with-rspec)
|
34
|
+
- [Mock](#mock)
|
35
|
+
- [Setup](#setup)
|
36
|
+
- [Usage](#usage-1)
|
37
|
+
- [Matchers](#matchers)
|
38
|
+
- [Setup](#setup-1)
|
39
|
+
- [Rails project](#rails-project)
|
40
|
+
- [Usage](#usage-2)
|
41
|
+
|
42
|
+
|
43
|
+
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
|
44
|
+
|
45
|
+
# Requirements
|
46
|
+
|
47
|
+
* At least Ruby 2.0+
|
48
|
+
|
49
|
+
It is currently used at Swile with Ruby 2.7 and Ruby 3 projects.
|
50
|
+
|
51
|
+
# Installation
|
52
|
+
|
53
|
+
Add this line to your application's Gemfile:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
gem 'easy_command'
|
57
|
+
```
|
58
|
+
|
59
|
+
And then execute:
|
60
|
+
|
61
|
+
$ bundle
|
62
|
+
|
63
|
+
Or install it yourself as:
|
64
|
+
|
65
|
+
$ gem install command
|
66
|
+
|
67
|
+
# Contributing
|
68
|
+
|
69
|
+
To ensure that our automatic release management system works perfectly, it is important to:
|
70
|
+
|
71
|
+
- strictly use conventional commits naming: https://github.com/googleapis/release-please#how-should-i-write-my-commits
|
72
|
+
- verify that all PRs name are compliant with conventional commits naming before squash-merging it into master
|
73
|
+
|
74
|
+
Please note that we are using auto release.
|
75
|
+
|
76
|
+
# Publication
|
77
|
+
|
78
|
+
## Automated
|
79
|
+
|
80
|
+
Gem publishing and releasing is now automated with [google-release-please](https://github.com/googleapis/release-please).
|
81
|
+
|
82
|
+
Workflow's configuration can be found in `.github/workflows/release.yml`
|
83
|
+
|
84
|
+
# Usage
|
85
|
+
|
86
|
+
Here's a basic example of a command that check if a collection is empty or not
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# define a command class
|
90
|
+
class CollectionChecker
|
91
|
+
# put Command before the class' ancestors chain
|
92
|
+
prepend EasyCommand
|
93
|
+
|
94
|
+
# mandatory: define a #call method. its return value will be available
|
95
|
+
# through #result
|
96
|
+
def call
|
97
|
+
@collection.empty? || errors.add(:collection, :failure, "Your collection is empty !.")
|
98
|
+
@collection.length
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# optional, initialize the command with some arguments
|
104
|
+
# optional, initialize can be public or private, private is better ;-)
|
105
|
+
def initialize(collection)
|
106
|
+
@collection = collection
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
Then, in your controller:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class CollectionController < ApplicationController
|
114
|
+
def create
|
115
|
+
# initialize and execute the command
|
116
|
+
command = CollectionChecker.call(params)
|
117
|
+
|
118
|
+
# check command outcome
|
119
|
+
if command.success?
|
120
|
+
# command#result will contain the number of items, if any
|
121
|
+
render json: { count: command.result }
|
122
|
+
else
|
123
|
+
render_error(
|
124
|
+
message: "Payload is empty.",
|
125
|
+
details: command.errors,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def render_error(details:, message: "Bad request", code: "BAD_REQUEST", status: 400)
|
133
|
+
payload = {
|
134
|
+
error: {
|
135
|
+
code: code,
|
136
|
+
message: message,
|
137
|
+
details: details,
|
138
|
+
}
|
139
|
+
}
|
140
|
+
render status: status, json: payload
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
When errors, the controller will return the following json :
|
146
|
+
|
147
|
+
```json
|
148
|
+
{
|
149
|
+
"error": {
|
150
|
+
"code": "BAD_REQUEST",
|
151
|
+
"message": "Payload is empty",
|
152
|
+
"details": {
|
153
|
+
"collection": [
|
154
|
+
{
|
155
|
+
"code": "failure",
|
156
|
+
"message": "Your collection is empty !."
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
```
|
163
|
+
|
164
|
+
## Returned objects
|
165
|
+
|
166
|
+
The EasyCommands' return values make use of the Result monad.
|
167
|
+
An EasyCommand will always return an `EasyCommand::Result` (either as an `EasyCommand::Success` or as an `EasyCommand::Failure`) which are easy to manipulate and to interface with. These objects both answer to `#success?`, `#failure?`, `#result` and `#errors` (with `#result` being the return value of the `#call` method by default).
|
168
|
+
|
169
|
+
This means that the mechanisms described below ([Subcommand](#subcommand) and [Command chaining](#command-chaining)) are
|
170
|
+
easily extendable and can be made compatible with objects that make use of them.
|
171
|
+
|
172
|
+
## Subcommand
|
173
|
+
|
174
|
+
It is also possible to call sub command and stop run if failed :
|
175
|
+
```ruby
|
176
|
+
class CollectionChecker
|
177
|
+
prepend EasyCommand
|
178
|
+
|
179
|
+
def initialize(collection)
|
180
|
+
@collection = collection
|
181
|
+
end
|
182
|
+
|
183
|
+
def call
|
184
|
+
assert_subcommand FormatChecker, @collection
|
185
|
+
@collection.empty? || errors.add(:collection, :failure, "Your collection is empty !.")
|
186
|
+
@collection.length
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class FormatChecker
|
191
|
+
prepend EasyCommand
|
192
|
+
|
193
|
+
def call
|
194
|
+
@collection.is_a?(Array) || errors.add(:collection, :failure, "Not an array")
|
195
|
+
@collection.class.name
|
196
|
+
end
|
197
|
+
|
198
|
+
def initialize(collection)
|
199
|
+
@collection = collection
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
command = CollectionChecker.call('foo')
|
204
|
+
command.success? # => false
|
205
|
+
command.failure? # => true
|
206
|
+
command.errors # => { collection: [ { code: :failure, message: "Not an array" } ] }
|
207
|
+
command.result # => nil
|
208
|
+
```
|
209
|
+
|
210
|
+
You can get result from your sub command :
|
211
|
+
```ruby
|
212
|
+
class CrossProduct
|
213
|
+
prepend EasyCommand
|
214
|
+
|
215
|
+
def call
|
216
|
+
product = assert_subcommand Multiply, @first, 100
|
217
|
+
product / @second
|
218
|
+
end
|
219
|
+
|
220
|
+
def initialize(first, second)
|
221
|
+
@first = first
|
222
|
+
@second = second
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class Multiply
|
227
|
+
def call
|
228
|
+
@first * @second
|
229
|
+
end
|
230
|
+
# ...
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
## Command chaining
|
235
|
+
|
236
|
+
Since EasyCommands are made to encapsulate a specific, unitary action it is frequent to need to chain them to represent a
|
237
|
+
logical flow. To do this, a `then` method has been provided (also aliased as `|`). This will feed the result of the
|
238
|
+
initial EasyCommand as the parameters of the following EasyCommand, and stop the execution is any error is encountered during
|
239
|
+
the flow.
|
240
|
+
|
241
|
+
This is compatible out-of-the-box with any object that answers to `#call` and returns a `EasyCommand::Result` (or similar
|
242
|
+
object).
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
class CreateUser
|
246
|
+
prepend EasyCommand
|
247
|
+
|
248
|
+
def call
|
249
|
+
puts "User #{@name} created!"
|
250
|
+
{
|
251
|
+
name: @name,
|
252
|
+
email: "#{@name.downcase}@swile.co"
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
def initialize(name)
|
257
|
+
@name = name
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class Emailer
|
262
|
+
prepend EasyCommand
|
263
|
+
|
264
|
+
def call
|
265
|
+
send_email
|
266
|
+
@user
|
267
|
+
end
|
268
|
+
|
269
|
+
def send_email
|
270
|
+
puts "Sending email at #{@email}"
|
271
|
+
if $mail_service_down
|
272
|
+
errors.add(:email, :delivery_error, "Couldn't send email to #{@email}")
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def initialize(user)
|
277
|
+
@user = user
|
278
|
+
@email = @user[:email]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class NotifyOtherServices
|
283
|
+
prepend EasyCommand
|
284
|
+
|
285
|
+
def call
|
286
|
+
puts "User created: #{@user}"
|
287
|
+
@user
|
288
|
+
end
|
289
|
+
|
290
|
+
def initialize(user)
|
291
|
+
@user = user
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
$mail_service_down = false
|
296
|
+
user_flow = EasyCommand::Params['Michel'] |
|
297
|
+
CreateUser |
|
298
|
+
Emailer |
|
299
|
+
NotifyOtherServices
|
300
|
+
# User Michel created !
|
301
|
+
# Sending email at michel@swile.co
|
302
|
+
# User created: { name: 'Michel', email: 'michel@swile.co' }
|
303
|
+
# => <EasyCommand::Success @result={ name: 'Michel', email: 'michel@swile.co' }>
|
304
|
+
|
305
|
+
$mail_service_down = true
|
306
|
+
user_flow = EasyCommand::Params['Michel'] |
|
307
|
+
CreateUser |
|
308
|
+
Emailer |
|
309
|
+
NotifyOtherServices
|
310
|
+
# User Michel created !
|
311
|
+
# Sending email at michel@swile.co
|
312
|
+
# => <EasyCommand::Error @errors={ email: [{code: :delivery_error, message: "Couldn't send email to michel@swile.co"}] }>
|
313
|
+
```
|
314
|
+
|
315
|
+
`EasyCommand::Params` is provided as a convenience object to encapsulate the initial params to feed into the flow for
|
316
|
+
readability, but `user_flow = CreateUser.call('Michel') | Emailer | NotifyOtherServices` would have been functionally
|
317
|
+
equivalent.
|
318
|
+
|
319
|
+
### Flow success callbacks
|
320
|
+
|
321
|
+
Since it is also common to react differently according to the result of the flow, convenience callback definition
|
322
|
+
methods are provided:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
user_flow.
|
326
|
+
on_success do |user|
|
327
|
+
puts "Process done without issues ! 🎉"
|
328
|
+
LaunchOnboardingProcess.call(user)
|
329
|
+
end.
|
330
|
+
on_failure do |errors|
|
331
|
+
puts "Encountered errors: #{errors}"
|
332
|
+
NotifyFailureToAdmin.call(errors)
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
## Merge errors from ActiveRecord instance
|
337
|
+
```ruby
|
338
|
+
class UserCreator
|
339
|
+
prepend EasyCommand
|
340
|
+
|
341
|
+
def call
|
342
|
+
@user.save!
|
343
|
+
rescue ActiveRecord::RecordInvalid
|
344
|
+
merge_errors_from_record(@user)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
invalid_user = User.new
|
349
|
+
command = UserCreator.call(invalid_user)
|
350
|
+
command.success? # => false
|
351
|
+
command.failure? # => true
|
352
|
+
command.errors # => { name: [ { code: :required, message: "must exist" } ] }
|
353
|
+
```
|
354
|
+
|
355
|
+
## Stopping execution of the command
|
356
|
+
|
357
|
+
To avoid the verbosity of numerous `return` statements, you have three alternative ways to stop the execution of a
|
358
|
+
command:
|
359
|
+
|
360
|
+
### abort
|
361
|
+
```ruby
|
362
|
+
class FormatChecker
|
363
|
+
prepend EasyCommand
|
364
|
+
|
365
|
+
def call
|
366
|
+
abort :collection, :failure, "Not an array" unless @collection.is_a?(Array)
|
367
|
+
@collection.class.name
|
368
|
+
end
|
369
|
+
|
370
|
+
def initialize(collection)
|
371
|
+
@collection = collection
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
command = FormatChecker.call("not array")
|
376
|
+
command.success? # => false
|
377
|
+
command.failure? # => true
|
378
|
+
command.errors # => { collection: [ { code: :failure, message: "Not an array" } ] }
|
379
|
+
```
|
380
|
+
|
381
|
+
It also accepts a `result:` parameter to give the Failure object a value.
|
382
|
+
```ruby
|
383
|
+
# ...
|
384
|
+
abort :collection, :failure, "Not an array", result: @collection
|
385
|
+
# ...
|
386
|
+
|
387
|
+
command = FormatChecker.call(my_custom_object)
|
388
|
+
command.result # => my_custom_object
|
389
|
+
```
|
390
|
+
|
391
|
+
### assert
|
392
|
+
```ruby
|
393
|
+
class UserDestroyer
|
394
|
+
prepend EasyCommand
|
395
|
+
|
396
|
+
def call
|
397
|
+
assert check_if_user_is_destroyable
|
398
|
+
@user.destroy!
|
399
|
+
end
|
400
|
+
|
401
|
+
def check_if_user_is_destroyable
|
402
|
+
errors.add :user, :active, "Can't destroy active users" if @user.projects.active.any?
|
403
|
+
errors.add :user, :sole_admin, "Can't destroy last admin" if @user.admin? && User.admin.count == 1
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
invalid_user = User.admin.with_active_projects.first
|
408
|
+
command = UserDestroyer.call(invalid_user)
|
409
|
+
command.success? # => false
|
410
|
+
command.failure? # => true
|
411
|
+
command.errors # => { user: [
|
412
|
+
# { code: :active, message: "Can't destroy active users" },
|
413
|
+
# { code: :sole_admin, message: "Can't destroy last admin" }
|
414
|
+
# ] }
|
415
|
+
```
|
416
|
+
|
417
|
+
It also accepts a `result:` parameter to give the Failure object a value.
|
418
|
+
```ruby
|
419
|
+
# ...
|
420
|
+
assert check_if_user_is_destroyable, result: @user
|
421
|
+
# ...
|
422
|
+
|
423
|
+
command = UserDestroyer.call(invalid_user)
|
424
|
+
command.result # => invalid_user
|
425
|
+
```
|
426
|
+
|
427
|
+
|
428
|
+
### ExitError
|
429
|
+
|
430
|
+
Raising an `ExitError` anywhere during `#call`'s execution will stop the command, this is not recommended but can be
|
431
|
+
used to develop your own failure helpers. It can be initialized with a `code` and `message` optional parameters and a named parameter `result:` to give the Failure object a value.
|
432
|
+
|
433
|
+
## Callback
|
434
|
+
|
435
|
+
Sometimes, you need to deport action, after all command and sub commands are executed.
|
436
|
+
It is useful to send email or broadcast notification when all operation succeeded.
|
437
|
+
To make this possible, you can use `#on_success` callback.
|
438
|
+
|
439
|
+
### #on_success
|
440
|
+
|
441
|
+
This callback works through `assert_sub` when using sub command system.
|
442
|
+
**Note: the `on_success` callback of a command will be executed as soon as the
|
443
|
+
subcommand is done if it the command is`call`ed directly instead of through `assert_sub`**
|
444
|
+
Examples are better than many words :wink:.
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
class Updater
|
448
|
+
def call; end
|
449
|
+
def on_success
|
450
|
+
puts "#{self.class.name}##{__method__}"
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
class CarUpdater < Updater
|
455
|
+
prepend EasyCommand
|
456
|
+
end
|
457
|
+
|
458
|
+
class BikeUpdater < Updater
|
459
|
+
prepend EasyCommand
|
460
|
+
end
|
461
|
+
|
462
|
+
class SkateUpdater < Updater
|
463
|
+
prepend EasyCommand
|
464
|
+
def call
|
465
|
+
abort :skate, :broken
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
class SuccessfulVehicleUpdater < Updater
|
470
|
+
prepend EasyCommand
|
471
|
+
def call
|
472
|
+
assert_sub CarUpdater
|
473
|
+
assert_sub BikeUpdater
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
class FailedVehicleUpdater < Updater
|
478
|
+
prepend EasyCommand
|
479
|
+
def call
|
480
|
+
assert_sub BikeUpdater
|
481
|
+
assert_sub SkateUpdater
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
SuccessfulVehicleUpdater.call
|
486
|
+
# CarUpdater#on_success
|
487
|
+
# BikeUpdater#on_success
|
488
|
+
# SuccessfulVehicleUpdater#on_success
|
489
|
+
|
490
|
+
FailedVehicleUpdater.call
|
491
|
+
# "nothing"
|
492
|
+
```
|
493
|
+
|
494
|
+
|
495
|
+
## Error message
|
496
|
+
|
497
|
+
The third parameter is the message.
|
498
|
+
```ruby
|
499
|
+
errors.add(:item, :invalid, 'It is invalid !')
|
500
|
+
```
|
501
|
+
|
502
|
+
A symbol can be used and the sentence will be generated with I18n (if it is loaded) :
|
503
|
+
```ruby
|
504
|
+
errors.add(:item, :invalid, :invalid_item)
|
505
|
+
```
|
506
|
+
|
507
|
+
Scope can be used with symbol :
|
508
|
+
```ruby
|
509
|
+
errors.add(:item, :invalid, :'errors.invalid_item')
|
510
|
+
# equivalent to
|
511
|
+
errors.add(:item, :invalid, :invalid_item, scope: :errors)
|
512
|
+
```
|
513
|
+
|
514
|
+
Error message is optional when adding error :
|
515
|
+
```ruby
|
516
|
+
errors.add(:item, :invalid)
|
517
|
+
```
|
518
|
+
|
519
|
+
is equivalent to
|
520
|
+
```ruby
|
521
|
+
errors.add(:item, :invalid, :invalid)
|
522
|
+
```
|
523
|
+
|
524
|
+
### Default scope
|
525
|
+
|
526
|
+
Inside an EasyCommand class, you can specify a base I18n scope by calling the class method `#i18n_scope=`, it will be the
|
527
|
+
default scope used to localize error messages during `errors.add`. Default value is `errors.messages`.
|
528
|
+
|
529
|
+
### Example
|
530
|
+
```yaml
|
531
|
+
# config/locales/en.yml
|
532
|
+
en:
|
533
|
+
errors:
|
534
|
+
messages:
|
535
|
+
date:
|
536
|
+
invalid: "Invalid date (yyyy-mm-dd)"
|
537
|
+
invalid: "Invalid value"
|
538
|
+
activerecord:
|
539
|
+
messages:
|
540
|
+
invalid: "Invalid record"
|
541
|
+
```
|
542
|
+
|
543
|
+
```ruby
|
544
|
+
# config/locales/en.yml
|
545
|
+
|
546
|
+
class CommandWithDefaultScope
|
547
|
+
prepend EasyCommand
|
548
|
+
|
549
|
+
def call
|
550
|
+
errors.add(:generic_attribute, :invalid) # Identical to errors.add(:generic_attribute, :invalid, :invalid)
|
551
|
+
errors.add(:date_attribute, :invalid, 'date.invalid')
|
552
|
+
end
|
553
|
+
end
|
554
|
+
CommandWithDefaultScope.call.errors == {
|
555
|
+
generic_attribute: [{ code: :invalid, message: "Invalid value" }],
|
556
|
+
date_attribute: [{ code: :invalid, message: "Invalid date (yyyy-mm-dd)" }],
|
557
|
+
}
|
558
|
+
|
559
|
+
class CommandWithCustomScope
|
560
|
+
prepend EasyCommand
|
561
|
+
|
562
|
+
self.i18n_scope = 'activerecord.messages'
|
563
|
+
|
564
|
+
def call
|
565
|
+
errors.add(:base, :invalid) # Identical to errors.add(:base_attribute, :invalid, :invalid)
|
566
|
+
end
|
567
|
+
end
|
568
|
+
CommandWithCustomScope.call.errors == {
|
569
|
+
base: [{ code: :invalid, message: "Invalid record" }],
|
570
|
+
}
|
571
|
+
```
|
572
|
+
|
573
|
+
# Test with Rspec
|
574
|
+
Make the spec file `spec/commands/collection_checker_spec.rb` like:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
describe CollectionChecker do
|
578
|
+
subject { described_class.call(collection) }
|
579
|
+
|
580
|
+
describe '.call' do
|
581
|
+
context 'when the context is successful' do
|
582
|
+
let(:collection) { [1] }
|
583
|
+
|
584
|
+
it 'succeeds' do
|
585
|
+
is_expected.to be_success
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
context 'when the context is not successful' do
|
590
|
+
let(:collection) { [] }
|
591
|
+
|
592
|
+
it 'fails' do
|
593
|
+
is_expected.to be_failure
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
```
|
599
|
+
|
600
|
+
## Mock
|
601
|
+
|
602
|
+
To simplify your life, the gem come with mock helper.
|
603
|
+
You must include `EasyCommand::SpecHelpers::MockCommandHelper`in your code.
|
604
|
+
|
605
|
+
### Setup
|
606
|
+
|
607
|
+
To allow this, you must require the `spec_helpers` file and include them into your specs files :
|
608
|
+
```ruby
|
609
|
+
require 'easy_command/spec_helpers'
|
610
|
+
describe CollectionChecker do
|
611
|
+
include EasyCommand::SpecHelpers::MockCommandHelper
|
612
|
+
# ...
|
613
|
+
end
|
614
|
+
```
|
615
|
+
|
616
|
+
or directly in your `spec_helpers` :
|
617
|
+
```ruby
|
618
|
+
require 'easy_command/spec_helpers'
|
619
|
+
RSpec.configure do |config|
|
620
|
+
config.include EasyCommand::SpecHelpers::MockCommandHelper
|
621
|
+
end
|
622
|
+
```
|
623
|
+
|
624
|
+
### Usage
|
625
|
+
|
626
|
+
You can mock a command, to be successful or to fail :
|
627
|
+
```ruby
|
628
|
+
describe "#mock_command" do
|
629
|
+
subject { mock }
|
630
|
+
|
631
|
+
context "to fail" do
|
632
|
+
let(:mock) do
|
633
|
+
mock_command(CollectionChecker,
|
634
|
+
success: false,
|
635
|
+
result: nil,
|
636
|
+
errors: { collection: [ code: :empty, message: "Your collection is empty !" ] },
|
637
|
+
)
|
638
|
+
end
|
639
|
+
|
640
|
+
it { is_expected.to be_failure }
|
641
|
+
it { is_expected.to_not be_success }
|
642
|
+
it { expect(subject.errors).to eql({ collection: [ code: :empty, message: "Your collection is empty !" ] }) }
|
643
|
+
it { expect(subject.result).to be_nil }
|
644
|
+
end
|
645
|
+
|
646
|
+
context "to success" do
|
647
|
+
let(:mock) do
|
648
|
+
mock_command(CollectionChecker,
|
649
|
+
success: true,
|
650
|
+
result: 10,
|
651
|
+
errors: {},
|
652
|
+
)
|
653
|
+
end
|
654
|
+
|
655
|
+
it { is_expected.to_not be_failure }
|
656
|
+
it { is_expected.to be_success }
|
657
|
+
it { expect(subject.errors).to be_empty }
|
658
|
+
it { expect(subject.result).to eql 10 }
|
659
|
+
end
|
660
|
+
end
|
661
|
+
```
|
662
|
+
|
663
|
+
For an unsuccessful command, you can use a simpler mock :
|
664
|
+
```ruby
|
665
|
+
let(:mock) do
|
666
|
+
mock_unsuccessful_command(CollectionChecker,
|
667
|
+
errors: { collection: { empty: "Your collection is empty !" } }
|
668
|
+
)
|
669
|
+
end
|
670
|
+
```
|
671
|
+
|
672
|
+
For a successful command, you can use a simpler mock :
|
673
|
+
```ruby
|
674
|
+
let(:mock) do
|
675
|
+
mock_successful_command(CollectionChecker,
|
676
|
+
result: 10
|
677
|
+
)
|
678
|
+
end
|
679
|
+
```
|
680
|
+
|
681
|
+
## Matchers
|
682
|
+
|
683
|
+
To simplify your life, the gem come with matchers.
|
684
|
+
You must include `EasyCommand::SpecHelpers::CommandMatchers`in your code.
|
685
|
+
|
686
|
+
### Setup
|
687
|
+
|
688
|
+
To allow this, you must require the `spec_helpers` file and include them into your specs files :
|
689
|
+
```ruby
|
690
|
+
require 'easy_command/spec_helpers'
|
691
|
+
describe CollectionChecker do
|
692
|
+
include EasyCommand::SpecHelpers::CommandMatchers
|
693
|
+
# ...
|
694
|
+
end
|
695
|
+
```
|
696
|
+
|
697
|
+
or directly in your `spec_helpers` :
|
698
|
+
```ruby
|
699
|
+
require 'easy_command/spec_helpers'
|
700
|
+
RSpec.configure do |config|
|
701
|
+
config.include EasyCommand::SpecHelpers::CommandMatchers
|
702
|
+
end
|
703
|
+
```
|
704
|
+
|
705
|
+
### Rails project
|
706
|
+
|
707
|
+
Instead of above, you can include matchers only for specific classes, using inference
|
708
|
+
|
709
|
+
```ruby
|
710
|
+
require 'easy_command/spec_helpers'
|
711
|
+
RSpec::Rails::DIRECTORY_MAPPINGS[:class] = %w[spec classes]
|
712
|
+
RSpec.configure do |config|
|
713
|
+
config.include EasyCommand::SpecHelpers::CommandMatchers, type: :class
|
714
|
+
end
|
715
|
+
```
|
716
|
+
|
717
|
+
### Usage
|
718
|
+
```ruby
|
719
|
+
subject { CollectionChecker.call({}) }
|
720
|
+
|
721
|
+
it { is_expected.to be_failure }
|
722
|
+
it { is_expected.to have_failed }
|
723
|
+
it { is_expected.to have_failed.with_error(:collection, :empty) }
|
724
|
+
it { is_expected.to have_failed.with_error(:collection, :empty, "Your collection is empty !") }
|
725
|
+
it { is_expected.to have_error(:collection, :empty) }
|
726
|
+
it { is_expected.to have_error(:collection, :empty, "Your collection is empty !") }
|
727
|
+
|
728
|
+
context "when called in a controller" do
|
729
|
+
before { get :index }
|
730
|
+
# the 3 matchers bellow are aliases
|
731
|
+
it { expect(CollectionChecker).to have_been_called_with_action_controller_parameters(payload) }
|
732
|
+
it { expect(CollectionChecker).to have_been_called_with_ac_parameters(payload) }
|
733
|
+
it { expect(CollectionChecker).to have_been_called_with_acp(payload) }
|
734
|
+
end
|
735
|
+
|
736
|
+
```
|