cuprum 1.0.0 → 1.1.0.rc.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +44 -1494
- data/lib/cuprum/built_in/identity_command.rb +4 -0
- data/lib/cuprum/built_in/identity_operation.rb +4 -4
- data/lib/cuprum/built_in/null_command.rb +5 -1
- data/lib/cuprum/command.rb +27 -16
- data/lib/cuprum/command_factory.rb +4 -4
- data/lib/cuprum/currying/curried_command.rb +1 -1
- data/lib/cuprum/error.rb +42 -0
- data/lib/cuprum/errors/command_not_implemented.rb +2 -3
- data/lib/cuprum/errors/multiple_errors.rb +39 -0
- data/lib/cuprum/errors/operation_not_called.rb +2 -3
- data/lib/cuprum/errors/uncaught_exception.rb +2 -2
- data/lib/cuprum/errors.rb +6 -1
- data/lib/cuprum/exception_handling.rb +4 -2
- data/lib/cuprum/map_command.rb +227 -0
- data/lib/cuprum/matcher.rb +2 -0
- data/lib/cuprum/matcher_list.rb +2 -2
- data/lib/cuprum/matching.rb +3 -1
- data/lib/cuprum/middleware.rb +20 -13
- data/lib/cuprum/operation.rb +8 -10
- data/lib/cuprum/processing.rb +22 -11
- data/lib/cuprum/result_list.rb +187 -0
- data/lib/cuprum/rspec/be_a_result_matcher.rb +6 -6
- data/lib/cuprum/steps.rb +4 -1
- data/lib/cuprum/utils/instance_spy.rb +22 -24
- data/lib/cuprum/version.rb +3 -3
- data/lib/cuprum.rb +3 -2
- metadata +20 -17
data/README.md
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
# Cuprum
|
2
2
|
|
3
|
-
|
3
|
+
Toolkit for implementing business logic as function objects.
|
4
4
|
|
5
|
-
|
5
|
+
Cuprum defines a Command object, which is a callable object that encapsulates some piece of business logic. Each call to a Command returns a Result with a status and optionally data or an error object. As objects, Commands can be passed as parameters, returned from methods (or other Commands). Commands also define a #step method, which can be used to gracefully handle failure states and define complex operations by combining simpler components.
|
6
6
|
|
7
|
-
|
8
|
-
- [Operations](#Operations) - A stateful `Command` that wraps and delegates to its most recent `Result`.
|
9
|
-
- [Results](#Results) - An immutable data object with a status (either `:success` or `:failure`), and optional `#value` and/or `#error` objects.
|
10
|
-
- [Errors](#Errors) - Encapsulates a failure state of a command.
|
11
|
-
- [Matchers](#Matchers) - Define handling for results based on status, error, and value.
|
7
|
+
It defines the following concepts:
|
12
8
|
|
13
|
-
|
9
|
+
- [Commands](http://sleepingkingstudios.github.io/cuprum/commands) - A function-like object that responds to `#call` and returns a `Result`.
|
10
|
+
- [Operations](http://sleepingkingstudios.github.io/cuprum/commands/operations) - A stateful `Command` that wraps and delegates to its most recent `Result`.
|
11
|
+
- [Results](http://sleepingkingstudios.github.io/cuprum/results) - An immutable data object with a status (either `:success` or `:failure`), and optional `#value` and/or `#error` objects.
|
12
|
+
- [Errors](http://sleepingkingstudios.github.io/cuprum/errors) - Encapsulates a failure state of a command.
|
13
|
+
- [Matchers](http://sleepingkingstudios.github.io/cuprum/matchers) - Define handling for results based on status, error, and value.
|
14
14
|
|
15
15
|
Traditional frameworks such as Rails focus on the objects of your application - the "nouns" such as User, Post, or Item. Using Cuprum or a similar library allows you the developer to make your business logic - the "verbs" such as Create User, Update Post or Ship Item - a first-class citizen of your project. This provides several advantages:
|
16
16
|
|
17
17
|
- **Consistency:** Use the same Commands to underlie controller actions, worker processes and test factories.
|
18
18
|
- **Encapsulation:** Each Command is defined and run in isolation, and dependencies must be explicitly provided to the command when it is initialized or run. This makes it easier to reason about the command's behavior and keep it insulated from changes elsewhere in the code.
|
19
19
|
- **Testability:** Because the logic is extracted from unnecessary context, testing its behavior is much cleaner and easier.
|
20
|
-
- **Composability:** Complex logic such as "find the object with this ID, update it with these attributes, and log the transaction to the reporting service" can be extracted into a series of simple Commands and composed together. The [step](
|
20
|
+
- **Composability:** Complex logic such as "find the object with this ID, update it with these attributes, and log the transaction to the reporting service" can be extracted into a series of simple Commands and composed together. The [step](http://sleepingkingstudios.github.io/cuprum/commands/steps) feature allows for complex control flows.
|
21
21
|
- **Reusability:** Logic common to multiple data models or instances in your code, such as "persist an object to the database" or "find all records with a given user and created in a date range" can be refactored into parameterized commands.
|
22
22
|
|
23
|
-
|
23
|
+
## Why Cuprum?
|
24
24
|
|
25
25
|
Cuprum allows you to define or extract business logic from models, controllers, jobs or freeform services, and to control the flow of that logic by composing together atomic commands. At its heart, Cuprum relies on three features: commands, results, and control flow using steps.
|
26
26
|
|
@@ -28,21 +28,23 @@ There are a number of other Ruby libraries and frameworks that provide similar s
|
|
28
28
|
|
29
29
|
On the opposite end of the scale, frameworks such as [Dry::Monads](https://dry-rb.org/gems/dry-monads/) or [Trailblazer](http://trailblazer.to/) can also provide similar functionality to Cuprum. These frameworks require a larger commitment to use, particularly for a smaller team or on a smaller project, and often use idiosyncratic syntax that requires a steep learning curve. Cuprum is designed to offer a lightweight alternative that should be much more accessible to new developers.
|
30
30
|
|
31
|
-
|
31
|
+
## Compatibility
|
32
32
|
|
33
|
-
Cuprum is tested against Ruby (MRI) 2.
|
33
|
+
Cuprum is tested against Ruby (MRI) 2.7 through 3.2.
|
34
34
|
|
35
|
-
|
35
|
+
## Documentation
|
36
36
|
|
37
|
-
|
37
|
+
Code documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
|
38
38
|
|
39
|
-
|
39
|
+
The full documentation is available via [GitHub Pages](http://sleepingkingstudios.github.io/cuprum), and includes the code documentation as well as a deeper explanation of Cuprum's features and design philosophy. It also includes documentation for prior versions of the gem.
|
40
40
|
|
41
|
-
|
41
|
+
## License
|
42
|
+
|
43
|
+
Copyright (c) 2017-2022 Rob Smith
|
42
44
|
|
43
45
|
Cuprum is released under the [MIT License](https://opensource.org/licenses/MIT).
|
44
46
|
|
45
|
-
|
47
|
+
## Contribute
|
46
48
|
|
47
49
|
The canonical repository for this gem is located at https://github.com/sleepingkingstudios/cuprum.
|
48
50
|
|
@@ -50,7 +52,7 @@ To report a bug or submit a feature request, please use the [Issue Tracker](http
|
|
50
52
|
|
51
53
|
To contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/cuprum/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.
|
52
54
|
|
53
|
-
|
55
|
+
## Code of Conduct
|
54
56
|
|
55
57
|
Please note that the `Cuprum` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/cuprum/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.
|
56
58
|
|
@@ -80,6 +82,8 @@ Here is the logic required to fulfill a reserve book request:
|
|
80
82
|
- If so, we create a `BookReservation` for the `Title` and the `Patron`.
|
81
83
|
- If not, we create a `BookLoan` for a `PhysicalBook` and the `Patron`.
|
82
84
|
|
85
|
+
### Defining Commands
|
86
|
+
|
83
87
|
Let's get started by handling the `Patron` validation.
|
84
88
|
|
85
89
|
```ruby
|
@@ -216,7 +220,7 @@ Through the magic of composition, each of the checks we defined in our prior com
|
|
216
220
|
|
217
221
|
Finally, we're using plain old Ruby conditionals to determine whether to reserve the book or add the patron to a wait list. Cuprum is a powerful tool, but you don't have to use it for everything - it's specifically designed to be easy to move back and forth between Cuprum and plain Ruby. We could absolutely define a `HasAvailableCopies` command, but we don't have to.
|
218
222
|
|
219
|
-
### Using
|
223
|
+
### Using Commands
|
220
224
|
|
221
225
|
We've defined our `LoanOrReserveTitle` command. How can we put it to work?
|
222
226
|
|
@@ -358,1478 +362,24 @@ We're also defining a custom error class, which gives us three benefits. First,
|
|
358
362
|
|
359
363
|
The latter two are particularly important when handling errors returned by a failing command. For example, an API response for a failed validation might include a JSON object serializing the validation errors. Likewise, the application should have different responses to an `InvalidSession` error (redirect to a login page) compared to a `BookNotFound` error (display a message and return to book selection) or a `PatronUnpaidFines` error (show a link to pay outstanding fines). Using custom error classes allows the application to adapt its behavior based on the type of failure, either with a conventional Ruby conditional or `case` statement, or by using a `Cuprum::Matcher`.
|
360
364
|
|
361
|
-
##
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
command = BuildPostCommand.new
|
383
|
-
result = command.call(title: 'The Hobbit')
|
384
|
-
result.class #=> Cuprum::Result
|
385
|
-
result.success? #=> true
|
386
|
-
|
387
|
-
book = result.value
|
388
|
-
book.class #=> Book
|
389
|
-
book.title #=> 'The Hobbit'
|
390
|
-
```
|
391
|
-
|
392
|
-
There are several takeaways from this example. First, we are defining a custom command class that inherits from `Cuprum::Command`. We are defining the `#process` method, which takes a single `attributes` parameter and returns an instance of `Book`. Then, we are creating an instance of the command, and invoking the `#call` method with an attributes hash. These attributes are passed to our `#process` implementation. Invoking `#call` returns a result, and the `#value` of the result is our new Book.
|
393
|
-
|
394
|
-
Because a command is just a Ruby object, we can also pass values to the constructor.
|
395
|
-
|
396
|
-
```ruby
|
397
|
-
class SaveBookCommand < Cuprum::Command
|
398
|
-
def initialize repository
|
399
|
-
@repository = repository
|
400
|
-
end
|
401
|
-
|
402
|
-
def process book
|
403
|
-
if @repository.persist(book)
|
404
|
-
success(book)
|
405
|
-
else
|
406
|
-
failure('unable to save book')
|
407
|
-
end
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
books = [
|
412
|
-
Book.new(title: 'The Fellowship of the Ring'),
|
413
|
-
Book.new(title: 'The Two Towers'),
|
414
|
-
Book.new(title: 'The Return of the King')
|
415
|
-
]
|
416
|
-
command = SaveBookCommand.new(books_repository)
|
417
|
-
books.each { |book| command.call(book) }
|
418
|
-
```
|
419
|
-
|
420
|
-
Here, we are defining a command that might fail - maybe the database is unavailable, or there's a constraint that is violated by the inserted attributes. If the call to `#persist` succeeds, we're returning a Result with a status of `:success` and the value set to the persisted book.
|
421
|
-
Conversely, if the call to `#persist` fails, we're returning a Result with a status of `:failure` and a custom error message. Since the `#process` method returns a Result, it is returned directly by `#call`.
|
422
|
-
|
423
|
-
Note also that we are reusing the same command three times, rather than creating a new save command for each book. Each book is persisted to the `books_repository`. This is also an example of how using commands can simplify code - notice that nothing about the `SaveBookCommand` is specific to the `Book` model. Thus, we could refactor this into a generic `SaveModelCommand`.
|
424
|
-
|
425
|
-
A command can also be defined by passing block to `Cuprum::Command.new`.
|
426
|
-
|
427
|
-
```ruby
|
428
|
-
increment_command = Cuprum::Command.new { |int| int + 1 }
|
429
|
-
|
430
|
-
increment_command.call(2).value #=> 3
|
431
|
-
```
|
432
|
-
|
433
|
-
If the command is wrapping a method on the receiver, the syntax is even simpler:
|
434
|
-
|
435
|
-
```ruby
|
436
|
-
inspect_command = Cuprum::Command.new { |obj| obj.inspect }
|
437
|
-
inspect_command = Cuprum::Command.new(&:inspect) # Equivalent to above.
|
438
|
-
```
|
439
|
-
|
440
|
-
Commands defined using `Cuprum::Command.new` are quick to use, but more difficult to read and to reuse. Defining your own command class is recommended if a command definition takes up more than one line, or if the command will be used in more than one place.
|
441
|
-
|
442
|
-
#### Result Values
|
443
|
-
|
444
|
-
Calling the `#call` method on a `Cuprum::Command` instance will always return an instance of `Cuprum::Result`. The result's `#value` property is determined by the object returned by the `#process` method (if the command is defined as a class) or the block (if the command is defined by passing a block to `Cuprum::Command.new`).
|
445
|
-
|
446
|
-
The `#value` depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the method `#to_cuprum_result` is considered to be a result.
|
447
|
-
|
448
|
-
If the object returned by `#process` is **not** a result, then the `#value` of the returned result is set to the object.
|
449
|
-
|
450
|
-
```ruby
|
451
|
-
command = Cuprum::Command.new { 'Greetings, programs!' }
|
452
|
-
result = command.call
|
453
|
-
result.class #=> Cuprum::Result
|
454
|
-
result.value #=> 'Greetings, programs!'
|
455
|
-
```
|
456
|
-
|
457
|
-
If the object returned by `#process` is a result object, then result is returned directly.
|
458
|
-
|
459
|
-
```ruby
|
460
|
-
command = Cuprum::Command.new { Cuprum::Result.new(value: 'Greetings, programs!') }
|
461
|
-
result = command.call
|
462
|
-
result.class #=> Cuprum::Result
|
463
|
-
result.value #=> 'Greetings, programs!'
|
464
|
-
```
|
465
|
-
|
466
|
-
#### Success, Failure, and Errors
|
467
|
-
|
468
|
-
Each Result has a status, either `:success` or `:failure`. A Result will have a status of `:failure` when it was created with an error object. Otherwise, a Result will have a status of `:success`. Returning a failing Result from a Command indicates that something went wrong while executing the Command.
|
469
|
-
|
470
|
-
```ruby
|
471
|
-
class PublishBookCommand < Cuprum::Command
|
472
|
-
private
|
473
|
-
|
474
|
-
def process book
|
475
|
-
if book.cover.nil?
|
476
|
-
return Cuprum::Result.new(error: 'This book does not have a cover.')
|
477
|
-
end
|
478
|
-
|
479
|
-
book.published = true
|
480
|
-
|
481
|
-
book
|
482
|
-
end
|
483
|
-
end
|
484
|
-
```
|
485
|
-
|
486
|
-
In addition, the result object defines `#success?` and `#failure?` predicates.
|
487
|
-
|
488
|
-
```ruby
|
489
|
-
book = Book.new(title: 'The Silmarillion', cover: Cover.new)
|
490
|
-
book.published? #=> false
|
491
|
-
|
492
|
-
result = PublishBookCommand.new.call(book)
|
493
|
-
result.error #=> nil
|
494
|
-
result.success? #=> true
|
495
|
-
result.failure? #=> false
|
496
|
-
result.value #=> book
|
497
|
-
book.published? #=> true
|
498
|
-
```
|
499
|
-
|
500
|
-
If the result does have an error, `#success?` will return false and `#failure?` will return true.
|
501
|
-
|
502
|
-
```ruby
|
503
|
-
book = Book.new(title: 'The Silmarillion', cover: nil)
|
504
|
-
book.published? #=> false
|
505
|
-
|
506
|
-
result = PublishBookCommand.new.call(book)
|
507
|
-
result.error #=> 'This book does not have a cover.'
|
508
|
-
result.success? #=> false
|
509
|
-
result.failure? #=> true
|
510
|
-
result.value #=> book
|
511
|
-
book.published? #=> false
|
512
|
-
```
|
513
|
-
|
514
|
-
#### Command Currying
|
515
|
-
|
516
|
-
Cuprum::Command defines the `#curry` method, which allows for partial application of command objects. Partial application (more commonly referred to, if imprecisely, as currying) refers to fixing some number of arguments to a function, resulting in a function with a smaller number of arguments.
|
517
|
-
|
518
|
-
In Cuprum's case, a curried (partially applied) command takes an original command and pre-defines some of its arguments. When the curried command is called, the predefined arguments and/or keywords will be combined with the arguments passed to #call.
|
519
|
-
|
520
|
-
##### Currying Arguments
|
521
|
-
|
522
|
-
We start by defining the base command. In this case, our base command takes two string arguments - a greeting and a person to be greeted.
|
523
|
-
|
524
|
-
```ruby
|
525
|
-
say_command = Cuprum::Command.new do |greeting, person|
|
526
|
-
"#{greeting}, #{person}!"
|
527
|
-
end
|
528
|
-
say_command.call('Hello', 'world')
|
529
|
-
#=> returns a result with value 'Hello, world!'
|
530
|
-
```
|
531
|
-
|
532
|
-
Next, we create a curried command. Here, we pass in one argument. This will set the first argument to always be "Greetings"; therefore, our curried command only takes one argument, the name of the person being greeted.
|
533
|
-
|
534
|
-
```ruby
|
535
|
-
greet_command = say_command.curry('Greetings')
|
536
|
-
greet_command.call('programs')
|
537
|
-
#=> returns a result with value 'Greetings, programs!'
|
538
|
-
```
|
539
|
-
|
540
|
-
Alternatively, we could pass both arguments to `#curry`. In this case, our curried argument does not take any arguments, and will always return the same string.
|
541
|
-
|
542
|
-
```ruby
|
543
|
-
recruit_command = say_command.curry('Greetings', 'starfighter')
|
544
|
-
recruit_command.call
|
545
|
-
#=> returns a result with value 'Greetings, starfighter!'
|
546
|
-
```
|
547
|
-
|
548
|
-
##### Currying Keywords
|
549
|
-
|
550
|
-
We can also pass keywords to `#curry`. Again, we start by defining our base command. In this case, our base command takes a mathematical operation (addition, subtraction, multiplication, etc) and a list of operands.
|
551
|
-
|
552
|
-
```ruby
|
553
|
-
math_command = Cuprum::Command.new do |operands:, operation:|
|
554
|
-
operations.reduce(&operation)
|
555
|
-
end
|
556
|
-
math_command.call(operands: [2, 2], operation: :+)
|
557
|
-
#=> returns a result with value 4
|
558
|
-
```
|
559
|
-
|
560
|
-
Our curried command still takes two keywords, but now the operation keyword is optional. It now defaults to :\*, for multiplication.
|
561
|
-
|
562
|
-
```ruby
|
563
|
-
multiply_command = math_command.curry(operation: :*)
|
564
|
-
multiply_command.call(operands: [3, 3])
|
565
|
-
#=> returns a result with value 9
|
566
|
-
```
|
567
|
-
|
568
|
-
#### Composing Commands
|
569
|
-
|
570
|
-
Because Cuprum::Command instances are proper objects, they can be composed like any other object. For example, we could define some basic mathematical operations by composing commands:
|
571
|
-
|
572
|
-
```ruby
|
573
|
-
increment_command = Cuprum::Command.new { |i| i + 1 }
|
574
|
-
increment_command.call(1).value #=> 2
|
575
|
-
increment_command.call(2).value #=> 3
|
576
|
-
increment_command.call(3).value #=> 4
|
577
|
-
|
578
|
-
add_command = Cuprum::Command.new do |addend, i|
|
579
|
-
# Here, we are composing commands together by calling the increment_command
|
580
|
-
# instance from inside the add_command definition.
|
581
|
-
addend.times { i = increment_command(i).value }
|
582
|
-
|
583
|
-
i
|
584
|
-
end
|
585
|
-
|
586
|
-
add_command.call(1, 1).value #=> 2
|
587
|
-
add_command.call(1, 2).value #=> 3
|
588
|
-
add_command.call(2, 1).value #=> 3
|
589
|
-
add_command.call(2, 2).value #=> 4
|
590
|
-
```
|
591
|
-
|
592
|
-
This can also be done using command classes.
|
593
|
-
|
594
|
-
```ruby
|
595
|
-
class IncrementCommand < Cuprum::Command
|
596
|
-
private
|
597
|
-
|
598
|
-
def process i
|
599
|
-
i + 1
|
600
|
-
end
|
601
|
-
end
|
602
|
-
|
603
|
-
class AddCommand < Cuprum::Command
|
604
|
-
def initialize addend
|
605
|
-
@addend = addend
|
606
|
-
end
|
607
|
-
|
608
|
-
private
|
609
|
-
|
610
|
-
def increment_command
|
611
|
-
@increment_command ||= IncrementCommand.new
|
612
|
-
end
|
613
|
-
|
614
|
-
def process i
|
615
|
-
addend.times { i = increment_command.call(i).value }
|
616
|
-
|
617
|
-
i
|
618
|
-
end
|
619
|
-
end
|
620
|
-
|
621
|
-
add_two_command = AddCommand.new(2)
|
622
|
-
add_two_command.call(0).value #=> 2
|
623
|
-
add_two_command.call(1).value #=> 3
|
624
|
-
add_two_command.call(8).value #=> 10
|
625
|
-
```
|
626
|
-
|
627
|
-
You can achieve even more powerful composition by passing in a command as an argument to a method, or by creating a method that returns a command.
|
628
|
-
|
629
|
-
##### Commands As Arguments
|
630
|
-
|
631
|
-
Since commands are objects, they can be passed in as arguments to a method or to another command. For example, consider a command that calls another command a given number of times:
|
632
|
-
|
633
|
-
```ruby
|
634
|
-
class RepeatCommand
|
635
|
-
def initialize(count)
|
636
|
-
@count = count
|
637
|
-
end
|
638
|
-
|
639
|
-
private
|
640
|
-
|
641
|
-
def process(command)
|
642
|
-
@count.times { command.call }
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
greet_command = Cuprum::Command.new { puts 'Greetings, programs!' }
|
647
|
-
repeat_command = RepeatCommand.new(3)
|
648
|
-
repeat_command.call(greet_command) #=> prints 'Greetings, programs!' 3 times
|
649
|
-
```
|
650
|
-
|
651
|
-
This is an implementation of the Strategy pattern, which allows us to customize the behavior of a part of our system by passing in implementation code rather than burying conditionals in our logic.
|
652
|
-
|
653
|
-
Consider a more concrete example. Suppose we are running an online bookstore that sells both physuical and electronic books, and serves both domestic and international customers. Depending on what the customer ordered and where they live, our business logic for fulfilling an order will have different shipping instructions.
|
654
|
-
|
655
|
-
Traditionally this would be handled with a conditional inside the order fulfillment code, which adds complexity. However, we can use the Strategy pattern and pass in our shipping code as a command.
|
656
|
-
|
657
|
-
```ruby
|
658
|
-
class DeliverEbook < Cuprum::Command; end
|
659
|
-
|
660
|
-
class ShipDomestic < Cuprum::Command; end
|
661
|
-
|
662
|
-
class ShipInternational < Cuprum::Command; end
|
663
|
-
|
664
|
-
class FulfillOrder < Cuprum::Command
|
665
|
-
def initialize(delivery_command)
|
666
|
-
@delivery_command = delivery_command
|
667
|
-
end
|
668
|
-
|
669
|
-
private
|
670
|
-
|
671
|
-
def process(book:, user:)
|
672
|
-
# Here we will check inventory, process payments, and so on. The final step
|
673
|
-
# is actually delivering the book to the user:
|
674
|
-
delivery_command.call(book: book, user: user)
|
675
|
-
end
|
676
|
-
end
|
677
|
-
```
|
678
|
-
|
679
|
-
This pattern is also useful for testing. When writing specs for the FulfillOrder command, simply pass in a mock double as the delivery command. This removes any need to stub out the implementation of whatever shipping method is used (or worse, calls to external services).
|
680
|
-
|
681
|
-
##### Commands As Returned Values
|
682
|
-
|
683
|
-
We can also return commands as an object from a method call or from another command. One use case for this is the Abstract Factory pattern.
|
684
|
-
|
685
|
-
Consider our shipping example, above. The traditional way to generate a shipping command is to use an `if-then-else` or `case` construct, which would be embedded in whatever code is calling `FulfillOrder`. This adds complexity and increases the testing burden.
|
686
|
-
|
687
|
-
Instead, let's create a factory command. This command will take a user and a book, and will return the command used to ship that item.
|
688
|
-
|
689
|
-
```ruby
|
690
|
-
class ShippingMethod < Cuprum::Command
|
691
|
-
private
|
692
|
-
|
693
|
-
def process(book:, user:)
|
694
|
-
return DeliverEbook.new(user.email) if book.ebook?
|
695
|
-
|
696
|
-
return ShipDomestic.new(user.address) if user.address&.domestic?
|
697
|
-
|
698
|
-
return ShipInternational.new(user.address) if user.address&.international?
|
699
|
-
|
700
|
-
err = Cuprum::Error.new(message: 'user does not have a valid address')
|
701
|
-
|
702
|
-
failure(err)
|
703
|
-
end
|
704
|
-
end
|
705
|
-
```
|
706
|
-
|
707
|
-
Notice that our factory includes error handling - if the user does not have a valid address, that is handled immediately rather than when trying to ship the item.
|
708
|
-
|
709
|
-
The [Command Factory](#label-Command+Factories) defined by Cuprum is another example of using the Abstract Factory pattern to return command instances. One use case for a command factory would be defining CRUD operations for data records. Depending on the class or the type of record passed in, the factory could return a generic command or a specific command tied to that specific record type.
|
710
|
-
|
711
|
-
#### Command Steps
|
712
|
-
|
713
|
-
Separating out business logic into commands is a powerful tool, but it does come with some overhead, particularly when checking whether a result is passing, or when converting between results and values. When a process has many steps, each of which can fail or return a value, this can result in a lot of boilerplate.
|
714
|
-
|
715
|
-
The solution Cuprum provides is the `#step` method, which calls either a named method or a given block. If the result of the block or method is passing, then the `#step` method returns the value of the result.
|
716
|
-
|
717
|
-
```ruby
|
718
|
-
triple_command = Cuprum::Command.new { |i| success(3 * i) }
|
719
|
-
|
720
|
-
int = 2
|
721
|
-
int = step { triple_command.call(int) } #=> returns 6
|
722
|
-
int = step { triple_command.call(int) } #=> returns 18
|
723
|
-
```
|
724
|
-
|
725
|
-
Notice that in each step, we are returning the *value* of the result from `#step`, not the result itself. This means we do not need explicit calls to the `#value` method.
|
726
|
-
|
727
|
-
Of course, not all commands return a passing result. If the result of the block or method is failing, then `#step` will throw `:cuprum_failed_result` and the result, immediately halting the execution chain. If the `#step` method is used inside a command definition (or inside a `#steps` block; [see below](#label-Using+Steps+Outside+Of+Commands)), that symbol will be caught and the failing result returned by `#call`.
|
728
|
-
|
729
|
-
```ruby
|
730
|
-
divide_command = Cuprum::Command.new do |dividend, divisor|
|
731
|
-
return failure('divide by zero') if divisor.zero?
|
732
|
-
|
733
|
-
success(dividend / divisor)
|
734
|
-
end
|
735
|
-
|
736
|
-
value = step { divide_command.call(10, 5) } #=> returns 2
|
737
|
-
value = step { divide_command.call(2, 0) } #=> throws :cuprum_failed_result
|
738
|
-
```
|
739
|
-
|
740
|
-
Here, the `divide_command` can either return a passing result (if the divisor is not zero) or a failing result (if the divisor is zero). When wrapped in a `#step`, the failing result is then thrown, halting execution.
|
741
|
-
|
742
|
-
This is important when using a sequence of steps. Let's consider a case study - reserving a book from the library. This entails several steps, each of which could potentially fail:
|
743
|
-
|
744
|
-
- Validating that the user can reserve books. Maybe the user has too many unpaid fines.
|
745
|
-
- Finding the requested book in the library system. Maybe the requested title isn't in the system.
|
746
|
-
- Placing a reservation on the book. Maybe there are no copies of the book available to reserve.
|
747
|
-
|
748
|
-
Using `#step`, as soon as one of the subtasks fails then the command will immediately return the failed value. This prevents us from hitting later subtasks with invalid data, it returns the actual failing result for analytics and for displaying a useful error message to the user, and it avoids the overhead (and the boilerplate) of exception-based failure handling.
|
749
|
-
|
750
|
-
```ruby
|
751
|
-
class CheckUserStatus < Cuprum::Command; end
|
752
|
-
|
753
|
-
class CreateBookReservation < Cuprum::Command; end
|
754
|
-
|
755
|
-
class FindBookByTitle < Cuprum::Command; end
|
756
|
-
|
757
|
-
class ReserveBookByTitle < Cuprum::Command
|
758
|
-
private
|
759
|
-
|
760
|
-
def process(title:, user:)
|
761
|
-
# If CheckUserStatus fails, #process will immediately return that result.
|
762
|
-
# For this step, we already have the user, so we don't need to use the
|
763
|
-
# result value.
|
764
|
-
step { CheckUserStatus.new.call(user) }
|
765
|
-
|
766
|
-
# Here, we are looking up the requested title. In this case, we will need
|
767
|
-
# the book object, so we save it as a variable. Notice that we don't need
|
768
|
-
# an explicit #value call - #step handles that for us.
|
769
|
-
book = step { FindBookByTitle.new.call(title) }
|
770
|
-
|
771
|
-
# Finally, we want to reserve the book. Since this is the last subtask, we
|
772
|
-
# don't strictly need to use #step. However, it's good practice, especially
|
773
|
-
# if we might need to add more steps to the command in the future.
|
774
|
-
step { CreateBookReservation.new.call(book: book, user: user) }
|
775
|
-
end
|
776
|
-
end
|
777
|
-
```
|
778
|
-
|
779
|
-
First, our user may not have borrowing privileges. In this case, `CheckUserStatus` will fail, and neither of the subsequent steps will be called. The `#call` method will return the failing result from `CheckUserStatus`.
|
780
|
-
|
781
|
-
```ruby
|
782
|
-
result = ReserveBookByTitle.new.call(
|
783
|
-
title: 'The C Programming Language',
|
784
|
-
user: 'Ed Dillinger'
|
785
|
-
)
|
786
|
-
result.class #=> Cuprum::Result
|
787
|
-
result.success? #=> false
|
788
|
-
result.error #=> 'not authorized to reserve book'
|
789
|
-
```
|
790
|
-
|
791
|
-
Second, our user may be valid but our requested title may not exist in the system. In this case, `FindBookByTitle` will fail, and the final step will not be called. The `#call` method will return the failing result from `FindBookByTitle`.
|
792
|
-
|
793
|
-
```ruby
|
794
|
-
result = ReserveBookByTitle.new.call(
|
795
|
-
title: 'Using GOTO For Fun And Profit',
|
796
|
-
user: 'Alan Bradley'
|
797
|
-
)
|
798
|
-
result.class #=> Cuprum::Result
|
799
|
-
result.success? #=> false
|
800
|
-
result.error #=> 'title not found'
|
801
|
-
```
|
802
|
-
|
803
|
-
Third, our user and book may be valid, but all of the copies are checked out. In this case, each of the steps will be called, and the `#call` method will return the failing result from `CreateBookReservation`.
|
804
|
-
|
805
|
-
```ruby
|
806
|
-
result = ReserveBookByTitle.new.call(
|
807
|
-
title: 'Design Patterns: Elements of Reusable Object-Oriented Software',
|
808
|
-
user: 'Alan Bradley'
|
809
|
-
)
|
810
|
-
result.class #=> Cuprum::Result
|
811
|
-
result.success? #=> false
|
812
|
-
result.error #=> 'no copies available'
|
813
|
-
```
|
814
|
-
|
815
|
-
Finally, if each of the steps succeeds, the `#call` method will return the result of the final step.
|
816
|
-
|
817
|
-
```ruby
|
818
|
-
result = ReserveBookByTitle.new.call(
|
819
|
-
title: 'The C Programming Language',
|
820
|
-
user: 'Alan Bradley'
|
821
|
-
)
|
822
|
-
result.class #=> Cuprum::Result
|
823
|
-
result.success? #=> true
|
824
|
-
result.value #=> an instance of BookReservation
|
825
|
-
```
|
826
|
-
|
827
|
-
##### Using Steps Outside Of Commands
|
828
|
-
|
829
|
-
Steps can also be used outside of a command. For example, a controller action might define a sequence of steps to run when the corresponding endpoint is called.
|
830
|
-
|
831
|
-
To use steps outside of a command, include the `Cuprum::Steps` module. Then, each sequence of steps should be wrapped in a `#steps` block as follows:
|
832
|
-
|
833
|
-
```ruby
|
834
|
-
steps do
|
835
|
-
step { check_something }
|
836
|
-
|
837
|
-
obj = step { find_something }
|
838
|
-
|
839
|
-
step :do_something, with: obj
|
840
|
-
end
|
841
|
-
```
|
842
|
-
|
843
|
-
Each step will be executed in sequence until a failing result is returned by the block or method. The `#steps` block will return that failing result. If no step returns a failing result, then the return value of the block will be wrapped in a result and returned by `#steps`.
|
844
|
-
|
845
|
-
Let's consider the example of a controller action for creating a new resource. This would have several steps, each of which can fail:
|
846
|
-
|
847
|
-
- First, we build a new instance of the resource with the provided attributes. This can fail if the attributes are incompatible with the resource, e.g. with extra attributes not included in the resource's table columns.
|
848
|
-
- Second, we run validations on the resource itself. This can fail if the attributes do not match the expected format.
|
849
|
-
- Finally, we persist the resource to the database. This can fail if the record violates any database constraints, or if the database itself is unavailable.
|
850
|
-
|
851
|
-
```ruby
|
852
|
-
class BooksController
|
853
|
-
include Cuprum::Steps
|
854
|
-
|
855
|
-
def create
|
856
|
-
attributes = params[:books]
|
857
|
-
result = steps do
|
858
|
-
@book = step :build_book, attributes
|
859
|
-
|
860
|
-
step :run_validations, @book
|
861
|
-
|
862
|
-
step :persist_book, book
|
863
|
-
end
|
864
|
-
|
865
|
-
result.success ? redirect_to(@book) : render(:edit)
|
866
|
-
end
|
867
|
-
|
868
|
-
private
|
869
|
-
|
870
|
-
def build_book(attributes)
|
871
|
-
success(Book.new(attributes))
|
872
|
-
rescue InvalidAttributes
|
873
|
-
failure('attributes are invalid')
|
874
|
-
end
|
875
|
-
|
876
|
-
def persist_book(book)
|
877
|
-
book.save ? success(book) : failure('unable to persist book')
|
878
|
-
end
|
879
|
-
|
880
|
-
def run_validations(book)
|
881
|
-
book.valid? ? success : failure('book is invalid')
|
882
|
-
end
|
883
|
-
end
|
884
|
-
```
|
885
|
-
|
886
|
-
A few things to note about this example. First, we have a couple of examples of wrapping existing code in a result, both by rescuing exceptions (in `#build_book`) or by checking a returned status (in `#persist_book`). Second, note that each of our helper methods can be reused in other controller actions. For even more encapsulation and reusability, the next step might be to convert those methods to commands of their own.
|
887
|
-
|
888
|
-
You can define even more complex logic by defining multiple `#steps` blocks. Each block represents a series of tasks that will terminate on the first failure. Steps blocks can even be nested in one another, or inside a `#process` method.
|
889
|
-
|
890
|
-
#### Handling Exceptions
|
891
|
-
|
892
|
-
require 'cuprum/exception_handling'
|
893
|
-
|
894
|
-
Cuprum defines a utility module to rescue uncaught exceptions when calling a command.
|
895
|
-
|
896
|
-
```ruby
|
897
|
-
class UnsafeCommand < Cuprum::Command
|
898
|
-
private
|
899
|
-
|
900
|
-
def process
|
901
|
-
raise 'Something went wrong.'
|
902
|
-
end
|
903
|
-
end
|
904
|
-
|
905
|
-
class SafeCommand < UnsafeCommand
|
906
|
-
include Cuprum::ExceptionHandling
|
907
|
-
end
|
908
|
-
|
909
|
-
UnsafeCommand.new.call
|
910
|
-
#=> raises a StandardError
|
911
|
-
|
912
|
-
result = SafeCommand.new.call
|
913
|
-
#=> a Cuprum::Result
|
914
|
-
result.error
|
915
|
-
#=> a Cuprum::Errors::UncaughtException error.
|
916
|
-
result.error.message
|
917
|
-
#=> 'uncaught exception in SafeCommand -' \
|
918
|
-
' StandardError: Something went wrong.'
|
919
|
-
```
|
920
|
-
|
921
|
-
Exception handling is *not* included by default - add `include Cuprum::ExceptionHandling` to your command classes to use this feature.
|
922
|
-
|
923
|
-
### Results
|
924
|
-
|
925
|
-
require 'cuprum'
|
926
|
-
|
927
|
-
A `Cuprum::Result` is a data object that encapsulates the result of calling a Cuprum command. Each result has a `#value`, an `#error` object (defaults to `nil`), and a `#status` (either `:success` or `:failure`, and accessible via the `#success?` and `#failure?` predicates).
|
928
|
-
|
929
|
-
```ruby
|
930
|
-
result = Cuprum::Result.new
|
931
|
-
|
932
|
-
result.value #=> nil
|
933
|
-
result.error #=> nil
|
934
|
-
result.status #=> :success
|
935
|
-
result.success? #=> true
|
936
|
-
result.failure? #=> true
|
937
|
-
```
|
938
|
-
|
939
|
-
Creating a result with a value stores the value.
|
940
|
-
|
941
|
-
```ruby
|
942
|
-
value = 'A result value'.freeze
|
943
|
-
result = Cuprum::Result.new(value: value)
|
944
|
-
|
945
|
-
result.value #=> 'A result value'
|
946
|
-
result.error #=> nil
|
947
|
-
result.status #=> :success
|
948
|
-
result.success? #=> true
|
949
|
-
result.failure? #=> false
|
950
|
-
```
|
951
|
-
|
952
|
-
Creating a Result with an error stores the error and sets the status to `:failure`.
|
953
|
-
|
954
|
-
```ruby
|
955
|
-
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
|
956
|
-
result = Cuprum::Result.new(error: error)
|
957
|
-
result.value #=> nil
|
958
|
-
result.error #=> Error with message "I'm sorry, something went wrong."
|
959
|
-
result.status #=> :failure
|
960
|
-
result.success? #=> false
|
961
|
-
result.failure? #=> true
|
962
|
-
```
|
963
|
-
|
964
|
-
Although using a `Cuprum::Error` instance as the `:error` is recommended, it is not required. You can use a custom error object, or just a string message.
|
965
|
-
|
966
|
-
```ruby
|
967
|
-
result = Cuprum::Result.new(error: "I'm sorry, something went wrong.")
|
968
|
-
result.value #=> nil
|
969
|
-
result.error #=> "I'm sorry, something went wrong."
|
970
|
-
result.status #=> :failure
|
971
|
-
result.success? #=> false
|
972
|
-
result.failure? #=> true
|
973
|
-
```
|
974
|
-
|
975
|
-
Finally, the status can be overridden via the `:status` keyword.
|
976
|
-
|
977
|
-
```ruby
|
978
|
-
result = Cuprum::Result.new(status: :failure)
|
979
|
-
result.error #=> nil
|
980
|
-
result.status #=> :failure
|
981
|
-
result.success? #=> false
|
982
|
-
result.failure? #=> true
|
983
|
-
|
984
|
-
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
|
985
|
-
result = Cuprum::Result.new(error: error, status: :success)
|
986
|
-
result.error #=> Error with message "I'm sorry, something went wrong."
|
987
|
-
result.status #=> :success
|
988
|
-
result.success? #=> true
|
989
|
-
result.failure? #=> false
|
990
|
-
```
|
991
|
-
|
992
|
-
### Errors
|
993
|
-
|
994
|
-
require 'cuprum/error'
|
995
|
-
|
996
|
-
A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property which defaults to nil. Each Error also has a `#type` property which is determined by the Error class or subclass, although it can be overridden by passing a `:type` parameter to the constructor.
|
997
|
-
|
998
|
-
```ruby
|
999
|
-
error = Cuprum::Error.new
|
1000
|
-
error.message => # nil
|
1001
|
-
error.type => 'cuprum.error'
|
1002
|
-
|
1003
|
-
error = Cuprum::Error.new(message: 'Something went wrong.')
|
1004
|
-
error.message => # 'Something went wrong.'
|
1005
|
-
|
1006
|
-
error = Cuprum::Error.new(type: 'example.custom_type')
|
1007
|
-
error.type => 'example.custom_type'
|
1008
|
-
```
|
1009
|
-
|
1010
|
-
Each application should define its own failure states as errors. For example, a typical web application might define the following errors:
|
1011
|
-
|
1012
|
-
```ruby
|
1013
|
-
class NotFoundError < Cuprum::Error
|
1014
|
-
TYPE = 'example.errors.not_found'
|
1015
|
-
|
1016
|
-
def initialize(resource:, resource_id:)
|
1017
|
-
@resource = resource
|
1018
|
-
@resource_id = resource_id
|
1019
|
-
|
1020
|
-
super(
|
1021
|
-
message: "#{resource} not found with id #{resource_id}",
|
1022
|
-
resource: resource,
|
1023
|
-
resource_id: resource_id
|
1024
|
-
)
|
1025
|
-
end
|
1026
|
-
|
1027
|
-
attr_reader :resource, :resource_id
|
1028
|
-
end
|
1029
|
-
|
1030
|
-
class ValidationError < Cuprum::Error
|
1031
|
-
TYPE = 'example.errors.validation'
|
1032
|
-
|
1033
|
-
def initialize(resource:, errors:)
|
1034
|
-
@resource = resource
|
1035
|
-
@errors = errors
|
1036
|
-
|
1037
|
-
super(
|
1038
|
-
errors: errors,
|
1039
|
-
message: "#{resource} was invalid",
|
1040
|
-
resource: resource
|
1041
|
-
)
|
1042
|
-
end
|
1043
|
-
|
1044
|
-
attr_reader :resource, :errors
|
1045
|
-
end
|
1046
|
-
```
|
1047
|
-
|
1048
|
-
It is optional but recommended to use a `Cuprum::Error` when returning a failed result from a command.
|
1049
|
-
|
1050
|
-
#### Comparing Errors
|
1051
|
-
|
1052
|
-
There are circumstances when it is useful to compare Error objects, such as when writing tests to specify the failure states of a command. To accommodate this, you can pass additional properties to `Cuprum::Error.new` (or to `super` when defining a subclass). These "comparable properties", plus the type and message (if any), are used to compare the errors.
|
1053
|
-
|
1054
|
-
An instance of `Cuprum::Error` is equal to another (using the `#==` equality comparison) if and only if the two errors have the same `class` and the two errors have the same comparable properties.
|
1055
|
-
|
1056
|
-
```ruby
|
1057
|
-
red = Cuprum::Error.new(message: 'wrong color', color: 'red')
|
1058
|
-
blue = Cuprum::Error.new(message: 'wrong color', color: 'blue')
|
1059
|
-
crimson = Cuprum::Error.new(message: 'wrong color', color: 'red')
|
1060
|
-
|
1061
|
-
red == blue
|
1062
|
-
#=> false
|
1063
|
-
|
1064
|
-
red == crimson
|
1065
|
-
#=> true
|
1066
|
-
```
|
1067
|
-
|
1068
|
-
This can be particularly important when defining Error subclasses. By passing the constructor parameters to `super`, below, we will be able to compare different instances of the `NotFoundError`. The errors will only be equal if they have the same message, resource, and resource_id properties.
|
1069
|
-
|
1070
|
-
```ruby
|
1071
|
-
class NotFoundError < Cuprum::Error
|
1072
|
-
def initialize(resource:, resource_id:)
|
1073
|
-
@resource = resource
|
1074
|
-
@resource_id = resource_id
|
1075
|
-
|
1076
|
-
super(
|
1077
|
-
message: "#{resource} not found with id #{resource_id}",
|
1078
|
-
resource: resource,
|
1079
|
-
resource_id: resource_id,
|
1080
|
-
)
|
1081
|
-
end
|
1082
|
-
|
1083
|
-
attr_reader :resource, :resource_id
|
1084
|
-
end
|
1085
|
-
```
|
1086
|
-
|
1087
|
-
Finally, by overriding the `#comparable_properties` method, you can customize how Error instances are compared.
|
1088
|
-
|
1089
|
-
```ruby
|
1090
|
-
class WrongColorError < Cuprum::Error
|
1091
|
-
def initialize(color:, shape:)
|
1092
|
-
super(message: "the #{shape} is the wrong color")
|
1093
|
-
|
1094
|
-
@color = color
|
1095
|
-
@shape = shape
|
1096
|
-
end
|
1097
|
-
|
1098
|
-
attr_reader :color
|
1099
|
-
|
1100
|
-
protected
|
1101
|
-
|
1102
|
-
def comparable_properties
|
1103
|
-
{ color: color }
|
1104
|
-
end
|
1105
|
-
end
|
1106
|
-
```
|
1107
|
-
|
1108
|
-
#### Serializing Errors
|
1109
|
-
|
1110
|
-
Some use cases require serializing error objects - for example, rendering an error response as JSON. To handle this, `Cuprum::Error` defines an `#as_json` method, which generates a representation of the error as a `Hash` with `String` keys. By default, this includes the `#type` and `#message` (if any) as well as an empty `:data` Hash.
|
1111
|
-
|
1112
|
-
Subclasses can override this behavior to include additional information in the `:data` Hash, which should always use `String` keys and have values composed of basic types and data structures. For example, if an error is passed a `Class`, consider serializing the name of the class to `:data`.
|
1113
|
-
|
1114
|
-
```ruby
|
1115
|
-
error = Cuprum::Error.new
|
1116
|
-
error.as_json #=> { data: {}, message: nil, type: 'cuprum.error' }
|
1117
|
-
|
1118
|
-
error = Cuprum::Error.new(message: 'Something went wrong.')
|
1119
|
-
error.as_json #=> { data: {}, message: 'Something went wrong.', type: 'cuprum.error' }
|
1120
|
-
|
1121
|
-
error = Cuprum::Error.new(type: 'example.custom_error')
|
1122
|
-
error.as_json #=> { data: {}, message: nil, type: 'example.custom_error' }
|
1123
|
-
|
1124
|
-
class ModuleError < Cuprum::Error
|
1125
|
-
TYPE = 'example.module_error'
|
1126
|
-
|
1127
|
-
def initialize(actual:)
|
1128
|
-
@actual = actual
|
1129
|
-
message = "Expected a Module, but #{actual.name} is a Class"
|
1130
|
-
|
1131
|
-
super(actual: actual, message: message)
|
1132
|
-
end
|
1133
|
-
|
1134
|
-
attr_reader :actual
|
1135
|
-
|
1136
|
-
private
|
1137
|
-
|
1138
|
-
def as_json_data
|
1139
|
-
{ actual: actual.name }
|
1140
|
-
end
|
1141
|
-
end
|
1142
|
-
|
1143
|
-
error = ModuleError.new(actual: String)
|
1144
|
-
error.as_json #=>
|
1145
|
-
# {
|
1146
|
-
# data: { actual: 'String' },
|
1147
|
-
# message: 'Expected a Module, but String is a Class',
|
1148
|
-
# type: 'example.module_error'
|
1149
|
-
# }
|
1150
|
-
```
|
1151
|
-
|
1152
|
-
**Important Note:** Be careful when serializing error data - this may expose sensitive information or internal details about your system that you don't want to display to users. Recommended practice is to have a whitelist of serializable errors; all other errors will display a generic error message instead.
|
1153
|
-
|
1154
|
-
### Middleware
|
1155
|
-
|
1156
|
-
```ruby
|
1157
|
-
require 'cuprum/middleware'
|
1158
|
-
```
|
1159
|
-
|
1160
|
-
A middleware command wraps the execution of another command, allowing the developer to compose functionality without an explicit wrapper command. Because the middleware is responsible for calling the wrapped command, it has control over when that command is called, with what parameters, and how the command result is handled.
|
1161
|
-
|
1162
|
-
To use middleware, start by defining a middleware command. This can either be a class that includes Cuprum::Middleware, or a command instance that extends Cuprum::Middleware. Each middleware command's #process method takes as its first argument the wrapped command. By convention, any additional arguments and any keywords or a block are passed to the wrapped command, but some middleware will override ths behavior.
|
1163
|
-
|
1164
|
-
```ruby
|
1165
|
-
class ExampleCommand < Cuprum::Command
|
1166
|
-
private def process(**options)
|
1167
|
-
return failure(options[:error]) if options[:error]
|
1168
|
-
|
1169
|
-
"Options: #{options.inspect}"
|
1170
|
-
end
|
1171
|
-
end
|
1172
|
-
|
1173
|
-
class LoggingMiddleware < Cuprum::Command
|
1174
|
-
include Cuprum::Middleware
|
1175
|
-
|
1176
|
-
# The middleware injects a logging step before the wrapped command is
|
1177
|
-
# called. Notice that this middleware is generic, and can be used with
|
1178
|
-
# virtually any other command.
|
1179
|
-
private def process(next_command, *args, **kwargs)
|
1180
|
-
Logger.info("Calling command #{next_command.class}")
|
1181
|
-
|
1182
|
-
super
|
1183
|
-
end
|
1184
|
-
end
|
1185
|
-
|
1186
|
-
command = Command.new { |**opts| "Called with #{opts.inspect}" }
|
1187
|
-
middleware = LoggingMiddleware.new
|
1188
|
-
result = middleware.call(command, { id: 0 })
|
1189
|
-
#=> logs "Calling command ExampleCommand"
|
1190
|
-
result.value
|
1191
|
-
#=> "Options: { id: 0 }"
|
1192
|
-
```
|
1193
|
-
|
1194
|
-
When defining #process, make sure to either call super or call the wrapped command directly, unless the middleware is specifically intended not to call the wrapped command under those circumstances.
|
1195
|
-
|
1196
|
-
Middleware is powerful because it allows the developer to manipulate the parameters passed to a command, add handling to a result, or even intercept or override the command execution. These are some of the possible use cases for middleware:
|
1197
|
-
|
1198
|
-
- Injecting code before or after a command.
|
1199
|
-
- Changing the parameters passed to a command.
|
1200
|
-
- Adding behavior based on the command result.
|
1201
|
-
- Overriding the command behavior based on the parameters.
|
1202
|
-
|
1203
|
-
```ruby
|
1204
|
-
class AuthenticationMiddleware < Cuprum::Command
|
1205
|
-
include Cuprum::Middleware
|
1206
|
-
|
1207
|
-
# The middleware finds the current user based on the given keywords. If
|
1208
|
-
# a valid user is found, the user is then passed on to the command.
|
1209
|
-
# If a user is not found, then the middleware will immediately halt (due
|
1210
|
-
# to #step) and return the failing result from the authentication
|
1211
|
-
# command.
|
1212
|
-
private def process(next_command, *args, **kwargs)
|
1213
|
-
current_user = step { AuthenticateUser.new.call(**kwargs) }
|
1214
|
-
|
1215
|
-
super(next_command, *args, current_user: current_user, **kwargs)
|
1216
|
-
end
|
1217
|
-
end
|
1218
|
-
```
|
1219
|
-
|
1220
|
-
Middleware is loosely coupled, meaning that one middleware command can wrap any number of other commands. One example would be logging middleware, which could record when a command is called and with what parameters. For a more involved example, consider authorization in a web application. If individual actions are defined as commands, then a single authorization middleware class could wrap each individual action, reducing both the testing burden and the amount of code that must be maintained.
|
1221
|
-
|
1222
|
-
### Operations
|
1223
|
-
|
1224
|
-
require 'cuprum'
|
1225
|
-
|
1226
|
-
An Operation is like a Command, but with two key differences. First, an Operation retains a reference to the result object from the most recent time the operation was called, and delegates the methods defined by `Cuprum::Result` to the most recent result. This allows a called Operation to replace a `Cuprum::Result` in any code that expects or returns a result. Second, the `#call` method returns the operation instance, rather than the result itself.
|
1227
|
-
|
1228
|
-
These two features allow developers to simplify logic around calling and using the results of operations, and reduce the need for boilerplate code (particularly when using an operation as part of an existing framework, such as inside of an asynchronous worker or a Rails controller action).
|
1229
|
-
|
1230
|
-
```ruby
|
1231
|
-
class CreateBookOperation < Cuprum::Operation
|
1232
|
-
def process
|
1233
|
-
# Implementation here.
|
1234
|
-
end
|
1235
|
-
end
|
1236
|
-
|
1237
|
-
# Defining a controller action using an operation.
|
1238
|
-
def create
|
1239
|
-
operation = CreateBookOperation.new.call(book_params)
|
1240
|
-
|
1241
|
-
if operation.success?
|
1242
|
-
redirect_to(operation.value)
|
1243
|
-
else
|
1244
|
-
@book = operation.value
|
1245
|
-
|
1246
|
-
render :new
|
1247
|
-
end
|
1248
|
-
end
|
1249
|
-
```
|
1250
|
-
|
1251
|
-
Like a Command, an Operation can be defined directly by passing an implementation block to the constructor or by creating a subclass that overwrites the #process method.
|
1252
|
-
|
1253
|
-
An operation inherits the `#call` method from Cuprum::Command (see above), and delegates the `#value`, `#error`, `#success?`, and `#failure` methods to the most recent result. If the operation has not been called, these methods will return default values.
|
1254
|
-
|
1255
|
-
#### The Operation Mixin
|
1256
|
-
|
1257
|
-
The implementation of `Cuprum::Operation` is defined by the `Cuprum::Operation::Mixin` module, which provides the methods defined above. Any command class or instance can be converted to an operation by including (for a class) or extending (for an instance) the operation mixin.
|
1258
|
-
|
1259
|
-
### Matchers
|
1260
|
-
|
1261
|
-
require 'cuprum/matcher'
|
1262
|
-
|
1263
|
-
A Matcher provides a simple DSL for defining behavior based on a Cuprum result object.
|
1264
|
-
|
1265
|
-
```ruby
|
1266
|
-
matcher = Cuprum::Matcher.new do
|
1267
|
-
match(:failure) { 'Something went wrong' }
|
1268
|
-
|
1269
|
-
match(:success) { 'Ok' }
|
1270
|
-
end
|
1271
|
-
|
1272
|
-
matcher.call(Cuprum::Result.new(status: :failure))
|
1273
|
-
#=> 'Something went wrong'
|
1274
|
-
|
1275
|
-
matcher.call(Cuprum::Result.new(status: :success))
|
1276
|
-
#=> 'Ok'
|
1277
|
-
```
|
1278
|
-
|
1279
|
-
First, the matcher defines possible matches using the `.match` method. This can either be called on a subclass of `Cuprum::Matcher` or by passing a block to the constructor, as above. Each match clause must have the matching status, and a block that is executed when a result matches that clause. The clause can also filter by the result value or error (see Matching Values And Errors, below).
|
1280
|
-
|
1281
|
-
Once the matcher has found a matching clause, it then calls the block in the clause definition. If the block accepts an argument, the result is passed to the block; otherwise, the block is called with no arguments. This allows the match clause to use the error or value of the result.
|
1282
|
-
|
1283
|
-
```ruby
|
1284
|
-
matcher = Cuprum::Matcher.new do
|
1285
|
-
match(:failure) { |result| result.error.message }
|
1286
|
-
end
|
1287
|
-
|
1288
|
-
error = Cuprum::Error.new(message: 'An error has occurred.')
|
1289
|
-
matcher.call(Cuprum::Result.new(error: error))
|
1290
|
-
#=> 'An error has occurred.'
|
1291
|
-
```
|
1292
|
-
|
1293
|
-
If the result does not match any of the clauses, a `Cuprum::Matching::NoMatchError` is raised.
|
1294
|
-
|
1295
|
-
```ruby
|
1296
|
-
matcher = Cuprum::Matcher.new do
|
1297
|
-
match(:success) { :ok }
|
1298
|
-
end
|
1299
|
-
|
1300
|
-
matcher.call(Cuprum::Result.new(status: :failure))
|
1301
|
-
#=> raises Cuprum::Matching::NoMatchError
|
1302
|
-
```
|
1303
|
-
|
1304
|
-
#### Matching Values And Errors
|
1305
|
-
|
1306
|
-
In addition to a status, match clauses can specify the type of the value or error of a matching result. The error or value must be a Class or Module, and the clause will then match only results whose error or value is an instance of the specified Class or Module (or a subclass of the Class).
|
1307
|
-
|
1308
|
-
```ruby
|
1309
|
-
class MagicSmokeError < Cuprum::Error; end
|
1310
|
-
|
1311
|
-
matcher = Cuprum::Matcher.new do
|
1312
|
-
match(:failure) { 'Something went wrong.' }
|
1313
|
-
|
1314
|
-
match(:failure, error: Cuprum::Error) do |result|
|
1315
|
-
"ERROR: #{result.error.message}"
|
1316
|
-
end
|
1317
|
-
|
1318
|
-
match(:failure, error: MagicSmokeError) do
|
1319
|
-
"PANIC: #{result.error.message}"
|
1320
|
-
end
|
1321
|
-
end
|
1322
|
-
|
1323
|
-
matcher.call(Cuprum::Result.new(status: :failure))
|
1324
|
-
#=> 'Something went wrong.'
|
1325
|
-
|
1326
|
-
error = Cuprum::Error.new(message: 'An error has occurred.')
|
1327
|
-
matcher.call(Cuprum::Result.new(error: error)
|
1328
|
-
#=> 'ERROR: An error has occurred.'
|
1329
|
-
|
1330
|
-
error = MagicSmokeError.new(message: 'The magic smoke is escaping.')
|
1331
|
-
matcher.call(Cuprum::Result.new(error: error))
|
1332
|
-
#=> 'PANIC: The magic smoke is escaping.'
|
1333
|
-
```
|
1334
|
-
|
1335
|
-
The matcher will always apply the most specific match clause. In the example above, the result with a `MagicSmokeError` matches all three clauses, but only the final clause is executed.
|
1336
|
-
|
1337
|
-
You can also specify the value of a matching result:
|
1338
|
-
|
1339
|
-
```ruby
|
1340
|
-
matcher = Cuprum::Matcher.new do
|
1341
|
-
match(:success, value: String) { 'a String' }
|
1342
|
-
|
1343
|
-
match(:success, value: Symbol) { 'a Symbol' }
|
1344
|
-
end
|
1345
|
-
|
1346
|
-
matcher.call(Cuprum::Result.new(value: 'Greetings, programs!'))
|
1347
|
-
#=> 'a String'
|
1348
|
-
|
1349
|
-
matcher.call(Cuprum::Result.new(value: :greetings_starfighter))
|
1350
|
-
#=> 'a Symbol'
|
1351
|
-
```
|
1352
|
-
|
1353
|
-
#### Using Matcher Classes
|
1354
|
-
|
1355
|
-
Matcher classes allow you to define custom behavior that can be called as part of the defined match clauses.
|
1356
|
-
|
1357
|
-
```ruby
|
1358
|
-
class LogMatcher < Cuprum::Matcher
|
1359
|
-
match(:failure) { |result| log(:error, result.error.message) }
|
1360
|
-
|
1361
|
-
match(:success) { log(:info, 'Ok') }
|
1362
|
-
|
1363
|
-
def log(level, message)
|
1364
|
-
puts "#{level.upcase}: #{message}"
|
1365
|
-
end
|
1366
|
-
end
|
1367
|
-
|
1368
|
-
matcher = LogMatcher.new
|
1369
|
-
matcher.call(Cuprum::Result.new(status: :success))
|
1370
|
-
#=> prints "INFO: Ok" to STDOUT
|
1371
|
-
```
|
1372
|
-
|
1373
|
-
Match clauses are also inherited by matcher subclasses. Inherited clauses are sorted the same as clauses defined on the matcher directly - the most specific clause is matched first, followed by less specific clauses and finally the generic clause (if any) for that result status.
|
1374
|
-
|
1375
|
-
```ruby
|
1376
|
-
class CustomLogMatcher < Cuprum::Matcher
|
1377
|
-
match(:failure, error: ReallyBadError) do |result|
|
1378
|
-
log(:fatal, result.error.message)
|
1379
|
-
end
|
1380
|
-
end
|
1381
|
-
|
1382
|
-
matcher = CustomLogMatcher.new
|
1383
|
-
result = Cuprum::Result.new(error: Cuprum::Error.new('Something went wrong.'))
|
1384
|
-
matcher.call(result)
|
1385
|
-
#=> prints "ERROR: Something went wrong." to STDOUT
|
1386
|
-
|
1387
|
-
result = Cuprum::Result.new(error: ReallyBadError.new('Computer on fire.'))
|
1388
|
-
matcher.call(result)
|
1389
|
-
#=> prints "FATAL: Computer on fire." to STDOUT
|
1390
|
-
```
|
1391
|
-
|
1392
|
-
#### Match Contexts
|
1393
|
-
|
1394
|
-
Match contexts provide an alternative to defining custom matcher classes - instead of defining custom behavior in the matcher itself, the match clauses can be executed in the context of another object.
|
1395
|
-
|
1396
|
-
```ruby
|
1397
|
-
class Inflector
|
1398
|
-
def capitalize(message)
|
1399
|
-
message.split(' ').map(&:capitalize).join(' ')
|
1400
|
-
end
|
1401
|
-
end
|
1402
|
-
|
1403
|
-
matcher = Cuprum::Matcher.new(inflector) do
|
1404
|
-
match(:success) { |result| capitalize(result.value) }
|
1405
|
-
end
|
1406
|
-
matcher.call(Cuprum::Result.new(value: 'greetings starfighter'))
|
1407
|
-
#=> 'Greetings Starfighter'
|
1408
|
-
```
|
1409
|
-
|
1410
|
-
For example, a controller in a web framework might need to define behavior for handling different success and error cases for business logic that is defined as Commands. The controller itself defines methods such as `#render` and `#redirect` - by creating a matcher using the controller as the match context, the matcher can call upon those methods to generate a response.
|
1411
|
-
|
1412
|
-
You can also call an existing matcher with a new context. The `#with_context` method returns a copy of the matcher with the given object set as the match context.
|
1413
|
-
|
1414
|
-
```ruby
|
1415
|
-
matcher = Cuprum::Matcher.new do
|
1416
|
-
match(:success) { |result| capitalize(result.value) }
|
1417
|
-
end
|
1418
|
-
matcher
|
1419
|
-
.with_context(inflector)
|
1420
|
-
.call(Cuprum::Result.new(value: 'greetings starfighter'))
|
1421
|
-
#=> 'Greetings Starfighter'
|
1422
|
-
```
|
1423
|
-
|
1424
|
-
#### Matcher Lists
|
1425
|
-
|
1426
|
-
Matcher lists handle matching a result against an ordered group of matchers.
|
1427
|
-
|
1428
|
-
When given a result, a matcher list will check for the most specific matching clause in each of the matchers. A clause matching both the value and error will match first, followed by a clause matching only the result value or error, and finally a clause matching only the result status will match.
|
1429
|
-
|
1430
|
-
If none of the matchers have a clause that matches the result, a `Cuprum::Matching::NoMatchError` will be raised.
|
1431
|
-
|
1432
|
-
```ruby
|
1433
|
-
generic_matcher = Cuprum::Matcher.new do
|
1434
|
-
match(:failure) { 'generic failure' }
|
1435
|
-
#
|
1436
|
-
match(:failure, error: CustomError) { 'custom failure' }
|
1437
|
-
end
|
1438
|
-
specific_matcher = Cuprum::Matcher.new do
|
1439
|
-
match(:failure, error: Cuprum::Error) { 'specific failure' }
|
1440
|
-
end
|
1441
|
-
matcher_list = Cuprum::MatcherList.new(
|
1442
|
-
[
|
1443
|
-
specific_matcher,
|
1444
|
-
generic_matcher
|
1445
|
-
]
|
1446
|
-
)
|
1447
|
-
|
1448
|
-
generic_matcher = Cuprum::Matcher.new do
|
1449
|
-
match(:failure) { 'generic failure' }
|
1450
|
-
|
1451
|
-
match(:failure, error: CustomError) { 'custom failure' }
|
1452
|
-
end
|
1453
|
-
specific_matcher = Cuprum::Matcher.new do
|
1454
|
-
match(:failure, error: Cuprum::Error) { 'specific failure' }
|
1455
|
-
end
|
1456
|
-
matcher_list = Cuprum::MatcherList.new(
|
1457
|
-
[
|
1458
|
-
specific_matcher,
|
1459
|
-
generic_matcher
|
1460
|
-
]
|
1461
|
-
)
|
1462
|
-
|
1463
|
-
# A failure without an error does not match the first matcher, so the
|
1464
|
-
# matcher list continues on to the next matcher in the list.
|
1465
|
-
result = Cuprum::Result.new(status: :failure)
|
1466
|
-
matcher_list.call(result)
|
1467
|
-
#=> 'generic failure'
|
1468
|
-
|
1469
|
-
# A failure with an error matches the first matcher.
|
1470
|
-
error = Cuprum::Error.new(message: 'Something went wrong.')
|
1471
|
-
result = Cuprum::Result.new(error: error)
|
1472
|
-
matcher_list.call(result)
|
1473
|
-
#=> 'specific failure'
|
1474
|
-
|
1475
|
-
# A failure with an error subclass still matches the first matcher, even
|
1476
|
-
# though the second matcher has a more exact match.
|
1477
|
-
error = CustomError.new(message: 'The magic smoke is escaping.')
|
1478
|
-
result = Cuprum::Result.new(error: error)
|
1479
|
-
matcher_list.call(result)
|
1480
|
-
#=> 'specific failure'
|
1481
|
-
```
|
1482
|
-
|
1483
|
-
One use case for matcher lists would be in defining hierarchies of classes or objects that have matching functionality. For example, a generic controller class might define default success and failure behavior, an included mixin might provide handling for a particular scope of errors, and a specific controller might override the default behavior for a given action. Using a matcher list allows each class or module to define its own behavior as independent matchers, which the matcher list then composes together.
|
1484
|
-
|
1485
|
-
### Command Factories
|
1486
|
-
|
1487
|
-
Commands are powerful and flexible objects, but they do have a few disadvantages compared to traditional service objects which allow the developer to group together related functionality and shared implementation details. To bridge this gap, Cuprum implements the CommandFactory class. Command factories provide a DSL to quickly group together related commands and create context-specific command classes or instances.
|
1488
|
-
|
1489
|
-
For example, consider a basic entity command:
|
1490
|
-
|
1491
|
-
```ruby
|
1492
|
-
class Book
|
1493
|
-
def initialize(attributes = {})
|
1494
|
-
@title = attributes[:title]
|
1495
|
-
@author = attributes[:author]
|
1496
|
-
end
|
1497
|
-
|
1498
|
-
attr_accessor :author, :publisher, :title
|
1499
|
-
end
|
1500
|
-
|
1501
|
-
class BuildBookCommand < Cuprum::Command
|
1502
|
-
private
|
1503
|
-
|
1504
|
-
def process(attributes = {})
|
1505
|
-
Book.new(attributes)
|
1506
|
-
end
|
1507
|
-
end
|
1508
|
-
|
1509
|
-
class BookFactory < Cuprum::CommandFactory
|
1510
|
-
command :build, BuildBookCommand
|
1511
|
-
end
|
1512
|
-
```
|
1513
|
-
|
1514
|
-
Our factory is defined by subclassing `Cuprum::CommandFactory`, and then we map the individual commands with the `::command` or `::command_class` class methods. In this case, we've defined a Book factory with the build command. The build command can be accessed on a factory instance in one of two ways.
|
1515
|
-
|
1516
|
-
First, the command class can be accessed directly as a constant on the factory instance.
|
1517
|
-
|
1518
|
-
```ruby
|
1519
|
-
factory = BookFactory.new
|
1520
|
-
factory::Build #=> BuildBookCommand
|
1521
|
-
```
|
1522
|
-
|
1523
|
-
Second, the factory instance now defines a `#build` method, which returns an instance of our defined command class. This command instance can be called like any command, or returned or passed around like any other object.
|
1524
|
-
|
1525
|
-
```ruby
|
1526
|
-
factory = BookFactory.new
|
1527
|
-
|
1528
|
-
attrs = { title: 'A Wizard of Earthsea', author: 'Ursula K. Le Guin' }
|
1529
|
-
command = factory.build() #=> an instance of BuildBookCommand
|
1530
|
-
result = command.call(attrs) #=> an instance of Cuprum::Result
|
1531
|
-
book = result.value #=> an instance of Book
|
1532
|
-
|
1533
|
-
book.title #=> 'A Wizard of Earthsea'
|
1534
|
-
book.author #=> 'Ursula K. Le Guin'
|
1535
|
-
book.publisher #=> nil
|
1536
|
-
```
|
1537
|
-
|
1538
|
-
#### The ::command Method And A Command Class
|
1539
|
-
|
1540
|
-
The first way to define a command for a factory is by calling the `::command` method and passing it the name of the command and a command class:
|
1541
|
-
|
1542
|
-
```ruby
|
1543
|
-
class BookFactory < Cuprum::CommandFactory
|
1544
|
-
command :build, BuildBookCommand
|
1545
|
-
end
|
1546
|
-
```
|
1547
|
-
|
1548
|
-
This makes the command class available on a factory instance as `::Build`, and generates the `#build` method which returns an instance of `BuildBookCommand`.
|
1549
|
-
|
1550
|
-
#### The ::command Method And A Block
|
1551
|
-
|
1552
|
-
By calling the `::command` method with a block, you can define a command with additional control over how the generated command. The block must return an instance of a subclass of Cuprum::Command.
|
1553
|
-
|
1554
|
-
```ruby
|
1555
|
-
class PublishBookCommand < Cuprum::Command
|
1556
|
-
def initialize(publisher:)
|
1557
|
-
@publisher = publisher
|
1558
|
-
end
|
1559
|
-
|
1560
|
-
attr_reader :publisher
|
1561
|
-
|
1562
|
-
private
|
1563
|
-
|
1564
|
-
def process(book)
|
1565
|
-
book.publisher = publisher
|
1566
|
-
|
1567
|
-
book
|
1568
|
-
end
|
1569
|
-
end
|
1570
|
-
|
1571
|
-
class BookFactory < Cuprum::CommandFactory
|
1572
|
-
command :publish do |publisher|
|
1573
|
-
PublishBookCommand.new(publisher: publisher)
|
1574
|
-
end
|
1575
|
-
end
|
1576
|
-
```
|
1577
|
-
|
1578
|
-
This defines the `#publish` method on an instance of the factory. The method takes one argument (the publisher), which is then passed on to the constructor for `PublishBookCommand` by our block. Finally, the block returns an instance of the publish command, which is then returned by `#publish`.
|
1579
|
-
|
1580
|
-
```ruby
|
1581
|
-
factory = BookFactory.new
|
1582
|
-
book = Book.new(title: 'The Tombs of Atuan', author: 'Ursula K. Le Guin')
|
1583
|
-
book.publisher #=> nil
|
1584
|
-
|
1585
|
-
command = factory.publish('Harper & Row') #=> an instance of PublishBookCommand
|
1586
|
-
result = command.call(book) #=> an instance of Cuprum::Result
|
1587
|
-
book.publisher #=> 'Harper & Row'
|
1588
|
-
```
|
1589
|
-
|
1590
|
-
Note that unlike when `::command` is called with a command class, calling `::command` with a block will not set a constant on the factory instance. In this case, trying to access the `PublishBookCommand` at `factory::Publish` will raise a `NameError`.
|
1591
|
-
|
1592
|
-
The block is evaluated in the context of the factory instance. This means that instance variables or methods are available to the block, allowing you to create commands with instance-specific configuration.
|
1593
|
-
|
1594
|
-
```ruby
|
1595
|
-
class PublishedBooksCommand < Cuprum::Command
|
1596
|
-
def initialize(collection = [])
|
1597
|
-
@collection = collection
|
1598
|
-
end
|
1599
|
-
|
1600
|
-
attr_reader :collection
|
1601
|
-
|
1602
|
-
private
|
1603
|
-
|
1604
|
-
def process
|
1605
|
-
books.reject { |book| book.publisher.nil? }
|
1606
|
-
end
|
1607
|
-
end
|
1608
|
-
|
1609
|
-
class BookFactory < Cuprum::CommandFactory
|
1610
|
-
def initialize(books)
|
1611
|
-
@books_collection = books
|
1612
|
-
end
|
1613
|
-
|
1614
|
-
attr_reader :books_collection
|
1615
|
-
|
1616
|
-
command :published do
|
1617
|
-
PublishedBooksCommand.new(books_collection)
|
1618
|
-
end
|
1619
|
-
end
|
1620
|
-
```
|
1621
|
-
|
1622
|
-
This defines the `#published` method on an instance of the factory. The method takes no arguments, but grabs the books collection from the factory instance. The block returns an instance of `PublishedBooksCommand`, which is then returned by `#published`.
|
1623
|
-
|
1624
|
-
```ruby
|
1625
|
-
books = [Book.new, Book.new(publisher: 'Baen'), Book.new(publisher: 'Tor')]
|
1626
|
-
factory = BookFactory.new(books)
|
1627
|
-
factory.books_collection #=> the books array
|
1628
|
-
|
1629
|
-
command = factory.published #=> an instance of PublishedBooksCommand
|
1630
|
-
result = command.call #=> an instance of Cuprum::Result
|
1631
|
-
ary = result.value #=> an array with the published books
|
1632
|
-
|
1633
|
-
ary.count #=> 2
|
1634
|
-
ary.any? { |book| book.publisher == 'Baen' } #=> true
|
1635
|
-
ary.any? { |book| book.publisher.nil? } #=> false
|
1636
|
-
```
|
1637
|
-
|
1638
|
-
Simple commands can be defined directly in the block, rather than referencing an existing command class:
|
1639
|
-
|
1640
|
-
```ruby
|
1641
|
-
class BookFactory < Cuprum::CommandFactory
|
1642
|
-
command :published_by_baen do
|
1643
|
-
Cuprum::Command.new do |books|
|
1644
|
-
books.select { |book| book.publisher == 'Baen' }
|
1645
|
-
end
|
1646
|
-
end
|
1647
|
-
end
|
1648
|
-
|
1649
|
-
books = [Book.new, Book.new(publisher: 'Baen'), Book.new(publisher: 'Tor')]
|
1650
|
-
factory = BookFactory.new(books)
|
1651
|
-
|
1652
|
-
command = factory.published_by_baen #=> an instance of the anonymous command
|
1653
|
-
result = command.call #=> an instance of Cuprum::Result
|
1654
|
-
ary = result.value #=> an array with the selected books
|
1655
|
-
|
1656
|
-
ary.count #=> 1
|
1657
|
-
```
|
1658
|
-
|
1659
|
-
#### The ::command_class Method
|
1660
|
-
|
1661
|
-
The final way to define a command for a factory is calling the `::command_class` method with the command name and a block. The block must return a subclass (not an instance) of Cuprum::Command. This offers a balance between flexibility and power.
|
1662
|
-
|
1663
|
-
```ruby
|
1664
|
-
class SelectByAuthorCommand < Cuprum::Command
|
1665
|
-
def initialize(author)
|
1666
|
-
@author = author
|
1667
|
-
end
|
1668
|
-
|
1669
|
-
attr_reader :author
|
1670
|
-
|
1671
|
-
private
|
1672
|
-
|
1673
|
-
def process(books)
|
1674
|
-
books.select { |book| book.author == author }
|
1675
|
-
end
|
1676
|
-
end
|
1677
|
-
|
1678
|
-
class BooksFactory < Cuprum::CommandFactory
|
1679
|
-
command_class :select_by_author do
|
1680
|
-
SelectByAuthorCommand
|
1681
|
-
end
|
1682
|
-
end
|
1683
|
-
```
|
1684
|
-
|
1685
|
-
The command class can be accessed directly as a constant on the factory instance:
|
1686
|
-
|
1687
|
-
```ruby
|
1688
|
-
factory = BookFactory.new
|
1689
|
-
factory::SelectByAuthor #=> SelectByAuthorCommand
|
1690
|
-
```
|
1691
|
-
|
1692
|
-
The factory instance now defines a `#select_by_author` method, which returns an instance of our defined command class. This command instance can be called like any command, or returned or passed around like any other object.
|
1693
|
-
|
1694
|
-
```ruby
|
1695
|
-
factory = BookFactory.new
|
1696
|
-
books = [
|
1697
|
-
Book.new,
|
1698
|
-
Book.new(author: 'Arthur C. Clarke'),
|
1699
|
-
Book.new(author: 'Ursula K. Le Guin')
|
1700
|
-
]
|
1701
|
-
|
1702
|
-
command = factory.select_by_author('Ursula K. Le Guin')
|
1703
|
-
#=> an instance of SelectByAuthorCommand
|
1704
|
-
command.author #=> 'Ursula K. Le Guin'
|
1705
|
-
|
1706
|
-
result = command.call(books) #=> an instance of Cuprum::Result
|
1707
|
-
ary = result.value #=> an array with the selected books
|
1708
|
-
|
1709
|
-
ary.count #=> 1
|
1710
|
-
ary.any? { |book| book.author == 'Ursula K. Le Guin' } #=> true
|
1711
|
-
ary.any? { |book| book.author == 'Arthur C. Clarke' } #=> false
|
1712
|
-
ary.any? { |book| book.author.nil? } #=> false
|
1713
|
-
```
|
1714
|
-
|
1715
|
-
The block is evaluated in the context of the factory instance. This means that instance variables or methods are available to the block, allowing you to create custom command subclasses with instance-specific configuration.
|
1716
|
-
|
1717
|
-
```ruby
|
1718
|
-
class SaveBookCommand < Cuprum::Command
|
1719
|
-
def initialize(collection = [])
|
1720
|
-
@collection = collection
|
1721
|
-
end
|
1722
|
-
|
1723
|
-
attr_reader :collection
|
1724
|
-
|
1725
|
-
private
|
1726
|
-
|
1727
|
-
def process(book)
|
1728
|
-
books << book
|
1729
|
-
|
1730
|
-
book
|
1731
|
-
end
|
1732
|
-
end
|
1733
|
-
|
1734
|
-
class BookFactory < Cuprum::CommandFactory
|
1735
|
-
command :save do
|
1736
|
-
collection = self.books_collection
|
1737
|
-
|
1738
|
-
Class.new(SaveBookCommand) do
|
1739
|
-
define_method(:initialize) do
|
1740
|
-
@books = collection
|
1741
|
-
end
|
1742
|
-
end
|
1743
|
-
end
|
1744
|
-
|
1745
|
-
def initialize(books)
|
1746
|
-
@books_collection = books
|
1747
|
-
end
|
1748
|
-
|
1749
|
-
attr_reader :books_collection
|
1750
|
-
end
|
1751
|
-
```
|
1752
|
-
|
1753
|
-
The custom command subclass can be accessed directly as a constant on the factory instance:
|
1754
|
-
|
1755
|
-
```ruby
|
1756
|
-
books = [Book.new, Book.new, Book.new]
|
1757
|
-
factory = BookFactory.new(books)
|
1758
|
-
factory::Save #=> a subclass of SaveBookCommand
|
1759
|
-
|
1760
|
-
command = factory::Save.new # an instance of the command subclass
|
1761
|
-
command.collection #=> the books array
|
1762
|
-
command.collection.count #=> 3
|
1763
|
-
```
|
1764
|
-
|
1765
|
-
The factory instance now defines a `#save` method, which returns an instance of our custom command subclass. This command instance can be called like any command, or returned or passed around like any other object.
|
1766
|
-
|
1767
|
-
The custom command subclass can be accessed directly as a constant on the factory instance:
|
1768
|
-
|
1769
|
-
```ruby
|
1770
|
-
books = [Book.new, Book.new, Book.new]
|
1771
|
-
factory = BookFactory.new(books)
|
1772
|
-
command = factory.save # an instance of the command subclass
|
1773
|
-
command.collection #=> the books array
|
1774
|
-
command.collection.count #=> 3
|
1775
|
-
|
1776
|
-
book = Book.new(title: 'The Farthest Shore', author: 'Ursula K. Le Guin')
|
1777
|
-
result = command.call(book) #=> an instance of Cuprum::Result
|
1778
|
-
|
1779
|
-
books.count #=> 4
|
1780
|
-
books.include?(book) #=> true
|
1781
|
-
```
|
1782
|
-
|
1783
|
-
### Built In Commands
|
1784
|
-
|
1785
|
-
Cuprum includes a small number of predefined commands and their equivalent operations.
|
1786
|
-
|
1787
|
-
#### IdentityCommand
|
1788
|
-
|
1789
|
-
require 'cuprum/built_in/identity_command'
|
1790
|
-
|
1791
|
-
A pregenerated command that returns the value or result with which it was called.
|
1792
|
-
|
1793
|
-
```ruby
|
1794
|
-
command = Cuprum::BuiltIn::IdentityCommand.new
|
1795
|
-
result = command.call('expected value')
|
1796
|
-
result.value #=> 'expected value'
|
1797
|
-
result.success? #=> true
|
1798
|
-
```
|
1799
|
-
|
1800
|
-
#### IdentityOperation
|
1801
|
-
|
1802
|
-
require 'cuprum/built_in/identity_operation'
|
1803
|
-
|
1804
|
-
A pregenerated operation that sets its result to the value or result with which it was called.
|
1805
|
-
|
1806
|
-
```ruby
|
1807
|
-
operation = Cuprum::BuiltIn::IdentityOperation.new.call('expected value')
|
1808
|
-
operation.value #=> 'expected value'
|
1809
|
-
operation.success? #=> true
|
1810
|
-
```
|
1811
|
-
|
1812
|
-
#### NullCommand
|
1813
|
-
|
1814
|
-
require 'cuprum/built_in/null_command'
|
1815
|
-
|
1816
|
-
A pregenerated command that does nothing when called. Accepts any arguments.
|
1817
|
-
|
1818
|
-
```ruby
|
1819
|
-
command = Cuprum::BuiltIn::NullCommand.new
|
1820
|
-
result = command.call
|
1821
|
-
result.value #=> nil
|
1822
|
-
result.success? #=> true
|
1823
|
-
```
|
1824
|
-
|
1825
|
-
#### NullOperation
|
1826
|
-
|
1827
|
-
require 'cuprum/built_in/null_operation'
|
1828
|
-
|
1829
|
-
A pregenerated operation that does nothing when called. Accepts any arguments.
|
1830
|
-
|
1831
|
-
```ruby
|
1832
|
-
operation = Cuprum::BuiltIn::NullOperation.new.call
|
1833
|
-
operation.value #=> nil
|
1834
|
-
operation.success? #=> true
|
1835
|
-
```
|
365
|
+
## Dedication
|
366
|
+
|
367
|
+
> The lights begin to twinkle from the rocks;\
|
368
|
+
> The long day wanes; the slow moon climbs; the deep\
|
369
|
+
> Moans round with many voices. Come, my friends,\
|
370
|
+
> 'T is not too late to seek a newer world.\
|
371
|
+
> Push off, and sitting well in order smite\
|
372
|
+
> The sounding furrows; for my purpose holds\
|
373
|
+
> To sail beyond the sunset, and the baths\
|
374
|
+
> Of all the western stars, until I die.\
|
375
|
+
> It may be that the gulfs will wash us down:\
|
376
|
+
> It may be we shall touch the Happy Isles,\
|
377
|
+
> And see the great Achilles, whom we knew.\
|
378
|
+
> Tho' much is taken, much abides; and tho'\
|
379
|
+
> We are not now that strength which in old days\
|
380
|
+
> Moved earth and heaven, that which we are, we are;\
|
381
|
+
> One equal temper of heroic hearts,\
|
382
|
+
> Made weak by time and fate, but strong in will\
|
383
|
+
> To strive, to seek, to find, and not to yield.
|
384
|
+
>
|
385
|
+
> from Ulysses, by Alfred, Lord Tennyson
|