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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +5 -0
  3. data/.github/workflows/ci.yaml +52 -0
  4. data/.github/workflows/lint.yaml +38 -0
  5. data/.github/workflows/release.yml +43 -0
  6. data/.gitignore +14 -0
  7. data/.release-please-manifest.json +3 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +14 -0
  10. data/.rubocop_maintainer_style.yml +34 -0
  11. data/.rubocop_style.yml +142 -0
  12. data/.rubocop_todo.yml +453 -0
  13. data/.ruby-version +1 -0
  14. data/CHANGELOG.md +89 -0
  15. data/Gemfile +19 -0
  16. data/LICENSE.txt +22 -0
  17. data/README.md +736 -0
  18. data/easy_command.gemspec +26 -0
  19. data/lib/easy_command/chainable.rb +16 -0
  20. data/lib/easy_command/errors.rb +85 -0
  21. data/lib/easy_command/result.rb +53 -0
  22. data/lib/easy_command/ruby-2-7-specific.rb +49 -0
  23. data/lib/easy_command/ruby-2-specific.rb +53 -0
  24. data/lib/easy_command/ruby-3-specific.rb +49 -0
  25. data/lib/easy_command/spec_helpers/command_matchers.rb +89 -0
  26. data/lib/easy_command/spec_helpers/mock_command_helper.rb +89 -0
  27. data/lib/easy_command/spec_helpers.rb +2 -0
  28. data/lib/easy_command/version.rb +3 -0
  29. data/lib/easy_command.rb +94 -0
  30. data/locales/en.yml +2 -0
  31. data/release-please-config.json +11 -0
  32. data/spec/easy_command/errors_spec.rb +121 -0
  33. data/spec/easy_command/result_spec.rb +176 -0
  34. data/spec/easy_command_spec.rb +298 -0
  35. data/spec/factories/addition_command.rb +12 -0
  36. data/spec/factories/callback_command.rb +20 -0
  37. data/spec/factories/failure_command.rb +12 -0
  38. data/spec/factories/missed_call_command.rb +7 -0
  39. data/spec/factories/multiplication_command.rb +12 -0
  40. data/spec/factories/sub_command.rb +19 -0
  41. data/spec/factories/subcommand_command.rb +14 -0
  42. data/spec/factories/success_command.rb +11 -0
  43. data/spec/spec_helper.rb +16 -0
  44. 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
+ ```