lite-command 2.1.3 → 3.0.1

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