tzu 0.1.0.0 → 0.1.1.0

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