tzu 0.0.2.0 → 0.1.0.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: e476045f4ece3c811b3fdaedad37f1a035f12e13
4
- data.tar.gz: b699bc51b73743dcaa26a42394fe8a086018dd7b
3
+ metadata.gz: 9fc0ed30a4b98b00a486d4fa819753e2c3903975
4
+ data.tar.gz: 79ec5b04b569d4463d0d7f4c88e9b5271476fed3
5
5
  SHA512:
6
- metadata.gz: 2838ccd7ae2ed0cab8e6827ddeaa765520a55a49f42865f7739d96142e232dfbe96e4ea0d6ca20996f3a39fc6258124d90e39fde3adc0cdde769b341d2e0efdf
7
- data.tar.gz: 337506517ab4d5e0dc25b6a1e7a6fab72aeb71dc47129d8adfbd304a7e579bb4416680440fe7b6109624ea0735ce3ce8381d80461773230bf49dc4d97426c18d
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/organizer'
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 ClassMethods
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!(request_object(params))
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 fail!(type, data={})
67
- raise Failure.new(type, data)
38
+ def command_name
39
+ self.class.command_name
68
40
  end
69
41
 
70
42
  private
71
43
 
72
- def request_object(params)
73
- klass = request_class
74
- return klass.new(params) if klass && !params.is_a?(klass)
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