tzu 0.1.0.0 → 0.1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9fc0ed30a4b98b00a486d4fa819753e2c3903975
4
- data.tar.gz: 79ec5b04b569d4463d0d7f4c88e9b5271476fed3
3
+ metadata.gz: b65373fa789405d5d165a29c71bd15b3a227450b
4
+ data.tar.gz: e7a9845bc66eba286135a9e7cbb64b9c197fa409
5
5
  SHA512:
6
- metadata.gz: 2918e22b6a49c4d9d878413621fa0d1456f369a41fe84adafc0fdcbc2d9996caaf531e42a102cb11fc1b1a6d07d8ec311dc3c6b0904cfbcc792dd21792824a0f
7
- data.tar.gz: b3007e68797f9b89bfa75682eda9ceab964598e548e404c8e23039db2ee9e563e81fb19f99b4a47ffa3994114caa0f1afa36533e881230db6de7273d167ee656
6
+ metadata.gz: a9a239244309bd5eae81848753bbdcdad5b22aac74b070ff73cb4ea843be47c1715a19758e06bcf6ed0025066423c2f6cace651b1f84f4f8b02888e767a09044
7
+ data.tar.gz: 14b6723526bfa3dbe24a36d5dc65caa122cbc5f2e12eb2c7891aacedb6d89542736809ecca93044da744e2296e8ff190995695b05a49d042e15e4f883756815d
data/README.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Tzu
2
2
 
3
+ Tzu provides a simple interface for writing classes that encapsulate a single command.
4
+
5
+ **Commands should:**
6
+
7
+ - Do exactly one thing (Single Responsibility Priciple)
8
+ - Be self-documenting
9
+ - Be testable
10
+ - Be easy to mock and stub
11
+
12
+ **Benefits**
13
+
14
+ - File and class names say what your code *actually does*, making onboarding and debugging a much simpler process.
15
+ - Minimize the instances of persistence logic throughout the application
16
+ - The Rails 'where do I put...?' question is solved. Models, Controllers, Workers and even Rake Tasks become slim.
17
+ - Maintain all of the benefits of Object Oriented programming while executing a procedural action, or Sequence of procedural actions.
18
+
19
+ **Documentation**
20
+
21
+ - [Usage](#usage)
22
+ - [Validation](#validation)
23
+ - [Passing Blocks](#passing-blocks)
24
+ - [Hooks](#hooks)
25
+ - [Request Objects](#request-objects)
26
+
27
+ **Sequences**
28
+ - [Configure](#configure)
29
+ - [Execute](#execute)
30
+ - [Integrating Non Tzu Classes](#integrating-non-tzu-classes)
31
+ - [Hooks for Sequences](#hooks-for-sequences)
32
+ - [Mocking and Stubbing](#mocking-and-stubbing)
33
+
3
34
  ## Usage
4
35
 
5
36
  Tzu commands must include Tzu and implement a `#call` method.
@@ -79,7 +110,11 @@ outcome = MyRescueCommand.run!(params_that_cause_error)
79
110
 
80
111
  Note that if you pass a string to `invalid!`, it will coerce the result into a hash of the form:
81
112
 
82
- ```
113
+ ```ruby
114
+ # Invoking:
115
+ invalid!('Error String')
116
+
117
+ # Translates to:
83
118
  { errors: 'Error String' }
84
119
  ```
85
120
 
@@ -149,9 +184,7 @@ MyCommand.run(message: 'Hello!')
149
184
  #=> End Around 1
150
185
  ```
151
186
 
152
-
153
-
154
- ## Request objects
187
+ ## Request Objects
155
188
 
156
189
  You can define a request object for your command using the `#request_object` method.
157
190
 
@@ -196,6 +229,7 @@ Virtus.model validates the types of your inputs, and also makes them available v
196
229
  class MyRequestObject
197
230
  include Virtus.model
198
231
  include ActiveModel::Validations
232
+
199
233
  validates :name, :age, presence: :true
200
234
 
201
235
  attribute :name, String
@@ -225,7 +259,9 @@ outcome.type? #=> :validation
225
259
  outcome.result #=> {:age=>["can't be blank"]}
226
260
  ```
227
261
 
228
- # Configure a sequence of Tzu commands
262
+ # Execute Commands in Sequence
263
+
264
+ ## Configure
229
265
 
230
266
  Tzu provides a declarative way of encapsulating sequential command execution.
231
267
 
@@ -249,7 +285,7 @@ class MakeMeSoundImportant
249
285
  end
250
286
  ```
251
287
 
252
- The Tzu::Sequence provides a DSL for executing them in sequence:
288
+ Tzu::Sequence provides a DSL for executing them in sequence:
253
289
 
254
290
  ```ruby
255
291
  class ProclaimMyImportance
@@ -276,7 +312,7 @@ Each command to be executed is defined as the first argument of `step`.
276
312
  The `receives` method inside the `step` block allows you to mutate the parameters being passed into the command.
277
313
  It is passed both the original parameters and a hash containing the results of prior commands.
278
314
 
279
- By default, the keys of the `prior_results` hash are underscored/symbolized command names.
315
+ By default, the keys of the `prior_results` hash are demodulized/underscored/symbolized command names.
280
316
  You can define your own keys using the `as` method.
281
317
 
282
318
  ```ruby
@@ -288,9 +324,15 @@ step SayMyName do
288
324
  end
289
325
  ```
290
326
 
291
- # Executing the sequence
327
+ If you don't need to mutate the parameters for the command, simply omit `receives`.
328
+
329
+ ```ruby
330
+ step SayMyName
331
+ ```
292
332
 
293
- By default, Sequences return the result of the final command within an Outcome object,
333
+ ## Execute
334
+
335
+ By default, Sequences return the result of the final command.
294
336
 
295
337
  ```ruby
296
338
  outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
@@ -363,7 +405,95 @@ outcome.result
363
405
  #=> { name: 'Jessica', original_message: 'Hello, Jessica', message: 'BULLETIN: Hello, Jessica! You are the most important citizen of Azerbaijan!' }
364
406
  ```
365
407
 
366
- # Hooks for Sequences
408
+ ## Integrating Non Tzu Classes
409
+
410
+ Sometimes there is a need to combine non-Tzu classes with Tzu classes in a sequence.
411
+
412
+ As an example, let's say I wanted to query a record, update it, and pass the updated record to a Tzu command.
413
+ To do this, I'll use the [Get](https://github.com/onfido/get) and [Tradesman](https://github.com/onfido/tradesman/) libraries.
414
+
415
+ When invoked on its own, Get looks like this:
416
+ ```ruby
417
+ Get::UserByName.run(name)
418
+ ```
419
+
420
+ Tradesman Update looks like this:
421
+ ```ruby
422
+ Tradesman::UpdateUser.go(user_id, update_params)
423
+ ```
424
+
425
+ The integration of Get into `Tzu::Sequence` is easy, as it only expects one parameter, and it's invoked with `#run`.
426
+ Tradesman is more complicated; it expects two parameters - a User ID and a hash to update that record with - and it's invoked with `#go`.
427
+
428
+ Tradesman offers the `invoke_with` and `receives_many` arguments to deal with these differences.
429
+
430
+ `invoke_with` is self-explanatory, and defaults to `#run`.
431
+
432
+ The `receives_many` block must return an array, which will be passed as a splat to the `invoke_with` method.
433
+ ```ruby
434
+ class NonTzuSequence
435
+ include Tzu::Sequence
436
+
437
+ step Get::UserByName do
438
+ receives do |params|
439
+ params[:name]
440
+ end
441
+ end
442
+
443
+ step Tradesman::UpdateUser do
444
+ invoke_with :go
445
+
446
+ receives_many do |params, prior_results|
447
+ [
448
+ prior_results[:user_by_name].id,
449
+ params[:update_params]
450
+ ]
451
+ end
452
+ end
453
+
454
+ step SayMyName do
455
+ receives do |params, prior_results|
456
+ prior_results[:update_user].name
457
+ end
458
+ end
459
+ end
460
+
461
+ outcome = NonTzuSequence.run(name: 'Blake', update_params: { name: 'Morgan' })
462
+ outcome.result #=> 'Hello, Morgan'
463
+ ```
464
+
465
+ You can pass multiple parameters to `Tzu::Sequence` instead of a parameters hash, just make sure you add the correct amount of arguments to your `receives` and `receives_many` blocks.
466
+
467
+ ```ruby
468
+ class NonTzuSequence
469
+ include Tzu::Sequence
470
+
471
+ step Get::UserByName do
472
+ receives do |name, update_params|
473
+ name
474
+ end
475
+ end
476
+
477
+ step Tradesman::UpdateUser do
478
+ invoke_with :go
479
+
480
+ receives_many do |name, update_params, prior_results|
481
+ [prior_results[:user_by_name].id, update_params]
482
+ end
483
+ end
484
+
485
+ step SayMyName do
486
+ receives do |name, update_params, prior_results|
487
+ prior_results[:update_user].name
488
+ end
489
+ end
490
+ end
491
+
492
+ outcome = NonTzuSequence.run('Blake', { name: 'Morgan' })
493
+ outcome.result #=> 'Hello, Morgan'
494
+ ```
495
+
496
+ ## Hooks for Sequences
367
497
 
368
498
  Tzu sequences have the same `before`, `after`, and `around` hooks available in Tzu commands.
369
499
  This is particularly useful for wrapping multiple commands in a transaction.
@@ -394,3 +524,7 @@ class ProclaimMyImportance
394
524
  end
395
525
  end
396
526
  ```
527
+
528
+ ## Mocking and Stubbing
529
+
530
+ Tzu has a specialized (and well-documented) gem for mocking/stubbing, [TzuMock](https://github.com/onfido/tzu_mock).
data/lib/tzu/errors.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Tzu
2
+ class InvalidSequence < StandardError
3
+ end
4
+ end
data/lib/tzu/hooks.rb CHANGED
@@ -37,12 +37,12 @@ module Tzu
37
37
  end
38
38
  end
39
39
 
40
- def with_hooks(params, &block)
40
+ def with_hooks(*params, &block)
41
41
  result = nil
42
42
  run_around_hooks do
43
- run_before_hooks(params)
44
- result = yield(params)
45
- run_after_hooks(params)
43
+ run_before_hooks(*params)
44
+ result = yield(*params)
45
+ run_after_hooks(*params)
46
46
  end
47
47
  result
48
48
  end
@@ -25,8 +25,8 @@ module Tzu
25
25
  end
26
26
  end
27
27
 
28
- def method_missing(method, *args, &block)
29
- @request_klass = args.first if method == :request_object
28
+ def request_object(klass)
29
+ @request_klass = klass
30
30
  end
31
31
  end
32
32
  end
data/lib/tzu/sequence.rb CHANGED
@@ -7,13 +7,8 @@ module Tzu
7
7
  class << self
8
8
  attr_reader :steps, :result_block
9
9
 
10
- def run(params)
11
- new(params).run
12
- end
13
-
14
- def method_missing(method, *args, &block)
15
- return add_step(args.first, &block) if method == :step
16
- super
10
+ def run(*params)
11
+ new(*params).run
17
12
  end
18
13
 
19
14
  def type
@@ -25,19 +20,20 @@ module Tzu
25
20
  @type = type
26
21
  end
27
22
 
28
- def add_step(klass, &block)
23
+ def step(klass, &block)
29
24
  @steps = [] unless @steps
30
25
 
31
26
  step = Step.new(klass)
32
- step.instance_eval(&block)
27
+ step.instance_eval(&block) if block
33
28
 
34
29
  @steps << step
35
30
  end
36
31
  end
37
32
 
38
- def initialize(params)
33
+ def initialize(*params)
39
34
  @params = params
40
35
  @last_outcome = nil
36
+ @prior_results = {}
41
37
  end
42
38
 
43
39
  def run
@@ -50,15 +46,25 @@ module Tzu
50
46
  def sequence_results
51
47
  with_hooks(@params) do |params|
52
48
  self.class.steps.reduce({}) do |prior_results, step|
53
- @last_outcome = step.run(params, prior_results)
54
- break if @last_outcome.failure?
55
- prior_results.merge!(step.name => @last_outcome.result)
49
+ @last_outcome = step.run(*params, prior_results)
50
+ break if last_outcome_is_failure?
51
+ merge_last_outcome_into_prior_results(step, prior_results)
56
52
  end
57
53
  end
58
54
  end
59
55
 
60
56
  private
61
57
 
58
+ def last_outcome_is_failure?
59
+ return true if (@last_outcome.respond_to?(:failure?) && @last_outcome.failure?)
60
+ false
61
+ end
62
+
63
+ def merge_last_outcome_into_prior_results(step, prior_results)
64
+ result = @last_outcome.respond_to?(:result) ? @last_outcome.result : @last_outcome
65
+ prior_results.merge(step.name => result)
66
+ end
67
+
62
68
  def mutated_result(results)
63
69
  Outcome.new(true, instance_exec(@params, results, &self.class.result_block))
64
70
  end
data/lib/tzu/step.rb CHANGED
@@ -1,34 +1,64 @@
1
1
  module Tzu
2
2
  class Step
3
+ DOUBLE_MUTATOR = 'You cannot define both receives and receives_many'
4
+
3
5
  String.send(:include, ::Tzu::CoreExtensions::String)
4
- attr_reader :klass, :param_mutator
6
+ attr_reader :klass, :single_mutator, :splat_mutator
5
7
 
6
8
  def initialize(klass)
7
9
  @klass = klass
10
+ @invoke_method = :run
8
11
  end
9
12
 
10
- def run(params, prior_results)
11
- command_params = process(params, prior_results)
12
- @klass.run(command_params)
13
+ def run(*params, prior_results)
14
+ # Forward parameters as splat if no mutators are defined
15
+ return @klass.send(@invoke_method, *params) if mutator.nil?
16
+
17
+ command_params = process(*params, prior_results)
18
+
19
+ return @klass.send(@invoke_method, command_params) unless splat?
20
+ @klass.send(@invoke_method, *command_params)
13
21
  end
14
22
 
15
23
  def name
16
24
  return @name if @name && @name.is_a?(Symbol)
17
- @klass.to_s.symbolize
25
+ @klass.to_s.split('::').last.symbolize
18
26
  end
19
27
 
20
28
  def receives(&block)
21
- @param_mutator = block
29
+ double_mutator_error if splat?
30
+ @single_mutator = block
31
+ end
32
+
33
+ def receives_many(&block)
34
+ double_mutator_error if @single_mutator.present?
35
+ @splat_mutator = block
22
36
  end
23
37
 
24
38
  def as(name)
25
39
  @name = name
26
40
  end
27
41
 
42
+ def invoke_with(method)
43
+ @invoke_method = method
44
+ end
45
+
28
46
  private
29
47
 
30
- def process(params, prior_results)
31
- instance_exec(params, prior_results, &@param_mutator)
48
+ def double_mutator_error
49
+ raise Tzu::InvalidSequence.new(DOUBLE_MUTATOR)
50
+ end
51
+
52
+ def mutator
53
+ @single_mutator || @splat_mutator
54
+ end
55
+
56
+ def splat?
57
+ @splat_mutator.present?
58
+ end
59
+
60
+ def process(*params, prior_results)
61
+ instance_exec(*params, prior_results, &mutator)
32
62
  end
33
63
  end
34
64
  end
data/lib/tzu.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'tzu/core_extensions/string'
2
+ require 'tzu/errors'
2
3
  require 'tzu/run_methods'
3
4
  require 'tzu/failure'
4
5
  require 'tzu/hooks'
@@ -16,7 +16,7 @@ class MakeMeSoundImportant
16
16
  end
17
17
  end
18
18
 
19
- class InvalidCommand
19
+ class ThrowInvalidError
20
20
  include Tzu
21
21
 
22
22
  def call(params)
@@ -24,6 +24,14 @@ class InvalidCommand
24
24
  end
25
25
  end
26
26
 
27
+ class ConstructGreeting
28
+ class << self
29
+ def go(greeting, name)
30
+ "#{greeting}, #{name}"
31
+ end
32
+ end
33
+ end
34
+
27
35
  class MultiStepSimple
28
36
  include Tzu::Sequence
29
37
 
@@ -43,14 +51,33 @@ class MultiStepSimple
43
51
  end
44
52
  end
45
53
 
54
+ class MultiStepNonTzu
55
+ include Tzu::Sequence
56
+
57
+ step ConstructGreeting do
58
+ as :say_my_name
59
+ invoke_with :go
60
+
61
+ receives_many do |greeting, name, country|
62
+ [greeting, name]
63
+ end
64
+ end
65
+
66
+ step MakeMeSoundImportant do
67
+ receives do |greeting, name, country, prior_results|
68
+ {
69
+ boring_message: prior_results[:say_my_name],
70
+ country: country
71
+ }
72
+ end
73
+ end
74
+ end
75
+
46
76
  class MultiStepComplex
47
77
  include Tzu::Sequence
48
78
 
49
79
  step SayMyName do
50
80
  as :first_command
51
- receives do |params|
52
- { name: params[:name] }
53
- end
54
81
  end
55
82
 
56
83
  step MakeMeSoundImportant do
@@ -71,9 +98,6 @@ class MultiStepProcessResults
71
98
 
72
99
  step SayMyName do
73
100
  as :first_command
74
- receives do |params|
75
- { name: params[:name] }
76
- end
77
101
  end
78
102
 
79
103
  step MakeMeSoundImportant do
@@ -97,13 +121,9 @@ end
97
121
  class MultiStepInvalid
98
122
  include Tzu::Sequence
99
123
 
100
- step SayMyName do
101
- receives do |params|
102
- { name: params[:name] }
103
- end
104
- end
124
+ step SayMyName
105
125
 
106
- step InvalidCommand do
126
+ step ThrowInvalidError do
107
127
  receives do |params, prior_results|
108
128
  { answer: "#{params[:name]}!!! #{prior_results[:say_my_name]}" }
109
129
  end
@@ -128,16 +148,16 @@ describe Tzu::Sequence do
128
148
  steps.each { |step| expect(step.is_a? Tzu::Step).to be true }
129
149
  end
130
150
 
131
- it 'passes the appropriate klass, name, and param_mutator to each step' do
151
+ it 'passes the appropriate klass, name, and param_mutators to each step' do
132
152
  say_my_name = steps.first
133
153
  expect(say_my_name.klass).to eq SayMyName
134
154
  expect(say_my_name.name).to eq :say_my_name
135
- expect(say_my_name.param_mutator.is_a? Proc).to be true
155
+ expect(say_my_name.single_mutator.is_a? Proc).to be true
136
156
 
137
157
  make_me_sound_important = steps.last
138
158
  expect(make_me_sound_important.klass).to eq MakeMeSoundImportant
139
159
  expect(make_me_sound_important.name).to eq :make_me_sound_important
140
- expect(make_me_sound_important.param_mutator.is_a? Proc).to be true
160
+ expect(make_me_sound_important.single_mutator.is_a? Proc).to be true
141
161
  end
142
162
  end
143
163
 
@@ -148,16 +168,15 @@ describe Tzu::Sequence do
148
168
  steps.each { |step| expect(step.is_a? Tzu::Step).to be true }
149
169
  end
150
170
 
151
- it 'passes the appropriate klass, name, and param_mutator to each step' do
171
+ it 'passes the appropriate klass, name, and param_mutators to each step' do
152
172
  say_my_name = steps.first
153
173
  expect(say_my_name.klass).to eq SayMyName
154
174
  expect(say_my_name.name).to eq :first_command
155
- expect(say_my_name.param_mutator.is_a? Proc).to be true
156
175
 
157
176
  make_me_sound_important = steps.last
158
177
  expect(make_me_sound_important.klass).to eq MakeMeSoundImportant
159
178
  expect(make_me_sound_important.name).to eq :final_command
160
- expect(make_me_sound_important.param_mutator.is_a? Proc).to be true
179
+ expect(make_me_sound_important.single_mutator.is_a? Proc).to be true
161
180
  end
162
181
  end
163
182
  end
@@ -177,6 +196,13 @@ describe Tzu::Sequence do
177
196
  end
178
197
  end
179
198
 
199
+ context MultiStepNonTzu do
200
+ it 'returns the outcome of the last command' do
201
+ outcome = MultiStepNonTzu.run('Greetings', 'Christopher', 'Canada')
202
+ expect(outcome.result).to eq 'Greetings, Christopher! You are the most important citizen of Canada!'
203
+ end
204
+ end
205
+
180
206
  context MultiStepComplex do
181
207
  it 'returns the outcome of the last command' do
182
208
  outcome = MultiStepComplex.run(params)
data/spec/step_spec.rb CHANGED
@@ -21,4 +21,36 @@ describe Tzu::Step do
21
21
  end
22
22
  end
23
23
  end
24
+
25
+ context '#receives' do
26
+ context 'when splat_mutator is already defined' do
27
+ let(:step) { Tzu::Step.new(:step_name) }
28
+
29
+ before do
30
+ step.receives_many do |variable|
31
+ [1, 2, 3]
32
+ end
33
+ end
34
+
35
+ it 'throws error' do
36
+ expect { step.receives { |var| 'foo'} } .to raise_error(Tzu::InvalidSequence)
37
+ end
38
+ end
39
+ end
40
+
41
+ context '#receives_many' do
42
+ context 'when single_mutator is already defined' do
43
+ let(:step) { Tzu::Step.new(:step_name) }
44
+
45
+ before do
46
+ step.receives do |var|
47
+ 'hello'
48
+ end
49
+ end
50
+
51
+ it 'throws error' do
52
+ expect { step.receives_many { |var| [1, 2, 3] } } .to raise_error(Tzu::InvalidSequence)
53
+ end
54
+ end
55
+ end
24
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tzu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.0
4
+ version: 0.1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morgan Bruce
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-07-24 00:00:00.000000000 Z
12
+ date: 2015-07-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -109,7 +109,7 @@ dependencies:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
- description: Encapsulate your database queries with dynamically generated classes
112
+ description: Tzu is a library for issuing commands in Ruby
113
113
  email: morgan@onfido.com
114
114
  executables: []
115
115
  extensions: []
@@ -119,6 +119,7 @@ files:
119
119
  - README.md
120
120
  - lib/tzu.rb
121
121
  - lib/tzu/core_extensions/string.rb
122
+ - lib/tzu/errors.rb
122
123
  - lib/tzu/failure.rb
123
124
  - lib/tzu/hooks.rb
124
125
  - lib/tzu/invalid.rb
@@ -159,8 +160,7 @@ rubyforge_project:
159
160
  rubygems_version: 2.2.2
160
161
  signing_key:
161
162
  specification_version: 4
162
- summary: Get is a library designed to encapsulate Rails database queries and prevent
163
- query pollution in the view layer.
163
+ summary: Standardise and encapsulate your application's actions
164
164
  test_files:
165
165
  - spec/hooks_spec.rb
166
166
  - spec/outcome_spec.rb