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