tzu 0.0.2.0 → 0.1.0.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 +4 -4
- data/README.md +395 -0
- data/lib/tzu.rb +12 -51
- data/lib/tzu/core_extensions/string.rb +17 -0
- data/lib/tzu/hooks.rb +21 -4
- data/lib/tzu/invalid.rb +0 -2
- data/lib/tzu/outcome.rb +0 -1
- data/lib/tzu/run_methods.rb +32 -0
- data/lib/tzu/sequence.rb +68 -0
- data/lib/tzu/step.rb +34 -0
- data/lib/tzu/validation.rb +5 -14
- data/spec/sequence_spec.rb +206 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/step_spec.rb +24 -0
- data/spec/tzu_spec.rb +195 -20
- data/spec/validation_spec.rb +21 -37
- metadata +24 -5
- data/lib/tzu/organizer.rb +0 -35
- data/spec/organizer_spec.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fc0ed30a4b98b00a486d4fa819753e2c3903975
|
4
|
+
data.tar.gz: 79ec5b04b569d4463d0d7f4c88e9b5271476fed3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2918e22b6a49c4d9d878413621fa0d1456f369a41fe84adafc0fdcbc2d9996caaf531e42a102cb11fc1b1a6d07d8ec311dc3c6b0904cfbcc792dd21792824a0f
|
7
|
+
data.tar.gz: b3007e68797f9b89bfa75682eda9ceab964598e548e404c8e23039db2ee9e563e81fb19f99b4a47ffa3994114caa0f1afa36533e881230db6de7273d167ee656
|
data/README.md
CHANGED
@@ -1 +1,396 @@
|
|
1
1
|
# Tzu
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
Tzu commands must include Tzu and implement a `#call` method.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class MyCommand
|
9
|
+
include Tzu
|
10
|
+
|
11
|
+
def call(params)
|
12
|
+
"My Command Response - #{params.message}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
Tzu exposes `#run` at the class level, and returns an Outcome object.
|
18
|
+
The Outcome's `result` will be the return value of the command's `#call` method.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
outcome = MyCommand.run(message: 'Hello!')
|
22
|
+
#=> #<Tzu::Outcome @success=false, @result='My Command Response - Hello!'>
|
23
|
+
|
24
|
+
outcome.success? #=> true
|
25
|
+
outcome.failure? #=> false
|
26
|
+
outcome.result #=> 'My Command Response - Hello!'
|
27
|
+
```
|
28
|
+
|
29
|
+
## Validation
|
30
|
+
|
31
|
+
Tzu also provides an `invalid!` method that allows you to elegantly escape execution.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class MyCommand
|
35
|
+
include Tzu
|
36
|
+
|
37
|
+
def call(params)
|
38
|
+
invalid!('You did not do it') unless params[:message] == 'I did it!'
|
39
|
+
"My Command Response - #{params[:message]}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
When invoking Tzu with `#run`, `invalid!` will return an invalid Outcome.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
outcome = MyCommand.run(message: 'Hello!')
|
48
|
+
outcome.success? #=> false
|
49
|
+
outcome.failure? #=> true
|
50
|
+
outcome.type #=> :validation
|
51
|
+
outcome.result #=> { errors: 'You did not do it' }
|
52
|
+
```
|
53
|
+
|
54
|
+
When invoking Tzu with `#run!`, `invalid!` will throw a Tzu::Invalid error.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
outcome = MyCommand.run!(message: 'Hello!') #=> Tzu::Invalid: 'You did not do it'
|
58
|
+
```
|
59
|
+
|
60
|
+
If you use `invalid!` while catching an exception, you can pass the exception as an argument.
|
61
|
+
The exception's `#message` value will be passed along to the outcome.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class MyRescueCommand
|
65
|
+
include Tzu
|
66
|
+
|
67
|
+
def call(params)
|
68
|
+
raise StandardError.new('You did not do it')
|
69
|
+
rescue StandardError => e
|
70
|
+
invalid!(e)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
outcome = MyRescueCommand.run!(params_that_cause_error)
|
77
|
+
#=> Tzu::Invalid: 'You did not do it'
|
78
|
+
```
|
79
|
+
|
80
|
+
Note that if you pass a string to `invalid!`, it will coerce the result into a hash of the form:
|
81
|
+
|
82
|
+
```
|
83
|
+
{ errors: 'Error String' }
|
84
|
+
```
|
85
|
+
|
86
|
+
Any other type will simply be passed through.
|
87
|
+
|
88
|
+
## Passing Blocks
|
89
|
+
|
90
|
+
You can also pass a block to Tzu commands.
|
91
|
+
|
92
|
+
Successful commands will execute the `success` block, and invalid commands will execute the `invalid` block.
|
93
|
+
This is particularly useful in controllers:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
MyCommand.run(message: params[:message]) do
|
97
|
+
success do |result|
|
98
|
+
render(json: {message: result}.to_json, status: 200)
|
99
|
+
end
|
100
|
+
|
101
|
+
invalid do |errors|
|
102
|
+
render(json: errors.to_json, status: 422)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
## Hooks
|
108
|
+
|
109
|
+
Tzu commands accept `before`, `after`, and `around` hooks.
|
110
|
+
All hooks are executed in the order they are declared.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class MyCommand
|
114
|
+
include Tzu
|
115
|
+
|
116
|
+
around do |command|
|
117
|
+
puts 'Begin Around 1'
|
118
|
+
command.call
|
119
|
+
puts 'End Around 1'
|
120
|
+
end
|
121
|
+
|
122
|
+
around do |command|
|
123
|
+
puts 'Begin Around 2'
|
124
|
+
command.call
|
125
|
+
puts 'End Around 2'
|
126
|
+
end
|
127
|
+
|
128
|
+
before { puts 'Before 1' }
|
129
|
+
before { puts 'Before 2' }
|
130
|
+
|
131
|
+
after { puts 'After 1' }
|
132
|
+
after { puts 'After 2' }
|
133
|
+
|
134
|
+
def call(params)
|
135
|
+
puts "My Command Response - #{params[:message]}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
MyCommand.run(message: 'Hello!')
|
140
|
+
|
141
|
+
#=> Begin Around 1
|
142
|
+
#=> Begin Around 2
|
143
|
+
#=> Before 1
|
144
|
+
#=> Before 2
|
145
|
+
#=> My Command Response - Hello!
|
146
|
+
#=> After 1
|
147
|
+
#=> After 2
|
148
|
+
#=> End Around 2
|
149
|
+
#=> End Around 1
|
150
|
+
```
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
## Request objects
|
155
|
+
|
156
|
+
You can define a request object for your command using the `#request_object` method.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class MyValidatedCommand
|
160
|
+
include Tzu, Tzu::Validation
|
161
|
+
|
162
|
+
request_object MyRequestObject
|
163
|
+
|
164
|
+
def call(request)
|
165
|
+
"Name: #{request.name}, Age: #{request.age}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
Request objects must implement an initializer that accepts the command's parameters.
|
171
|
+
|
172
|
+
If you wish to validate your parameters, the Request object must implement `#valid?` and `#errors`.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
class MySimpleRequestObject
|
176
|
+
def initialize(params)
|
177
|
+
@params = params
|
178
|
+
end
|
179
|
+
|
180
|
+
def valid?
|
181
|
+
# Validate Parameters
|
182
|
+
end
|
183
|
+
|
184
|
+
def errors
|
185
|
+
# Why aren't I valid?
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
A very useful combination for request objects is Virtus.model and ActiveModel::Validations.
|
191
|
+
|
192
|
+
ActiveModel::Validations exposes all of the validators used on Rails models.
|
193
|
+
Virtus.model validates the types of your inputs, and also makes them available via dot notation.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class MyRequestObject
|
197
|
+
include Virtus.model
|
198
|
+
include ActiveModel::Validations
|
199
|
+
validates :name, :age, presence: :true
|
200
|
+
|
201
|
+
attribute :name, String
|
202
|
+
attribute :age, Integer
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
If your request object is invalid, Tzu will return an invalid outcome before reaching the `#call` method.
|
207
|
+
The invalid Outcome's result is populated by the request object's `#errors` method.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
class MyValidatedCommand
|
211
|
+
include Tzu, Tzu::Validation
|
212
|
+
|
213
|
+
request_object MyRequestObject
|
214
|
+
|
215
|
+
def call(request)
|
216
|
+
"Name: #{request.name}, Age: #{request.age}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
outcome = MyValidatedCommand.run(name: 'Charles')
|
221
|
+
#=> #<Command::Outcome @success=false, @result={:age=>["can't be blank"]}, @type=:validation>
|
222
|
+
|
223
|
+
outcome.success? #=> false
|
224
|
+
outcome.type? #=> :validation
|
225
|
+
outcome.result #=> {:age=>["can't be blank"]}
|
226
|
+
```
|
227
|
+
|
228
|
+
# Configure a sequence of Tzu commands
|
229
|
+
|
230
|
+
Tzu provides a declarative way of encapsulating sequential command execution.
|
231
|
+
|
232
|
+
Consider the following commands:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class SayMyName
|
236
|
+
include Tzu
|
237
|
+
|
238
|
+
def call(params)
|
239
|
+
"Hello, #{params[:name]}"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class MakeMeSoundImportant
|
244
|
+
include Tzu
|
245
|
+
|
246
|
+
def call(params)
|
247
|
+
"#{params[:boring_message]}! You are the most important citizen of #{params[:country]}!"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
The Tzu::Sequence provides a DSL for executing them in sequence:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class ProclaimMyImportance
|
256
|
+
include Tzu::Sequence
|
257
|
+
|
258
|
+
step SayMyName do
|
259
|
+
receives do |params|
|
260
|
+
{ name: params[:name] }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
step MakeMeSoundImportant do
|
265
|
+
receives do |params, prior_results|
|
266
|
+
{
|
267
|
+
boring_message: prior_results[:say_my_name],
|
268
|
+
country: params[:country]
|
269
|
+
}
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
Each command to be executed is defined as the first argument of `step`.
|
276
|
+
The `receives` method inside the `step` block allows you to mutate the parameters being passed into the command.
|
277
|
+
It is passed both the original parameters and a hash containing the results of prior commands.
|
278
|
+
|
279
|
+
By default, the keys of the `prior_results` hash are underscored/symbolized command names.
|
280
|
+
You can define your own keys using the `as` method.
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
step SayMyName do
|
284
|
+
as :first_command_key
|
285
|
+
receives do |params|
|
286
|
+
{ name: params[:name] }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
```
|
290
|
+
|
291
|
+
# Executing the sequence
|
292
|
+
|
293
|
+
By default, Sequences return the result of the final command within an Outcome object,
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
|
297
|
+
outcome.success? #=> true
|
298
|
+
outcome.result #=> 'Hello, Jessica! You are the most important citizen of Azerbaijan!'
|
299
|
+
```
|
300
|
+
|
301
|
+
Sequences can be configured to return the entire `prior_results` hash by passing `:take_all` to the `result` method.
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class ProclaimMyImportance
|
305
|
+
include Tzu::Sequence
|
306
|
+
|
307
|
+
step SayMyName do
|
308
|
+
receives do |params|
|
309
|
+
{ name: params[:name] }
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
step MakeMeSoundImportant do
|
314
|
+
receives do |params, prior_results|
|
315
|
+
{
|
316
|
+
boring_message: prior_results[:say_my_name],
|
317
|
+
country: params[:country]
|
318
|
+
}
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
result :take_all
|
323
|
+
end
|
324
|
+
|
325
|
+
outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
|
326
|
+
outcome.result
|
327
|
+
#=> { say_my_name: 'Hello, Jessica', make_me_sound_important: 'Hello, Jessica! You are the most important citizen of Azerbaijan!' }
|
328
|
+
```
|
329
|
+
|
330
|
+
You can also mutate the result into any form you choose by passing a block to `result`.
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
class ProclaimMyImportance
|
334
|
+
include Tzu::Sequence
|
335
|
+
|
336
|
+
step SayMyName do
|
337
|
+
receives do |params|
|
338
|
+
{ name: params[:name] }
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
step MakeMeSoundImportant do
|
343
|
+
as :final_command
|
344
|
+
receives do |params, prior_results|
|
345
|
+
{
|
346
|
+
boring_message: prior_results[:say_my_name],
|
347
|
+
country: params[:country]
|
348
|
+
}
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
result do |params, prior_results|
|
353
|
+
{
|
354
|
+
name: params[:name],
|
355
|
+
original_message: prior_results[:say_my_name],
|
356
|
+
message: "BULLETIN: #{prior_results[:final_command]}"
|
357
|
+
}
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
|
362
|
+
outcome.result
|
363
|
+
#=> { name: 'Jessica', original_message: 'Hello, Jessica', message: 'BULLETIN: Hello, Jessica! You are the most important citizen of Azerbaijan!' }
|
364
|
+
```
|
365
|
+
|
366
|
+
# Hooks for Sequences
|
367
|
+
|
368
|
+
Tzu sequences have the same `before`, `after`, and `around` hooks available in Tzu commands.
|
369
|
+
This is particularly useful for wrapping multiple commands in a transaction.
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
class ProclaimMyImportance
|
373
|
+
include Tzu::Sequence
|
374
|
+
|
375
|
+
around do |sequence|
|
376
|
+
ActiveRecord::Base.transaction do
|
377
|
+
sequence.call
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
step SayMyName do
|
382
|
+
receives do |params|
|
383
|
+
{ name: params[:name] }
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
step MakeMeSoundImportant do
|
388
|
+
receives do |params, prior_results|
|
389
|
+
{
|
390
|
+
boring_message: prior_results[:say_my_name],
|
391
|
+
country: params[:country]
|
392
|
+
}
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
```
|
data/lib/tzu.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
require 'tzu/core_extensions/string'
|
2
|
+
require 'tzu/run_methods'
|
1
3
|
require 'tzu/failure'
|
2
4
|
require 'tzu/hooks'
|
3
5
|
require 'tzu/invalid'
|
4
6
|
require 'tzu/match'
|
5
|
-
require 'tzu/
|
7
|
+
require 'tzu/sequence'
|
8
|
+
require 'tzu/step'
|
6
9
|
require 'tzu/outcome'
|
7
10
|
require 'tzu/validation'
|
8
11
|
require 'tzu/validation_result'
|
@@ -10,45 +13,14 @@ require 'tzu/validation_result'
|
|
10
13
|
module Tzu
|
11
14
|
def self.included(base)
|
12
15
|
base.class_eval do
|
13
|
-
extend
|
16
|
+
extend RunMethods
|
14
17
|
include Hooks
|
18
|
+
include Validation
|
15
19
|
end
|
16
20
|
end
|
17
21
|
|
18
|
-
module ClassMethods
|
19
|
-
def run(params, *context, &block)
|
20
|
-
result = get_instance(*context).run(params)
|
21
|
-
if block
|
22
|
-
result.handle(&block)
|
23
|
-
else
|
24
|
-
result
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def run!(params, *context)
|
29
|
-
get_instance(*context).run!(params)
|
30
|
-
end
|
31
|
-
|
32
|
-
def get_instance(*context)
|
33
|
-
method = respond_to?(:build) ? :build : :new
|
34
|
-
send(method, *context)
|
35
|
-
end
|
36
|
-
|
37
|
-
def command_name(value = nil)
|
38
|
-
if value.nil?
|
39
|
-
@name ||= name.underscore.to_sym
|
40
|
-
else
|
41
|
-
@name = (value.presence && value.to_sym)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def command_name
|
47
|
-
self.class.command_name
|
48
|
-
end
|
49
|
-
|
50
22
|
def run(params)
|
51
|
-
run!(
|
23
|
+
run!(init_request_object(params))
|
52
24
|
rescue Failure => f
|
53
25
|
Outcome.new(false, f.errors, f.type)
|
54
26
|
end
|
@@ -63,26 +35,15 @@ module Tzu
|
|
63
35
|
raise
|
64
36
|
end
|
65
37
|
|
66
|
-
def
|
67
|
-
|
38
|
+
def command_name
|
39
|
+
self.class.command_name
|
68
40
|
end
|
69
41
|
|
70
42
|
private
|
71
43
|
|
72
|
-
def
|
73
|
-
|
74
|
-
return
|
44
|
+
def init_request_object(params)
|
45
|
+
request_klass = self.class.request_klass
|
46
|
+
return request_klass.new(params) if request_klass
|
75
47
|
params
|
76
48
|
end
|
77
|
-
|
78
|
-
# Get the name of a request class related to calling class
|
79
|
-
# ie. Tzus::MyNameSpace::MyTzu
|
80
|
-
# has Tzus::MyNameSpace::Requests::MyTzu
|
81
|
-
def request_class
|
82
|
-
namespace = self.class.name.deconstantize.constantize
|
83
|
-
request_object_name = "Requests::#{ self.class.name.demodulize}"
|
84
|
-
namespace.qualified_const_get(request_object_name)
|
85
|
-
rescue NameError
|
86
|
-
false
|
87
|
-
end
|
88
49
|
end
|