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 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
@@ -3,6 +3,6 @@ source 'http://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development do
6
- gem 'devtools', :git => 'https://github.com/datamapper/devtools.git'
6
+ gem 'devtools', :git => 'https://github.com/rom-rb/devtools.git'
7
7
  eval File.read('Gemfile.devtools')
8
8
  end
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` can be thought of as a domain level request router. It assumes
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
- class that will be referred to as an *action* for the purposes of this
18
- document. The only protocol such actions must support is `#call(request)`.
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, actions can
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 or a storage engine abstraction.
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.data`. In
30
- addition to that, `response.success?` is available and will indicate
31
- wether invoking the action was successful or not.
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 Concord.new(:entries)
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(entries[relation_name])
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 Concord.new(:tuples)
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 Concord.new(:attributes)
340
+ include Equalizer.new(:id, :name)
323
341
 
324
- def id
325
- attributes[:id]
326
- end
342
+ attr_reader :id
343
+ attr_reader :name
327
344
 
328
- def name
329
- attributes[:name]
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 Concord.new(:storage)
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 Concord.new(:db)
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 Concord.new(:dispatcher)
363
- include Adamantium::Flat
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 = Class.new { def self.call(response); end }
444
- SendEmail = Class.new { def self.call(response); end }
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 &copy; 2013 Martin Gamsjaeger (snusnu). See [LICENSE](LICENSE) for details.
data/config/flay.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  ---
2
2
  threshold: 6
3
- total_score: 65
3
+ total_score: 69
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
@@ -36,5 +36,6 @@ end
36
36
  require 'substation/request'
37
37
  require 'substation/response'
38
38
  require 'substation/observer'
39
+ require 'substation/chain'
39
40
  require 'substation/dispatcher'
40
41
  require 'substation/support/utils'
@@ -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
@@ -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
 
@@ -57,7 +57,7 @@ module Substation
57
57
  case handler
58
58
  when Symbol, String
59
59
  Utils.const_get(handler)
60
- when Proc, Class
60
+ when Proc, Class, Chain
61
61
  handler
62
62
  else
63
63
  raise(ArgumentError)
@@ -1,4 +1,4 @@
1
1
  module Substation
2
2
  # Gem version
3
- VERSION = '0.0.6'.freeze
3
+ VERSION = '0.0.7'.freeze
4
4
  end
@@ -5,16 +5,28 @@ require 'spec_helper'
5
5
  module App
6
6
 
7
7
  class Database
8
- include Concord.new(:entries)
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(entries[relation_name])
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 Concord.new(:tuples)
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 Concord.new(:attributes)
53
+ include Equalizer.new(:id, :name)
38
54
 
39
- def id
40
- attributes[:id]
41
- end
55
+ attr_reader :id
56
+ attr_reader :name
42
57
 
43
- def name
44
- attributes[:name]
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 Concord.new(:storage)
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 Concord.new(:db)
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 Concord.new(:dispatcher)
78
- include Adamantium::Flat
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 = Class.new { def self.call(response); end }
159
- SendEmail = Class.new { def self.call(response); end }
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.6
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-05-17 00:00:00.000000000 Z
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