substation 0.0.5 → 0.0.6

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