substation 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|