substation 0.0.6 → 0.0.7
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.
- data/CONTRIBUTING.md +11 -0
- data/Changelog.md +9 -0
- data/Gemfile +1 -1
- data/README.md +241 -28
- data/config/flay.yml +1 -1
- data/config/reek.yml +7 -2
- data/lib/substation.rb +1 -0
- data/lib/substation/chain.rb +164 -0
- data/lib/substation/response.rb +12 -9
- data/lib/substation/support/utils.rb +1 -1
- data/lib/substation/version.rb +1 -1
- data/spec/integration/substation/dispatcher/call_spec.rb +46 -20
- data/spec/unit/substation/chain/call_spec.rb +57 -0
- data/spec/unit/substation/chain/incoming/result_spec.rb +21 -0
- data/spec/unit/substation/chain/outgoing/result_spec.rb +21 -0
- data/spec/unit/substation/response/request_spec.rb +16 -0
- data/spec/unit/substation/utils/class_methods/coerce_callable_spec.rb +6 -0
- metadata +8 -2
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Contributing
|
2
|
+
------------
|
3
|
+
|
4
|
+
* If you want your code merged into the mainline, please discuss the proposed changes with me before doing any work on it.
|
5
|
+
* Fork the project.
|
6
|
+
* Make your feature addition or bug fix.
|
7
|
+
* Follow this [style guide](https://github.com/dkubb/styleguide).
|
8
|
+
* Add specs for it. This is important so I don't break it in a future version unintentionally. Tests must cover all branches within the code, and code must be fully covered.
|
9
|
+
* Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
10
|
+
* Run "rake ci". This must pass and not show any regressions in the metrics for the code to be merged.
|
11
|
+
* Send me a pull request. Bonus points for topic branches.
|
data/Changelog.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# v0.0.7 2013-06-14
|
2
|
+
|
3
|
+
* [feature] Make `Substation::Response#request` part of the public API (snusnu)
|
4
|
+
* [feature] Introduce `Substation::Chain` to process an action as a chain of
|
5
|
+
incoming handlers, followed by the pivot handler and some outgoing
|
6
|
+
handlers.
|
7
|
+
|
8
|
+
[Compare v0.0.6..v0.0.7](https://github.com/snusnu/substation/compare/v0.0.6...v0.0.7)
|
9
|
+
|
1
10
|
# v0.0.6 2013-05-17
|
2
11
|
|
3
12
|
* [fixed] Fixed bug for actions configured with a const handler (snusnu)
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -12,23 +12,25 @@
|
|
12
12
|
[codeclimate]: https://codeclimate.com/github/snusnu/substation
|
13
13
|
[coveralls]: https://coveralls.io/r/snusnu/substation
|
14
14
|
|
15
|
-
`substation`
|
15
|
+
Think of `substation` as some sort of domain level request router. It assumes
|
16
16
|
that every usecase in your application has a name and is implemented in a dedicated
|
17
|
-
|
18
|
-
|
17
|
+
object that will be referred to as an *action*. The only protocol such actions must
|
18
|
+
support is `#call(request)`.
|
19
19
|
|
20
|
-
The contract for actions specifies that when invoked,
|
20
|
+
The contract for actions specifies that when invoked, they can
|
21
21
|
receive arbitrary input data which will be available in `request.input`.
|
22
22
|
Additionally, `request.env` contains an arbitrary object that
|
23
23
|
represents your application environment and will typically provide access
|
24
|
-
to useful things like a logger
|
24
|
+
to useful things like a logger and probably some sort of storage engine
|
25
|
+
abstraction object.
|
25
26
|
|
26
27
|
The contract further specifies that every action must return an instance
|
27
28
|
of either `Substation::Response::Success` or
|
28
29
|
`Substation::Response::Failure`. Again, arbitrary data can be associated
|
29
|
-
with any kind of response, and will be available in `response.
|
30
|
-
|
31
|
-
|
30
|
+
with any kind of response, and will be available in `response.output`. To
|
31
|
+
indicate wether invoking the action was successful or not, you can use
|
32
|
+
`response.success?`. In addition to that, `response.request` contains
|
33
|
+
the request object used to invoke the action.
|
32
34
|
|
33
35
|
`Substation::Dispatcher` stores a mapping of action names to the actual
|
34
36
|
objects implementing the action, as well as the application environment.
|
@@ -290,16 +292,28 @@ few simple actions.
|
|
290
292
|
module App
|
291
293
|
|
292
294
|
class Database
|
293
|
-
include
|
295
|
+
include Equalizer.new(:relations)
|
296
|
+
|
297
|
+
def initialize(relations)
|
298
|
+
@relations = relations
|
299
|
+
end
|
294
300
|
|
295
301
|
def [](relation_name)
|
296
|
-
Relation.new(
|
302
|
+
Relation.new(relations[relation_name])
|
297
303
|
end
|
298
304
|
|
305
|
+
protected
|
306
|
+
|
307
|
+
attr_reader :relations
|
308
|
+
|
299
309
|
class Relation
|
300
|
-
include
|
310
|
+
include Equalizer.new(:tuples)
|
301
311
|
include Enumerable
|
302
312
|
|
313
|
+
def initialize(tuples)
|
314
|
+
@tuples = tuples
|
315
|
+
end
|
316
|
+
|
303
317
|
def each(&block)
|
304
318
|
return to_enum unless block_given?
|
305
319
|
tuples.each(&block)
|
@@ -313,37 +327,45 @@ module App
|
|
313
327
|
def insert(tuple)
|
314
328
|
self.class.new(tuples + [tuple])
|
315
329
|
end
|
330
|
+
|
331
|
+
protected
|
332
|
+
|
333
|
+
attr_reader :tuples
|
316
334
|
end
|
317
335
|
end
|
318
336
|
|
319
337
|
module Models
|
320
338
|
|
321
339
|
class Person
|
322
|
-
include
|
340
|
+
include Equalizer.new(:id, :name)
|
323
341
|
|
324
|
-
|
325
|
-
|
326
|
-
end
|
342
|
+
attr_reader :id
|
343
|
+
attr_reader :name
|
327
344
|
|
328
|
-
def
|
329
|
-
attributes
|
345
|
+
def initialize(attributes)
|
346
|
+
@id, @name = attributes.values_at(:id, :name)
|
330
347
|
end
|
331
348
|
end
|
332
349
|
end # module Models
|
333
350
|
|
334
351
|
class Environment
|
335
|
-
include
|
336
|
-
include Adamantium::Flat
|
352
|
+
include Equalizer.new(:storage)
|
337
353
|
|
338
354
|
attr_reader :storage
|
355
|
+
|
356
|
+
def initialize(storage)
|
357
|
+
@storage = storage
|
358
|
+
end
|
339
359
|
end
|
340
360
|
|
341
361
|
class Storage
|
342
|
-
include
|
343
|
-
include Adamantium::Flat
|
344
|
-
|
362
|
+
include Equalizer.new(:db)
|
345
363
|
include Models
|
346
364
|
|
365
|
+
def initialize(db)
|
366
|
+
@db = db
|
367
|
+
end
|
368
|
+
|
347
369
|
def list_people
|
348
370
|
db[:people].all.map { |tuple| Person.new(tuple) }
|
349
371
|
end
|
@@ -356,14 +378,21 @@ module App
|
|
356
378
|
relation = db[:people].insert(:id => person.id, :name => person.name)
|
357
379
|
relation.map { |tuple| Person.new(tuple) }
|
358
380
|
end
|
381
|
+
|
382
|
+
protected
|
383
|
+
|
384
|
+
attr_reader :db
|
359
385
|
end
|
360
386
|
|
361
387
|
class App
|
362
|
-
include
|
363
|
-
|
388
|
+
include Equalizer.new(:dispatcher)
|
389
|
+
|
390
|
+
def initialize(dispatcher)
|
391
|
+
@dispatcher = dispatcher
|
392
|
+
end
|
364
393
|
|
365
394
|
def call(name, input = nil)
|
366
|
-
dispatcher.call(name, input)
|
395
|
+
@dispatcher.call(name, input)
|
367
396
|
end
|
368
397
|
end
|
369
398
|
|
@@ -373,7 +402,6 @@ module App
|
|
373
402
|
class Action
|
374
403
|
|
375
404
|
include AbstractType
|
376
|
-
include Adamantium::Flat
|
377
405
|
|
378
406
|
def self.call(request)
|
379
407
|
new(request).call
|
@@ -440,8 +468,8 @@ module App
|
|
440
468
|
end # module Actions
|
441
469
|
|
442
470
|
module Observers
|
443
|
-
LogEvent =
|
444
|
-
SendEmail =
|
471
|
+
LogEvent = Proc.new { |response| response }
|
472
|
+
SendEmail = Proc.new { |response| response }
|
445
473
|
end
|
446
474
|
|
447
475
|
DB = Database.new({
|
@@ -474,3 +502,188 @@ response = App::APP.call(:list_companies)
|
|
474
502
|
response.success? # => true
|
475
503
|
response.output # => [#<App::Models::Person attributes={:id=>1, :name=>"John"}>]
|
476
504
|
```
|
505
|
+
|
506
|
+
## Chains
|
507
|
+
|
508
|
+
In a typical application scenario, a few things need to happen before an
|
509
|
+
actual use case (an action) can be invoked. These things will often
|
510
|
+
include the following steps (probably in that order).
|
511
|
+
|
512
|
+
* Authentication
|
513
|
+
* Authorization
|
514
|
+
* Input data sanitization
|
515
|
+
* Input data validation
|
516
|
+
|
517
|
+
We only want to invoke our action if all those steps succeed. If any of
|
518
|
+
the above steps fails, we want to send back a response that provides
|
519
|
+
details about what exactly prevented us from further processing the
|
520
|
+
request. If authentication fails, why try to authorize. If authorization
|
521
|
+
fails, why try to sanitize. And so on.
|
522
|
+
|
523
|
+
If, however, all the above steps passed, we can
|
524
|
+
|
525
|
+
* Invoke the action
|
526
|
+
|
527
|
+
Oftentimes, at this point, we're not done just yet. We have invoked our
|
528
|
+
action and we probably got back some data, but we still need to turn it
|
529
|
+
into something the caller can easily consume. If you happen to develop a
|
530
|
+
web application for example, you'll probably want to render some HTML or
|
531
|
+
some JSON.
|
532
|
+
|
533
|
+
a) If you need to return HTML, you might
|
534
|
+
|
535
|
+
* Wrap the response data in some presenter object
|
536
|
+
* Wrap the presenter in some view object
|
537
|
+
* Use that view object to render an HTML template
|
538
|
+
|
539
|
+
b) If you need to return JSON, you might just
|
540
|
+
|
541
|
+
* Pass the response data to some serializer object and dump it to JSON
|
542
|
+
|
543
|
+
To allow chaining all those steps in a declarative way, substation
|
544
|
+
provides an object called `Substation::Chain`. Its contract is dead
|
545
|
+
simple:
|
546
|
+
|
547
|
+
1. `#call(Substation::Request) => Substation::Response`
|
548
|
+
2. `#result(Substation::Response) => Substation::Response`
|
549
|
+
|
550
|
+
You typically won't be calling `Substation::Chain#result` yourself, but
|
551
|
+
having it around, allows us to use chains in *incoming handlers*,
|
552
|
+
essentially nesting chains. This makes it possible to construct one
|
553
|
+
chain up until the pivot handler, and then reuse that same chain in one
|
554
|
+
usecase that takes the response and renders HTML, and in another that
|
555
|
+
renders JSON.
|
556
|
+
|
557
|
+
To construct a chain, you need to pass an enumerable of so called
|
558
|
+
handler objects to `Substation::Chain.new`. Handlers must support two
|
559
|
+
methods:
|
560
|
+
|
561
|
+
1. `#call(<Substation::Request, Substation::Response>) => Substation::Response`
|
562
|
+
2. `#result(Substation::Response) => <Substation::Request, Substation::Response>`
|
563
|
+
|
564
|
+
### Incoming handlers
|
565
|
+
|
566
|
+
All steps required *before* processing the action will potentially
|
567
|
+
produce a new, altered, `Substation::Request`. Therefore, the object
|
568
|
+
passed to `#call` must be an instance of `Substation::Request`.
|
569
|
+
|
570
|
+
Since `#call` must return a `Substation::Response` (because the chain
|
571
|
+
would halt and return that response in case calling its `#success?`
|
572
|
+
method would return `false`), we also need to implement `#result`
|
573
|
+
and have it return a `Substation::Request` instance that can be passed
|
574
|
+
on to the next handler.
|
575
|
+
|
576
|
+
The contract for incoming handlers therefore is:
|
577
|
+
|
578
|
+
1. `#call(Substation::Request) => Substation::Response`
|
579
|
+
2. `#result(Substation::Response) => Substation::Request`
|
580
|
+
|
581
|
+
By including the `Substation::Chain::Incoming` module into your handler
|
582
|
+
class, you'll get the following for free:
|
583
|
+
|
584
|
+
```ruby
|
585
|
+
def result(response)
|
586
|
+
Request.new(response.env, response.output)
|
587
|
+
end
|
588
|
+
```
|
589
|
+
|
590
|
+
This shows that an incoming handler can alter the incoming request in any
|
591
|
+
way that it wants to, as long as it returns the new request input data in
|
592
|
+
`Substation::Response#output` returned from `#call`.
|
593
|
+
|
594
|
+
### The pivot handler
|
595
|
+
|
596
|
+
Pivot is just another fancy name for the action in the context of a
|
597
|
+
chain. It's also the point where all subsequent handlers have to further
|
598
|
+
process the `Substation::Response` returned from invoking the action.
|
599
|
+
|
600
|
+
The contract for the pivot handler therefore is:
|
601
|
+
|
602
|
+
1. `#call(Substation::Request) => Substation::Response`
|
603
|
+
2. `#result(Substation::Response) => Substation::Response`
|
604
|
+
|
605
|
+
By including the `Substation::Chain::Pivot` module into your handler
|
606
|
+
class, you'll get the following for free:
|
607
|
+
|
608
|
+
```ruby
|
609
|
+
def result(response)
|
610
|
+
response
|
611
|
+
end
|
612
|
+
```
|
613
|
+
|
614
|
+
This reflects the fact that a pivot handler (since it's the one actually
|
615
|
+
producing the "raw" response, returns it unaltered.
|
616
|
+
|
617
|
+
### Outgoing handlers
|
618
|
+
|
619
|
+
All steps required *after* processing the action will potentially
|
620
|
+
produce a new, altered, `Substation::Response` instance to be returned.
|
621
|
+
Therefore the object passed to `#call` must be an instance of
|
622
|
+
`Substation::Response`. Since subsequent outgoing handlers might further
|
623
|
+
process the response, `#result` must be implemented so that it returns a
|
624
|
+
`Substation::Response` object that can be passed on to the next handler.
|
625
|
+
|
626
|
+
The contract for outgoing handlers therefore is:
|
627
|
+
|
628
|
+
1. `#call(Substation::Response) => Substation::Response`
|
629
|
+
2. `#result(Substation::Response) => Substation::Response`
|
630
|
+
|
631
|
+
By including the `Substation::Chain::Outgoing` module into your handler
|
632
|
+
class, you'll get the following for free:
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
def result(response)
|
636
|
+
response
|
637
|
+
end
|
638
|
+
```
|
639
|
+
|
640
|
+
This shows that an outgoing handler's `#call` can do anything with
|
641
|
+
the `Substation::Response#output` it received, as long as it makes
|
642
|
+
sure to return a new response with the new output properly set.
|
643
|
+
|
644
|
+
### Example
|
645
|
+
|
646
|
+
[substation-demo](https://github.com/snusnu/substation-demo) implements a
|
647
|
+
simple web application using `Substation::Chain`.
|
648
|
+
|
649
|
+
The demo implements a few of the above mentioned *incoming handlers*
|
650
|
+
for
|
651
|
+
|
652
|
+
* [Sanitization](https://github.com/snusnu/substation-demo/blob/master/demo/web/sanitizers.rb) using [ducktrap](https://github.com/mbj/ducktrap)
|
653
|
+
* [Validation](https://github.com/snusnu/substation-demo/blob/master/demo/validators.rb) using [vanguard](https://github.com/mbj/vanguard)
|
654
|
+
|
655
|
+
and some simple *outgoing handlers* for
|
656
|
+
|
657
|
+
* Wrapping response output in a
|
658
|
+
[presenter](https://github.com/snusnu/substation-demo/blob/master/demo/web/presenters.rb)
|
659
|
+
* [Serializing](https://github.com/snusnu/substation-demo/blob/master/demo/web/serializers.rb) response output to JSON
|
660
|
+
|
661
|
+
The
|
662
|
+
[handlers](https://github.com/snusnu/substation-demo/blob/master/demo/web/processors.rb)
|
663
|
+
are called *processors* in that app, and encapsulate the actual handler
|
664
|
+
performing the job. That's a common pattern, because you typically will
|
665
|
+
have to adapt to the interface your actual handlers provide.
|
666
|
+
|
667
|
+
Have a look at the base
|
668
|
+
[actions](https://github.com/snusnu/substation-demo/blob/master/demo/web/actions.rb)
|
669
|
+
that are then used to either produce
|
670
|
+
[HTML](https://github.com/snusnu/substation-demo/blob/master/demo/web/actions/html.rb)
|
671
|
+
or
|
672
|
+
[JSON](https://github.com/snusnu/substation-demo/blob/master/demo/web/actions/json.rb).
|
673
|
+
|
674
|
+
Finally it's all hooked up behind a few
|
675
|
+
[sinatra](https://github.com/sinatra/sinatra)
|
676
|
+
[routes](https://github.com/snusnu/substation-demo/blob/master/demo/web/routes.rb)
|
677
|
+
|
678
|
+
## Credits
|
679
|
+
|
680
|
+
* [snusnu](https://github.com/snusnu)
|
681
|
+
* [mbj](https://github.com/mbj)
|
682
|
+
|
683
|
+
## Contributing
|
684
|
+
|
685
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
686
|
+
|
687
|
+
## Copyright
|
688
|
+
|
689
|
+
Copyright © 2013 Martin Gamsjaeger (snusnu). See [LICENSE](LICENSE) for details.
|
data/config/flay.yml
CHANGED
data/config/reek.yml
CHANGED
@@ -23,7 +23,9 @@ DuplicateMethodCall:
|
|
23
23
|
allow_calls: []
|
24
24
|
FeatureEnvy:
|
25
25
|
enabled: true
|
26
|
-
exclude:
|
26
|
+
exclude:
|
27
|
+
- Substation::Chain#call # loops over instance state
|
28
|
+
- Substation::Chain::Incoming#result # defined in a module
|
27
29
|
IrresponsibleModule:
|
28
30
|
enabled: true
|
29
31
|
exclude: []
|
@@ -63,6 +65,8 @@ TooManyStatements:
|
|
63
65
|
- Substation::Utils#self.const_get
|
64
66
|
- Substation::Utils#self.symbolize_keys
|
65
67
|
- Substation::Dispatcher::Action#self.coerce
|
68
|
+
- Substation::Chain#call
|
69
|
+
- Substation::Utils#self.coerce_callable
|
66
70
|
max_statements: 3
|
67
71
|
UncommunicativeMethodName:
|
68
72
|
enabled: true
|
@@ -100,5 +104,6 @@ UnusedParameters:
|
|
100
104
|
exclude: []
|
101
105
|
UtilityFunction:
|
102
106
|
enabled: true
|
103
|
-
exclude:
|
107
|
+
exclude:
|
108
|
+
- Substation::Chain::Incoming#result # defined in a module
|
104
109
|
max_helper_calls: 0
|
data/lib/substation.rb
CHANGED
@@ -0,0 +1,164 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# Implements a chain of responsibility for an action
|
4
|
+
#
|
5
|
+
# An instance of this class will typically contain (in that order)
|
6
|
+
# a few handlers that process the incoming {Request} object, one
|
7
|
+
# handler that calls an action ({Chain::Pivot}), and some handlers
|
8
|
+
# that process the outgoing {Response} object.
|
9
|
+
#
|
10
|
+
# Both {Chain::Incoming} and {Chain::Outgoing} handlers must
|
11
|
+
# respond to `#call(response)` and `#result(response)`.
|
12
|
+
#
|
13
|
+
# @example chain handlers (used in instance method examples)
|
14
|
+
#
|
15
|
+
# module App
|
16
|
+
#
|
17
|
+
# class Handler
|
18
|
+
#
|
19
|
+
# def initialize(handler = nil)
|
20
|
+
# @handler = handler
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# protected
|
24
|
+
#
|
25
|
+
# attr_reader :handler
|
26
|
+
#
|
27
|
+
# class Incoming < self
|
28
|
+
# include Substation::Chain::Incoming
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class Outgoing < self
|
32
|
+
# include Substation::Chain::Outgoing
|
33
|
+
#
|
34
|
+
# private
|
35
|
+
#
|
36
|
+
# def respond_with(response, output)
|
37
|
+
# response.class.new(response.request, output)
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# class Validator < Handler::Incoming
|
43
|
+
# def call(request)
|
44
|
+
# result = handler.call(request.input)
|
45
|
+
# if result.valid?
|
46
|
+
# request.success(request.input)
|
47
|
+
# else
|
48
|
+
# request.error(result.violations)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# class Pivot < Handler
|
54
|
+
# include Substation::Chain::Pivot
|
55
|
+
#
|
56
|
+
# def call(request)
|
57
|
+
# handler.call(request)
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# class Presenter < Handler::Outgoing
|
62
|
+
# def call(response)
|
63
|
+
# respond_with(response, handler.new(response.output))
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
class Chain
|
69
|
+
|
70
|
+
# Supports chaining handlers processed before the {Pivot}
|
71
|
+
module Incoming
|
72
|
+
|
73
|
+
# The request passed on to the next handler in a {Chain}
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
#
|
77
|
+
# @param [Response] response
|
78
|
+
# the response returned from the previous handler in a {Chain}
|
79
|
+
#
|
80
|
+
# @return [Request]
|
81
|
+
# the request passed on to the next handler in a {Chain}
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
def result(response)
|
85
|
+
Request.new(response.env, response.output)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Supports chaining the {Pivot} or handlers processed after the {Pivot}
|
90
|
+
module Outgoing
|
91
|
+
|
92
|
+
# The response passed on to the next handler in a {Chain}
|
93
|
+
#
|
94
|
+
# @param [Response] response
|
95
|
+
# the response returned from the previous handler in a {Chain}
|
96
|
+
#
|
97
|
+
# @return [Response]
|
98
|
+
# the response passed on to the next handler in a {Chain}
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
def result(response)
|
102
|
+
response
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Supports chaining the {Pivot} handler
|
107
|
+
Pivot = Outgoing
|
108
|
+
|
109
|
+
include Concord.new(:handlers)
|
110
|
+
include Adamantium::Flat
|
111
|
+
include Pivot # allow nesting of chains
|
112
|
+
|
113
|
+
# Call the chain
|
114
|
+
#
|
115
|
+
# Invokes all handlers and returns either the first
|
116
|
+
# {Response::Failure} that it encounters, or if all
|
117
|
+
# goes well, the {Response::Success} returned from
|
118
|
+
# the last handler.
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
#
|
122
|
+
# module App
|
123
|
+
# SOME_ACTION = Substation::Chain.new [
|
124
|
+
# Validator.new(MY_VALIDATOR),
|
125
|
+
# Pivot.new(Actions::SOME_ACTION),
|
126
|
+
# Presenter.new(Presenters::SomePresenter)
|
127
|
+
# ]
|
128
|
+
#
|
129
|
+
# env = Object.new # your env would obviously differ
|
130
|
+
# input = { 'name' => 'John' }
|
131
|
+
# request = Substation::Request.new(env, input)
|
132
|
+
#
|
133
|
+
# response = SOME_ACTION.call(request)
|
134
|
+
#
|
135
|
+
# if response.success?
|
136
|
+
# response.output # => the output wrapped in a presenter
|
137
|
+
# else
|
138
|
+
# response.output # => if validation, pivot or presenter failed
|
139
|
+
# end
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# @param [Request] request
|
143
|
+
# the request to handle
|
144
|
+
#
|
145
|
+
# @return [Response::Success]
|
146
|
+
# the response returned from the last handler
|
147
|
+
#
|
148
|
+
# @return [Response::Failure]
|
149
|
+
# the response returned from the failing handler
|
150
|
+
#
|
151
|
+
# @raise [Exception]
|
152
|
+
# any exception that isn't explicitly rescued in client code
|
153
|
+
#
|
154
|
+
# @api public
|
155
|
+
def call(request)
|
156
|
+
handlers.inject(request) { |result, handler|
|
157
|
+
response = handler.call(result)
|
158
|
+
return response unless response.success?
|
159
|
+
handler.result(response)
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
end # class Chain
|
164
|
+
end # module Substation
|
data/lib/substation/response.rb
CHANGED
@@ -41,6 +41,18 @@ module Substation
|
|
41
41
|
include Equalizer.new(:request, :output)
|
42
42
|
include Adamantium::Flat
|
43
43
|
|
44
|
+
# The request that lead to this response
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
#
|
48
|
+
# response = dispatcher.call(:successful_action, :some_input)
|
49
|
+
# response.request # => request passed to action named :successful_action
|
50
|
+
#
|
51
|
+
# @return [Request]
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
attr_reader :request
|
55
|
+
|
44
56
|
# The application environment used within an action
|
45
57
|
#
|
46
58
|
# @example
|
@@ -115,15 +127,6 @@ module Substation
|
|
115
127
|
# @api public
|
116
128
|
abstract_method :success?
|
117
129
|
|
118
|
-
protected
|
119
|
-
|
120
|
-
# The request that lead to this response
|
121
|
-
#
|
122
|
-
# @return [Request]
|
123
|
-
#
|
124
|
-
# @api private
|
125
|
-
attr_reader :request
|
126
|
-
|
127
130
|
# An errorneous {Response}
|
128
131
|
class Failure < self
|
129
132
|
|
data/lib/substation/version.rb
CHANGED
@@ -5,16 +5,28 @@ require 'spec_helper'
|
|
5
5
|
module App
|
6
6
|
|
7
7
|
class Database
|
8
|
-
include
|
8
|
+
include Equalizer.new(:relations)
|
9
|
+
|
10
|
+
def initialize(relations)
|
11
|
+
@relations = relations
|
12
|
+
end
|
9
13
|
|
10
14
|
def [](relation_name)
|
11
|
-
Relation.new(
|
15
|
+
Relation.new(relations[relation_name])
|
12
16
|
end
|
13
17
|
|
18
|
+
protected
|
19
|
+
|
20
|
+
attr_reader :relations
|
21
|
+
|
14
22
|
class Relation
|
15
|
-
include
|
23
|
+
include Equalizer.new(:tuples)
|
16
24
|
include Enumerable
|
17
25
|
|
26
|
+
def initialize(tuples)
|
27
|
+
@tuples = tuples
|
28
|
+
end
|
29
|
+
|
18
30
|
def each(&block)
|
19
31
|
return to_enum unless block_given?
|
20
32
|
tuples.each(&block)
|
@@ -28,37 +40,45 @@ module App
|
|
28
40
|
def insert(tuple)
|
29
41
|
self.class.new(tuples + [tuple])
|
30
42
|
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
attr_reader :tuples
|
31
47
|
end
|
32
48
|
end
|
33
49
|
|
34
50
|
module Models
|
35
51
|
|
36
52
|
class Person
|
37
|
-
include
|
53
|
+
include Equalizer.new(:id, :name)
|
38
54
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
55
|
+
attr_reader :id
|
56
|
+
attr_reader :name
|
42
57
|
|
43
|
-
def
|
44
|
-
attributes
|
58
|
+
def initialize(attributes)
|
59
|
+
@id, @name = attributes.values_at(:id, :name)
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end # module Models
|
48
63
|
|
49
64
|
class Environment
|
50
|
-
include
|
51
|
-
include Adamantium::Flat
|
65
|
+
include Equalizer.new(:storage)
|
52
66
|
|
53
67
|
attr_reader :storage
|
68
|
+
|
69
|
+
def initialize(storage)
|
70
|
+
@storage = storage
|
71
|
+
end
|
54
72
|
end
|
55
73
|
|
56
74
|
class Storage
|
57
|
-
include
|
58
|
-
include Adamantium::Flat
|
59
|
-
|
75
|
+
include Equalizer.new(:db)
|
60
76
|
include Models
|
61
77
|
|
78
|
+
def initialize(db)
|
79
|
+
@db = db
|
80
|
+
end
|
81
|
+
|
62
82
|
def list_people
|
63
83
|
db[:people].all.map { |tuple| Person.new(tuple) }
|
64
84
|
end
|
@@ -71,14 +91,21 @@ module App
|
|
71
91
|
relation = db[:people].insert(:id => person.id, :name => person.name)
|
72
92
|
relation.map { |tuple| Person.new(tuple) }
|
73
93
|
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
attr_reader :db
|
74
98
|
end
|
75
99
|
|
76
100
|
class App
|
77
|
-
include
|
78
|
-
|
101
|
+
include Equalizer.new(:dispatcher)
|
102
|
+
|
103
|
+
def initialize(dispatcher)
|
104
|
+
@dispatcher = dispatcher
|
105
|
+
end
|
79
106
|
|
80
107
|
def call(name, input = nil)
|
81
|
-
dispatcher.call(name, input)
|
108
|
+
@dispatcher.call(name, input)
|
82
109
|
end
|
83
110
|
end
|
84
111
|
|
@@ -88,7 +115,6 @@ module App
|
|
88
115
|
class Action
|
89
116
|
|
90
117
|
include AbstractType
|
91
|
-
include Adamantium::Flat
|
92
118
|
|
93
119
|
def self.call(request)
|
94
120
|
new(request).call
|
@@ -155,8 +181,8 @@ module App
|
|
155
181
|
end # module Actions
|
156
182
|
|
157
183
|
module Observers
|
158
|
-
LogEvent =
|
159
|
-
SendEmail =
|
184
|
+
LogEvent = Proc.new { |response| response }
|
185
|
+
SendEmail = Proc.new { |response| response }
|
160
186
|
end
|
161
187
|
|
162
188
|
DB = Database.new({
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Chain, '#call' do
|
6
|
+
|
7
|
+
subject { object.call(request) }
|
8
|
+
|
9
|
+
let(:object) { described_class.new(handlers) }
|
10
|
+
let(:handlers) { [ handler_1, handler_2 ] }
|
11
|
+
let(:request) { Request.new(env, input) }
|
12
|
+
let(:env) { mock }
|
13
|
+
let(:input) { mock }
|
14
|
+
|
15
|
+
let(:handler_2) {
|
16
|
+
Class.new {
|
17
|
+
include Substation::Chain::Outgoing
|
18
|
+
def call(request)
|
19
|
+
request.success(request.input)
|
20
|
+
end
|
21
|
+
}.new
|
22
|
+
}
|
23
|
+
|
24
|
+
context "when all handlers are successful" do
|
25
|
+
let(:handler_1) {
|
26
|
+
Class.new {
|
27
|
+
include Substation::Chain::Incoming
|
28
|
+
def call(request)
|
29
|
+
request.success(request.input)
|
30
|
+
end
|
31
|
+
}.new
|
32
|
+
}
|
33
|
+
|
34
|
+
let(:response) { Response::Success.new(request, request.input) }
|
35
|
+
|
36
|
+
it { should eql(response) }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when an intermediate handler is not successful" do
|
40
|
+
let(:handler_1) {
|
41
|
+
Class.new {
|
42
|
+
include Substation::Chain::Incoming
|
43
|
+
def call(request)
|
44
|
+
request.error(request.input)
|
45
|
+
end
|
46
|
+
}.new
|
47
|
+
}
|
48
|
+
|
49
|
+
let(:response) { Response::Failure.new(request, request.input) }
|
50
|
+
|
51
|
+
before do
|
52
|
+
handler_2.should_not_receive(:call)
|
53
|
+
end
|
54
|
+
|
55
|
+
it { should eql(response) }
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Chain::Incoming, '#result' do
|
6
|
+
|
7
|
+
subject { object.result(response) }
|
8
|
+
|
9
|
+
let(:object) {
|
10
|
+
Class.new {
|
11
|
+
include Substation::Chain::Incoming
|
12
|
+
}.new
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:response) { Response::Success.new(request, input) }
|
16
|
+
let(:request) { Request.new(env, input) }
|
17
|
+
let(:env) { mock }
|
18
|
+
let(:input) { mock }
|
19
|
+
|
20
|
+
it { should eql(request) }
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Chain::Outgoing, '#result' do
|
6
|
+
|
7
|
+
subject { object.result(response) }
|
8
|
+
|
9
|
+
let(:object) {
|
10
|
+
Class.new {
|
11
|
+
include Substation::Chain::Outgoing
|
12
|
+
}.new
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:response) { Response::Success.new(request, input) }
|
16
|
+
let(:request) { Request.new(env, input) }
|
17
|
+
let(:env) { mock }
|
18
|
+
let(:input) { mock }
|
19
|
+
|
20
|
+
it { should be(response) }
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Response, '#request' do
|
6
|
+
|
7
|
+
subject { object.request }
|
8
|
+
|
9
|
+
let(:object) { Class.new(described_class).new(request, output) }
|
10
|
+
let(:request) { Request.new(env, input) }
|
11
|
+
let(:env) { mock }
|
12
|
+
let(:input) { mock }
|
13
|
+
let(:output) { mock }
|
14
|
+
|
15
|
+
it { should equal(request) }
|
16
|
+
end
|
@@ -30,6 +30,12 @@ describe Utils, '.coerce_callable' do
|
|
30
30
|
it { should be(handler) }
|
31
31
|
end
|
32
32
|
|
33
|
+
context "with a Chain handler" do
|
34
|
+
let(:handler) { Chain.new([]) }
|
35
|
+
|
36
|
+
it { should be(handler) }
|
37
|
+
end
|
38
|
+
|
33
39
|
context "with an unsupported handler" do
|
34
40
|
let(:handler) { mock }
|
35
41
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: substation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-06-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: adamantium
|
@@ -105,6 +105,7 @@ files:
|
|
105
105
|
- .rspec
|
106
106
|
- .rvmrc
|
107
107
|
- .travis.yml
|
108
|
+
- CONTRIBUTING.md
|
108
109
|
- Changelog.md
|
109
110
|
- Gemfile
|
110
111
|
- Gemfile.devtools
|
@@ -120,6 +121,7 @@ files:
|
|
120
121
|
- config/reek.yml
|
121
122
|
- config/yardstick.yml
|
122
123
|
- lib/substation.rb
|
124
|
+
- lib/substation/chain.rb
|
123
125
|
- lib/substation/dispatcher.rb
|
124
126
|
- lib/substation/observer.rb
|
125
127
|
- lib/substation/request.rb
|
@@ -128,6 +130,9 @@ files:
|
|
128
130
|
- lib/substation/version.rb
|
129
131
|
- spec/integration/substation/dispatcher/call_spec.rb
|
130
132
|
- spec/spec_helper.rb
|
133
|
+
- spec/unit/substation/chain/call_spec.rb
|
134
|
+
- spec/unit/substation/chain/incoming/result_spec.rb
|
135
|
+
- spec/unit/substation/chain/outgoing/result_spec.rb
|
131
136
|
- spec/unit/substation/dispatcher/action/call_spec.rb
|
132
137
|
- spec/unit/substation/dispatcher/action/class_methods/coerce_spec.rb
|
133
138
|
- spec/unit/substation/dispatcher/action_names_spec.rb
|
@@ -144,6 +149,7 @@ files:
|
|
144
149
|
- spec/unit/substation/response/failure/success_predicate_spec.rb
|
145
150
|
- spec/unit/substation/response/input_spec.rb
|
146
151
|
- spec/unit/substation/response/output_spec.rb
|
152
|
+
- spec/unit/substation/response/request_spec.rb
|
147
153
|
- spec/unit/substation/response/success/success_predicate_spec.rb
|
148
154
|
- spec/unit/substation/utils/class_methods/coerce_callable_spec.rb
|
149
155
|
- spec/unit/substation/utils/class_methods/const_get_spec.rb
|