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 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