substation 0.0.5 → 0.0.6

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/Changelog.md CHANGED
@@ -1,3 +1,9 @@
1
+ # v0.0.6 2013-05-17
2
+
3
+ * [fixed] Fixed bug for actions configured with a const handler (snusnu)
4
+
5
+ [Compare v0.0.5..v0.0.6](https://github.com/snusnu/substation/compare/v0.0.5...v0.0.6)
6
+
1
7
  # v0.0.5 2013-05-17
2
8
 
3
9
  * [feature] Shorter action config when no observers are needed (snusnu)
@@ -11,7 +17,6 @@ dispatcher = Substation::Dispatcher.coerce({
11
17
  :some_use_case => Proc.new { |request| request.success(:data) }
12
18
  }, env)
13
19
  ```
14
-
15
20
  [Compare v0.0.4..v0.0.5](https://github.com/snusnu/substation/compare/v0.0.4...v0.0.5)
16
21
 
17
22
  # v0.0.4 2013-05-15
data/README.md CHANGED
@@ -282,58 +282,195 @@ the course of performing a usecase (like a logger or a storage engine
282
282
  abstraction), you can encapsulate these objects within an application
283
283
  specific environment object, and send that along to every action.
284
284
 
285
- Here's a simple example with an environment that encapsulates a logger
286
- and an artificial storage abstraction object.
287
-
288
- The example builds on top of the application specific action baseclass
289
- shown above:
285
+ Here's a complete example with an environment that encapsulates a very
286
+ primitive storage abstraction object, one simple business entity, and a
287
+ few simple actions.
290
288
 
291
289
  ```ruby
292
290
  module App
291
+
292
+ class Database
293
+ include Concord.new(:entries)
294
+
295
+ def [](relation_name)
296
+ Relation.new(entries[relation_name])
297
+ end
298
+
299
+ class Relation
300
+ include Concord.new(:tuples)
301
+ include Enumerable
302
+
303
+ def each(&block)
304
+ return to_enum unless block_given?
305
+ tuples.each(&block)
306
+ self
307
+ end
308
+
309
+ def all
310
+ tuples
311
+ end
312
+
313
+ def insert(tuple)
314
+ self.class.new(tuples + [tuple])
315
+ end
316
+ end
317
+ end
318
+
319
+ module Models
320
+
321
+ class Person
322
+ include Concord.new(:attributes)
323
+
324
+ def id
325
+ attributes[:id]
326
+ end
327
+
328
+ def name
329
+ attributes[:name]
330
+ end
331
+ end
332
+ end # module Models
333
+
293
334
  class Environment
335
+ include Concord.new(:storage)
336
+ include Adamantium::Flat
337
+
294
338
  attr_reader :storage
295
- attr_reader :logger
339
+ end
340
+
341
+ class Storage
342
+ include Concord.new(:db)
343
+ include Adamantium::Flat
344
+
345
+ include Models
346
+
347
+ def list_people
348
+ db[:people].all.map { |tuple| Person.new(tuple) }
349
+ end
296
350
 
297
- def initialize(storage, logger)
298
- @storage, @logger = storage, logger
351
+ def load_person(id)
352
+ Person.new(db[:people].select { |tuple| tuple[:id] == id }.first)
353
+ end
354
+
355
+ def create_person(person)
356
+ relation = db[:people].insert(:id => person.id, :name => person.name)
357
+ relation.map { |tuple| Person.new(tuple) }
299
358
  end
300
359
  end
301
360
 
361
+ class App
362
+ include Concord.new(:dispatcher)
363
+ include Adamantium::Flat
364
+
365
+ def call(name, input = nil)
366
+ dispatcher.call(name, input)
367
+ end
368
+ end
369
+
370
+ # Base class for all actions
371
+ #
372
+ # @abstract
302
373
  class Action
303
- # ...
304
- # code from above example
305
- # ...
374
+
375
+ include AbstractType
376
+ include Adamantium::Flat
377
+
378
+ def self.call(request)
379
+ new(request).call
380
+ end
381
+
382
+ def initialize(request)
383
+ @request = request
384
+ @env = @request.env
385
+ @input = @request.input
386
+ end
387
+
388
+ abstract_method :call
389
+
390
+ private
391
+
392
+ attr_reader :request
393
+ attr_reader :env
394
+ attr_reader :input
306
395
 
307
396
  def db
308
397
  @env.storage
309
398
  end
399
+
400
+ def success(data)
401
+ @request.success(data)
402
+ end
403
+
404
+ def error(data)
405
+ @request.error(data)
406
+ end
310
407
  end
311
408
 
312
- class SomeUseCase < Action
409
+ module Actions
410
+ class ListPeople < Action
313
411
 
314
- def initialize(request)
315
- super
316
- @person = request.input
412
+ def call
413
+ success(db.list_people)
414
+ end
317
415
  end
318
416
 
319
- def call
320
- if person = db.save_person(@person)
321
- success(person)
322
- else
323
- error("Something went wrong")
417
+ class LoadPerson < Action
418
+ def initialize(request)
419
+ super
420
+ @id = input
421
+ end
422
+
423
+ def call
424
+ success(db.load_person(@id))
324
425
  end
325
426
  end
427
+
428
+ class CreatePerson < Action
429
+
430
+ def initialize(request)
431
+ super
432
+ @person = input
433
+ end
434
+
435
+ def call
436
+ success(db.create_person(@person))
437
+ end
438
+ end
439
+
440
+ end # module Actions
441
+
442
+ module Observers
443
+ LogEvent = Class.new { def self.call(response); end }
444
+ SendEmail = Class.new { def self.call(response); end }
326
445
  end
327
- end
328
446
 
329
- storage = App::Storage.new # some storage abstraction
330
- env = App::Environment.new(storage, Logger.new($stdout))
447
+ DB = Database.new({
448
+ :people => [{
449
+ :id => 1,
450
+ :name => 'John'
451
+ }]
452
+ })
453
+
454
+ actions = {
455
+ :list_people => Actions::ListPeople,
456
+ :load_person => Actions::LoadPerson,
457
+ :create_person => {
458
+ :action => Actions::CreatePerson,
459
+ :observer => [
460
+ Observers::LogEvent,
461
+ Observers::SendEmail
462
+ ]
463
+ }
464
+ }
465
+
466
+ storage = Storage.new(DB)
467
+ env = Environment.new(storage)
468
+ dispatcher = Substation::Dispatcher.coerce(actions, env)
331
469
 
332
- dispatcher = Substation::Dispatcher.coerce({
333
- 'some_use_case' => { 'action' => 'App::SomeUseCase' }
334
- }, env)
470
+ APP = App.new(dispatcher)
471
+ end
335
472
 
336
- # :some_input is no person, db.save_person will fail
337
- response = dispatcher.call(:some_use_case, :some_input)
338
- response.success? # => false
473
+ response = App::APP.call(:list_companies)
474
+ response.success? # => true
475
+ response.output # => [#<App::Models::Person attributes={:id=>1, :name=>"John"}>]
339
476
  ```
data/config/flay.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  ---
2
2
  threshold: 6
3
- total_score: 62
3
+ total_score: 65
@@ -57,7 +57,7 @@ module Substation
57
57
  case handler
58
58
  when Symbol, String
59
59
  Utils.const_get(handler)
60
- when Proc
60
+ when Proc, Class
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.5'.freeze
3
+ VERSION = '0.0.6'.freeze
4
4
  end
@@ -0,0 +1,234 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module App
6
+
7
+ class Database
8
+ include Concord.new(:entries)
9
+
10
+ def [](relation_name)
11
+ Relation.new(entries[relation_name])
12
+ end
13
+
14
+ class Relation
15
+ include Concord.new(:tuples)
16
+ include Enumerable
17
+
18
+ def each(&block)
19
+ return to_enum unless block_given?
20
+ tuples.each(&block)
21
+ self
22
+ end
23
+
24
+ def all
25
+ tuples
26
+ end
27
+
28
+ def insert(tuple)
29
+ self.class.new(tuples + [tuple])
30
+ end
31
+ end
32
+ end
33
+
34
+ module Models
35
+
36
+ class Person
37
+ include Concord.new(:attributes)
38
+
39
+ def id
40
+ attributes[:id]
41
+ end
42
+
43
+ def name
44
+ attributes[:name]
45
+ end
46
+ end
47
+ end # module Models
48
+
49
+ class Environment
50
+ include Concord.new(:storage)
51
+ include Adamantium::Flat
52
+
53
+ attr_reader :storage
54
+ end
55
+
56
+ class Storage
57
+ include Concord.new(:db)
58
+ include Adamantium::Flat
59
+
60
+ include Models
61
+
62
+ def list_people
63
+ db[:people].all.map { |tuple| Person.new(tuple) }
64
+ end
65
+
66
+ def load_person(id)
67
+ Person.new(db[:people].select { |tuple| tuple[:id] == id }.first)
68
+ end
69
+
70
+ def create_person(person)
71
+ relation = db[:people].insert(:id => person.id, :name => person.name)
72
+ relation.map { |tuple| Person.new(tuple) }
73
+ end
74
+ end
75
+
76
+ class App
77
+ include Concord.new(:dispatcher)
78
+ include Adamantium::Flat
79
+
80
+ def call(name, input = nil)
81
+ dispatcher.call(name, input)
82
+ end
83
+ end
84
+
85
+ # Base class for all actions
86
+ #
87
+ # @abstract
88
+ class Action
89
+
90
+ include AbstractType
91
+ include Adamantium::Flat
92
+
93
+ def self.call(request)
94
+ new(request).call
95
+ end
96
+
97
+ def initialize(request)
98
+ @request = request
99
+ @env = @request.env
100
+ @input = @request.input
101
+ end
102
+
103
+ abstract_method :call
104
+
105
+ private
106
+
107
+ attr_reader :request
108
+ attr_reader :env
109
+ attr_reader :input
110
+
111
+ def db
112
+ @env.storage
113
+ end
114
+
115
+ def success(data)
116
+ @request.success(data)
117
+ end
118
+
119
+ def error(data)
120
+ @request.error(data)
121
+ end
122
+ end
123
+
124
+ module Actions
125
+ class ListPeople < Action
126
+
127
+ def call
128
+ success(db.list_people)
129
+ end
130
+ end
131
+
132
+ class LoadPerson < Action
133
+ def initialize(request)
134
+ super
135
+ @id = input
136
+ end
137
+
138
+ def call
139
+ success(db.load_person(@id))
140
+ end
141
+ end
142
+
143
+ class CreatePerson < Action
144
+
145
+ def initialize(request)
146
+ super
147
+ @person = input
148
+ end
149
+
150
+ def call
151
+ success(db.create_person(@person))
152
+ end
153
+ end
154
+
155
+ end # module Actions
156
+
157
+ module Observers
158
+ LogEvent = Class.new { def self.call(response); end }
159
+ SendEmail = Class.new { def self.call(response); end }
160
+ end
161
+
162
+ DB = Database.new({
163
+ :people => [{
164
+ :id => 1,
165
+ :name => 'John'
166
+ }]
167
+ })
168
+
169
+ actions = {
170
+ :list_people => Actions::ListPeople,
171
+ :load_person => Actions::LoadPerson,
172
+ :create_person => {
173
+ :action => Actions::CreatePerson,
174
+ :observer => [
175
+ Observers::LogEvent,
176
+ Observers::SendEmail
177
+ ]
178
+ }
179
+ }
180
+
181
+ storage = Storage.new(DB)
182
+ env = Environment.new(storage)
183
+ dispatcher = Substation::Dispatcher.coerce(actions, env)
184
+
185
+ APP = App.new(dispatcher)
186
+ end
187
+
188
+ describe App::APP, '#call' do
189
+
190
+ context "when dispatching an action" do
191
+ subject { object.call(action, input) }
192
+
193
+ let(:object) { described_class }
194
+ let(:request) { Substation::Request.new(env, input) }
195
+ let(:env) { App::Environment.new(storage) }
196
+ let(:storage) { App::Storage.new(App::DB) }
197
+ let(:response) { Substation::Response::Success.new(request, output) }
198
+
199
+ let(:john) { App::Models::Person.new(:id => 1, :name => 'John') }
200
+ let(:jane) { App::Models::Person.new(:id => 2, :name => 'Jane') }
201
+
202
+ context "with no input data" do
203
+ let(:action) { :list_people }
204
+ let(:input) { nil }
205
+ let(:output) { [ john ] }
206
+
207
+ it { should eql(response) }
208
+ end
209
+
210
+ context "with input data" do
211
+ context "and no observer" do
212
+ let(:action) { :load_person }
213
+ let(:input) { 1 }
214
+ let(:output) { john }
215
+
216
+ it { should eq(response) }
217
+ end
218
+
219
+ context "and observers" do
220
+ let(:action) { :create_person }
221
+ let(:input) { jane }
222
+ let(:output) { [ john, jane ] }
223
+
224
+ before do
225
+ App::Observers::LogEvent.should_receive(:call).with(response).ordered
226
+ App::Observers::SendEmail.should_receive(:call).with(response).ordered
227
+ end
228
+
229
+ it { should eql(response) }
230
+ end
231
+
232
+ end
233
+ end
234
+ end
@@ -18,6 +18,12 @@ describe Utils, '.coerce_callable' do
18
18
  it { should be(Spec::Action::Success) }
19
19
  end
20
20
 
21
+ context "with a const handler" do
22
+ let(:handler) { Spec::Action::Success }
23
+
24
+ it { should be(Spec::Action::Success) }
25
+ end
26
+
21
27
  context "with a Proc handler" do
22
28
  let(:handler) { Proc.new { |response| respone } }
23
29
 
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.5
4
+ version: 0.0.6
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-16 00:00:00.000000000 Z
12
+ date: 2013-05-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: adamantium
@@ -126,6 +126,7 @@ files:
126
126
  - lib/substation/response.rb
127
127
  - lib/substation/support/utils.rb
128
128
  - lib/substation/version.rb
129
+ - spec/integration/substation/dispatcher/call_spec.rb
129
130
  - spec/spec_helper.rb
130
131
  - spec/unit/substation/dispatcher/action/call_spec.rb
131
132
  - spec/unit/substation/dispatcher/action/class_methods/coerce_spec.rb