lite-command 2.1.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -8,10 +8,6 @@ Lite::Command provides an API for building simple and complex command based serv
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
- > [!WARNING]
12
- > Gem versions `~> 2.0` are borked.
13
- > Version `~> 2.1.0` is the suggested working version.
14
-
15
11
  ```ruby
16
12
  gem 'lite-command'
17
13
  ```
@@ -26,15 +22,18 @@ Or install it yourself as:
26
22
 
27
23
  ## Table of Contents
28
24
 
29
- * [Setup](#setup)
25
+ * [Configuration](#configuration)
26
+ * [Usage](#usage)
30
27
  * [Execution](#execution)
31
28
  * [Dynamic Faults](#dynamic-faults)
32
29
  * [Context](#context)
33
30
  * [Attributes](#attributes)
31
+ * [Validations](#validations)
34
32
  * [States](#states)
35
33
  * [Statuses](#statuses)
36
- * [Callbacks](#callbacks)
34
+ * [Hooks](#hooks)
37
35
  * [State Hooks](#status-hooks)
36
+ * [Attribute Hooks](#attribute-hooks)
38
37
  * [Execution Hooks](#execution-hooks)
39
38
  * [Status Hooks](#status-hooks)
40
39
  * [Children](#children)
@@ -43,28 +42,38 @@ Or install it yourself as:
43
42
  * [Results](#results)
44
43
  * [Examples](#examples)
45
44
  * [Disable Instance Calls](#disable-instance-calls)
46
- * [ActiveModel Validations](#activemodel-validations)
47
45
  * [Generator](#generator)
48
46
 
49
- ## Setup
47
+ ## Configuration
48
+
49
+ `rails g lite:command:install` will generate the following file in your application root:
50
+ `config/initalizers/lite_command.rb`
51
+
52
+ ```ruby
53
+ Lite::Command.configure do |config|
54
+ config.raise_dynamic_faults = true
55
+ end
56
+ ```
57
+
58
+ ## Usage
50
59
 
51
- Defining a command is as simple as inheriting the base class and
52
- adding a `call` method to a command object (required).
60
+ Defining a command is as simple as inheriting the base class and adding a `call` method
61
+ to a command object (required).
53
62
 
54
63
  ```ruby
55
- class CalculatePower < Lite::Command::Base
64
+ class DecryptSecretMessage < Lite::Command::Base
56
65
 
57
66
  def call
58
- if all_even_numbers?
59
- context.result = ctx.a ** ctx.b
67
+ if invalid_magic_numbers?
68
+ invalid!("Invalid crypto message")
60
69
  else
61
- invalid!("All values must be even")
70
+ context.decrypted_message = SecretMessage.decrypt(context.encrypted_message)
62
71
  end
63
72
  end
64
73
 
65
74
  private
66
75
 
67
- def all_even_numbers?
76
+ def invalid_magic_numbers?
68
77
  # Some logic...
69
78
  end
70
79
 
@@ -72,38 +81,38 @@ end
72
81
  ```
73
82
 
74
83
  > [!TIP]
75
- > You should make all of your domain logic private so that only the command API is exposed.
84
+ > You should treat all command as emphemeral objects, so you should think about making
85
+ > all of your domain logic private and leaving the default command API is exposed.
76
86
 
77
87
  ## Execution
78
88
 
79
- Executing a command can be done as an instance or class call.
80
- It returns the command instance in a frozen state.
81
- These will never call will never raise an execption, but will
82
- be kept track of in its internal state.
89
+ Executing a command can be done as an instance or class call. It returns the command instance
90
+ in a frozen state. These will never call will never raise an execption, but will be kept track
91
+ of in its internal state.
83
92
 
84
93
  ```ruby
85
- CalculatePower.call(...)
94
+ DecryptSecretMessage.call(...)
86
95
  # - or -
87
- CalculatePower.new(...).call
96
+ DecryptSecretMessage.new(...).call
88
97
 
89
98
  # On success, fault and exception:
90
- #=> <CalculatePower ...>
99
+ #=> <DecryptSecretMessage ...>
91
100
  ```
92
101
 
93
102
  > [!TIP]
94
- > Class calls is the prefered format due to its readability.
103
+ > Class calls is the prefered format due to its readability. Read the [Disable Instance Calls](#disable-instance-calls)
104
+ > section on how to prevent instance style calls.
95
105
 
96
- Commands can be called with a `!` bang method to raise a
97
- `Lite::Command::Fault` based exception or the original
98
- `StandardError` based exception.
106
+ Commands can be called with a `!` bang method to raise a `Lite::Command::Fault` or the
107
+ original `StandardError` based exceptions.
99
108
 
100
109
  ```ruby
101
- CalculatePower.call!(...)
110
+ DecryptSecretMessage.call!(...)
102
111
  # - or -
103
- CalculatePower.new(...).call!
112
+ DecryptSecretMessage.new(...).call!
104
113
 
105
114
  # On success:
106
- #=> <CalculatePower ...>
115
+ #=> <DecryptSecretMessage ...>
107
116
 
108
117
  # On fault:
109
118
  #=> raises Lite::Command::Fault
@@ -114,12 +123,12 @@ CalculatePower.new(...).call!
114
123
 
115
124
  ### Dynamic Faults
116
125
 
117
- You can enable dynamic faults named after your command. This is
118
- especially helpful for catching + running custom logic or filtering
119
- out specific errors from you APM service.
126
+ Dynamic faults are custom faults named after your command. This is especially
127
+ helpful for catching + running custom logic or filtering out specific
128
+ exceptions from your APM service.
120
129
 
121
130
  ```ruby
122
- class CalculatePower < Lite::Command::Base
131
+ class DecryptSecretMessage < Lite::Command::Base
123
132
 
124
133
  def call
125
134
  fail!("Some failure")
@@ -127,14 +136,17 @@ class CalculatePower < Lite::Command::Base
127
136
 
128
137
  private
129
138
 
139
+ # Disable raising dynamic faults on a per command basis.
140
+ # The `raise_dynamic_faults` configuration option must be
141
+ # enabled for this method to have any affect.
130
142
  def raise_dynamic_faults?
131
- true
143
+ false
132
144
  end
133
145
 
134
146
  end
135
147
 
136
- CalculatePower.call!(...)
137
- #=> raises CalculatePower::Failure
148
+ DecryptSecretMessage.call!(...)
149
+ #=> raises DecryptSecretMessage::Failure
138
150
  ```
139
151
 
140
152
  ## Context
@@ -147,57 +159,49 @@ of its children commands.
147
159
  > Attributes that do **NOT** exist on the context will return `nil`.
148
160
 
149
161
  ```ruby
150
- class CalculatePower < Lite::Command::Base
162
+ class DecryptSecretMessage < Lite::Command::Base
151
163
 
152
164
  def call
153
165
  # `ctx` is an alias to `context`
154
- context.result = ctx.a ** ctx.b
166
+ context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)
155
167
  end
156
168
 
157
169
  end
158
170
 
159
- cmd = CalculatePower.call(a: 2, b: 3)
160
- cmd.context.result #=> 8
161
- cmd.ctx.fake #=> nil
171
+ cmd = DecryptSecretMessage.call(encrypted_message: "a22j3nkenjk2ne2")
172
+ cmd.context.decrypted_message #=> "Hello World"
173
+ cmd.ctx.fake_message #=> nil
162
174
  ```
163
175
 
164
176
  ### Attributes
165
177
 
166
- Delegate methods for a cleaner command setup, type checking and
167
- argument requirements. Setup a contract by using the `attribute`
168
- method which automatically delegates to `context`.
169
-
170
- | Options | Values | Default | Description |
171
- | ---------- | ------ | ------- | ----------- |
172
- | `from` | Symbol, String | `:context` | The object containing the attribute. |
173
- | `types` | Symbol, String, Array, Proc | | The allowed class types of the attribute value. |
174
- | `required` | Symbol, String, Boolean, Proc | `false` | The attribute must be passed to the context or delegatable (no matter the value). |
175
- | `filled` | Symbol, String, Boolean, Proc, Hash | `false` | The attribute value must be not be `nil`. Prevent empty values using `{ empty: false }` |
176
-
177
- > [!NOTE]
178
- > If optioned with some similar to `filled: true, types: [String, NilClass]`
179
- > then `NilClass` for the `types` option will be removed automatically.
178
+ Delegate methods for a cleaner command setup by declaring `required` and
179
+ `optional` arguments. `required` only verifies that argument was pass to the
180
+ context or can be called via defined method or another delegated method.
181
+ Is an `:if` or `:unless` callable option on a `required` delegation evaluates
182
+ to false, it will be delegated as an `optional` attribute.
180
183
 
181
184
  ```ruby
182
- class CalculatePower < Lite::Command::Base
183
-
184
- attribute :remote_storage, required: true, filled: true, types: RemoteStorage
185
+ class DecryptSecretMessage < Lite::Command::Base
185
186
 
186
- attribute :a, :b
187
- attribute :c, :d, from: :remote_storage, types: [Integer, Float]
188
- attribute :x, :y, from: :local_storage, filled: { empty: false }, if: :signed_in?
187
+ required :user, :encrypted_message
188
+ required :secret_key, from: :user
189
+ required :algo, :algo_detector, if: :signed_in?
190
+ optional :version
189
191
 
190
192
  def call
191
- context.result =
192
- (a.to_i ** b.to_i) +
193
- (c.to_i + d.to_i) -
194
- (x.to_i + y.to_i)
193
+ context.decrypted_message = SecretMessage.decrypt(
194
+ encrypted_message,
195
+ decryption_key: ENV["DECRYPT_KEY"],
196
+ algo: algo,
197
+ version: version || 2
198
+ )
195
199
  end
196
200
 
197
201
  private
198
202
 
199
- def local_storage
200
- @local_storage ||= LocalStorage.new(x: 1, y: 1, z: 99)
203
+ def algo_detector
204
+ @algo_detector ||= AlgoDetector.new(encrypted_message)
201
205
  end
202
206
 
203
207
  def signed_in?
@@ -207,19 +211,52 @@ class CalculatePower < Lite::Command::Base
207
211
  end
208
212
 
209
213
  # With valid options:
210
- rs = RemoteStorage.new(c: 2, d: 2, j: 99)
211
- cmd = CalculatePower.call(a: 2, b: 2, remote_storage: rs)
212
- cmd.status #=> "success"
213
- cmd.context.result #=> 6
214
+ cmd = DecryptSecretMessage.call(user: user, encrypted_message: "ll23k2j3kcms", version: 9)
215
+ cmd.status #=> "success"
216
+ cmd.context.decrypted_message #=> "Hola Mundo"
214
217
 
215
218
  # With invalid options:
216
- cmd = CalculatePower.call
219
+ cmd = DecryptSecretMessage.call
217
220
  cmd.status #=> "invalid"
218
- cmd.reason #=> "Invalid context attributes"
221
+ cmd.reason #=> "Encrypted message is a required argument. User is an undefined argument..."
219
222
  cmd.metadata #=> {
220
- #=> context: ["a is required", "remote_storage must be filled"],
221
- #=> remote_storage: ["d type invalid"]
222
- #=> local_storage: ["is not defined or an attribute"]
223
+ #=> user: ["is a required argument", "is an undefined argument"],
224
+ #=> encrypted_message: ["is a required argument"]
225
+ #=> }
226
+ ```
227
+
228
+ ### Validations
229
+
230
+ The full power of active model valdations is available to validate
231
+ any and all delegated arguments.
232
+
233
+ ```ruby
234
+ class DecryptSecretMessage < Lite::Command::Base
235
+
236
+ required :encrypted_message
237
+ optional :version
238
+
239
+ validates :encrypted_message, length: 10..999
240
+ validates :version, inclusion: { in: %w[v1 v3 v8], allow_blank: true }
241
+
242
+ def call
243
+ context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)
244
+ end
245
+
246
+ end
247
+
248
+ # With valid options:
249
+ cmd = DecryptSecretMessage.call(encrypted_message: "ll23k2j3kcms", version: "v1")
250
+ cmd.status #=> "success"
251
+ cmd.context.decrypted_message #=> "Hola Mundo"
252
+
253
+ # With invalid options:
254
+ cmd = DecryptSecretMessage.call(encrypted_message: "idk", version: "v23")
255
+ cmd.status #=> "invalid"
256
+ cmd.reason #=> "Encrypted message is too short (minimum is 10 character). Version is not included in list..."
257
+ cmd.metadata #=> {
258
+ #=> user: ["is not included in list"],
259
+ #=> encrypted_message: ["is too short (minimum is 10 character)"]
223
260
  #=> }
224
261
  ```
225
262
 
@@ -237,7 +274,7 @@ cmd.metadata #=> {
237
274
  > States are automatically transitioned and should **NEVER** be altered manually.
238
275
 
239
276
  ```ruby
240
- cmd = CalculatePower.call
277
+ cmd = DecryptSecretMessage.call
241
278
  cmd.state #=> "complete"
242
279
 
243
280
  cmd.pending? #=> false
@@ -267,53 +304,55 @@ A status of `success` is returned even if the command has **NOT** been executed.
267
304
  > Metadata may also be passed to enrich your fault response.
268
305
 
269
306
  ```ruby
270
- class CalculatePower < Lite::Command::Base
307
+ class DecryptSecretMessage < Lite::Command::Base
271
308
 
272
309
  def call
273
- if ctx.a.nil? || ctx.b.nil?
274
- invalid!("An a and b parameter must be passed")
275
- elsif ctx.a < 1 || ctx.b < 1
276
- failure!("Parameters must be >= 1")
277
- elsif ctx.a == 1 || ctx.b == 1
278
- noop!(
279
- "Anything to the power of 1 is 1",
280
- { i18n: "some.key" }
281
- )
310
+ if context.encrypted_message.empty?
311
+ noop!("No message to decrypt")
312
+ elsif context.encrypted_message.start_with?("== womp")
313
+ invalid!("Invalid message start value", i18n: "gb.invalid_start_value")
314
+ elsif context.encrypted_message.algo?(OldAlgo)
315
+ failure!("Unsafe encryption algo detected")
282
316
  else
283
- ctx.result = ctx.a ** ctx.b
317
+ context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)
284
318
  end
285
- rescue DivisionError => e
286
- error!("Cathcing it myself")
319
+ rescue CryptoError => e
320
+ Apm.report_error(e)
321
+ error!("Failed decryption due to: #{e}")
287
322
  end
288
323
 
289
324
  end
290
325
 
291
- cmd = CalculatePower.call(a: 1, b: 3)
292
- cmd.status #=> "noop"
293
- cmd.reason #=> "Anything to the power of 1 is 1"
294
- cmd.metadata #=> { i18n: "some.key" }
326
+ cmd = DecryptSecretMessage.call(encrypted_message: "2jk3hjeh2hj2jh")
327
+ cmd.status #=> "invalid"
328
+ cmd.reason #=> "Invalid message start value"
329
+ cmd.metadata #=> { i18n: "gb.invalid_start_value" }
295
330
 
296
331
  cmd.success? #=> false
297
- cmd.noop? #=> true
298
- cmd.noop?("Other reason") #=> false
299
- cmd.invalid? #=> false
332
+ cmd.noop? #=> false
333
+ cmd.invalid? #=> true
334
+ cmd.invalid?("Other reason") #=> false
300
335
  cmd.failure? #=> false
301
336
  cmd.error? #=> false
302
337
 
303
338
  # `success` or `noop`
304
- cmd.ok? #=> true
339
+ cmd.ok? #=> false
305
340
  cmd.ok?("Other reason") #=> false
306
341
 
307
342
  # NOT `success`
308
343
  cmd.fault? #=> true
309
344
  cmd.fault?("Other reason") #=> false
345
+
346
+ # `invalid` or `failure` or `error`
347
+ cmd.bad? #=> true
348
+ cmd.bad?("Other reason") #=> false
310
349
  ```
311
350
 
312
- ## Callbacks
351
+ ## Hooks
313
352
 
314
- Use callbacks to run arbituary code at transition points and
315
- on finalized internals. The following is an example of the hooks
316
- called for a failed command with a successful child command.
353
+ Use hooks to run arbituary code at transition points and on finalized internals.
354
+ The following is an example of the hooks called for a failed command with a
355
+ successful child command.
317
356
 
318
357
  ```ruby
319
358
  -> 1. FooCommand.on_pending
@@ -332,11 +371,10 @@ called for a failed command with a successful child command.
332
371
 
333
372
  ### Status Hooks
334
373
 
335
- Define one or more callbacks that are called during transitions
336
- between states.
374
+ Define one or more callbacks that are called during transitions between states.
337
375
 
338
376
  ```ruby
339
- class CalculatePower < Lite::Command::Base
377
+ class DecryptSecretMessage < Lite::Command::Base
340
378
 
341
379
  def call
342
380
  # ...
@@ -363,12 +401,32 @@ class CalculatePower < Lite::Command::Base
363
401
  end
364
402
  ```
365
403
 
404
+ ### Attribute Hooks
405
+
406
+ Define before attribtue validation callbacks.
407
+
408
+ ```ruby
409
+ class DecryptSecretMessage < Lite::Command::Base
410
+
411
+ def call
412
+ # ...
413
+ end
414
+
415
+ private
416
+
417
+ def on_before_validation
418
+ # eg: Normalize context data
419
+ end
420
+
421
+ end
422
+ ```
423
+
366
424
  ### Execution Hooks
367
425
 
368
426
  Define before and after callbacks to call around execution.
369
427
 
370
428
  ```ruby
371
- class CalculatePower < Lite::Command::Base
429
+ class DecryptSecretMessage < Lite::Command::Base
372
430
 
373
431
  def call
374
432
  # ...
@@ -393,7 +451,7 @@ Define one or more callbacks that are called after execution for
393
451
  specific statuses.
394
452
 
395
453
  ```ruby
396
- class CalculatePower < Lite::Command::Base
454
+ class DecryptSecretMessage < Lite::Command::Base
397
455
 
398
456
  def call
399
457
  # ...
@@ -429,16 +487,16 @@ end
429
487
 
430
488
  ## Children
431
489
 
432
- When building complex commands, its best that you pass the
433
- parents context to the child command (unless neccessary) so
434
- that it gains automated indexing and the parents `cmd_id`.
490
+ When building complex commands, its best that you pass the parents context to the
491
+ child command (unless neccessary) so that it gains automated indexing and the
492
+ parents `cmd_id`.
435
493
 
436
494
  ```ruby
437
- class CalculatePower < Lite::Command::Base
495
+ class DecryptSecretMessage < Lite::Command::Base
438
496
 
439
497
  def call
440
- context.merge!(some_other: "required value")
441
- CalculateSqrt.call(context)
498
+ context.merge!(decryption_key: ENV["DECRYPT_KEY"])
499
+ ValidateSecretMessage.call(context)
442
500
  end
443
501
 
444
502
  end
@@ -446,26 +504,24 @@ end
446
504
 
447
505
  ### Throwing Faults
448
506
 
449
- Throwing faults allows you to bubble up child faults up to the parent.
450
- Use it to create branches within your logic and create clean tracing
451
- of your command results. You can use `throw!` as a catch-all or any
452
- of the bang status method `failure!`. Any `reason` and `metadata` will
453
- be bubbled up from the original fault.
507
+ Throwing faults allows you to bubble up child faults up to the parent. Use it to create
508
+ branches within your logic and create clean tracing of your command results. You can use
509
+ `throw!` as a catch-all or any of the bang status method `failure!`. Any `reason` and
510
+ `metadata` will be bubbled up from the original fault.
454
511
 
455
512
  ```ruby
456
- class CalculatePower < Lite::Command::Base
513
+ class DecryptSecretMessage < Lite::Command::Base
457
514
 
458
515
  def call
459
- command = CalculateSqrt.call(context.merge!(some_other: "required value"))
516
+ context.merge!(decryption_key: ENV["DECRYPT_KEY"])
517
+ cmd = ValidateSecretMessage.call(context)
460
518
 
461
- if command.noop?("Sqrt of 1 is 1")
462
- # Manually throw a specific fault
463
- invalid!(command)
519
+ if cmd.invalid?("Invalid magic numbers")
520
+ error!(cmd) # Manually throw a specific fault
464
521
  elsif command.fault?
465
- # Automatically throws a matching fault
466
- throw!(command)
522
+ throw!(cmd) # Automatically throws a matching fault
467
523
  else
468
- # Success, do nothing
524
+ context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)
469
525
  end
470
526
  end
471
527
 
@@ -483,14 +539,14 @@ This is useful for composing multiple steps into one call.
483
539
  > so its no different than just passing the context forward. To change
484
540
  > this behavior, just override the `ok?` method with you logic, eg: just `success`
485
541
 
486
- > [!IMPORTANT]
542
+ > [!WARNING]
487
543
  > Do **NOT** define a call method in this class. The sequence logic is
488
544
  > automatically defined by the sequence class.
489
545
 
490
546
  ```ruby
491
547
  class ProcessCheckout < Lite::Command::Sequence
492
548
 
493
- attribute :user, required: true, filled: true
549
+ required :user
494
550
 
495
551
  step FinalizeInvoice
496
552
  step ChargeCard, if: :card_available?
@@ -513,13 +569,12 @@ seq = ProcessCheckout.call(...)
513
569
 
514
570
  ## Results
515
571
 
516
- During any point in the lifecyle of a command, `to_hash` can be
517
- called to dump out the current values. The `index` value is
518
- auto-incremented and the `cmd_id` is static when its passed to
519
- child commands. This helps with debugging and logging.
572
+ During any point in the lifecyle of a command, `to_hash` can be called to dump out
573
+ the current values. The `index` value is auto-incremented and the `cmd_id` is static
574
+ when its passed to child commands. This helps with debugging and logging.
520
575
 
521
576
  ```ruby
522
- command = CalculatePower.call(...)
577
+ command = DecryptSecretMessage.call(...)
523
578
  command.to_hash #=> {
524
579
  #=> index: 1,
525
580
  #=> cmd_id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
@@ -543,7 +598,7 @@ command.to_hash #=> {
543
598
  ### Disable Instance Calls
544
599
 
545
600
  ```ruby
546
- class CalculatePower < Lite::Command::Base
601
+ class DecryptSecretMessage < Lite::Command::Base
547
602
 
548
603
  private_class_method :new
549
604
 
@@ -553,48 +608,10 @@ class CalculatePower < Lite::Command::Base
553
608
 
554
609
  end
555
610
 
556
- CalculatePower.new(...).call
611
+ DecryptSecretMessage.new(...).call
557
612
  #=> raise NoMethodError
558
613
  ```
559
614
 
560
- ### ActiveModel Validations
561
-
562
- ```ruby
563
- class CalculatePower < Lite::Command::Base
564
- include ActiveModel::Validations
565
-
566
- validates :a, :b, presence: true
567
-
568
- def call
569
- # ...
570
- end
571
-
572
- def read_attribute_for_validation(key)
573
- context.public_send(key)
574
- end
575
-
576
- private
577
-
578
- def on_before_execution
579
- return if valid?
580
-
581
- invalid!(
582
- errors.full_messages.to_sentence,
583
- errors.to_hash
584
- )
585
- end
586
-
587
- end
588
-
589
- CalculatePower.call!
590
-
591
- # With `validate!`
592
- #=> raise ActiveRecord::RecordInvalid
593
-
594
- # With `valid?`
595
- #=> raise Lite::Command::Invalid
596
- ```
597
-
598
615
  ## Generator
599
616
 
600
617
  `rails g command NAME` will generate the following file:
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class InstallGenerator < Rails::Generators::Base
6
+
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ def copy_initializer_file
10
+ copy_file("install.rb", "config/initializers/lite_command.rb")
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Lite::Command.configure do |config|
4
+ config.raise_dynamic_faults = false
5
+ end