lite-command 1.5.0 → 2.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
@@ -4,7 +4,6 @@
4
4
  [![Build Status](https://travis-ci.org/drexed/lite-command.svg?branch=master)](https://travis-ci.org/drexed/lite-command)
5
5
 
6
6
  Lite::Command provides an API for building simple and complex command based service objects.
7
- It provides extensions for handling errors and memoization to improve your object workflow productivity.
8
7
 
9
8
  ## Installation
10
9
 
@@ -25,273 +24,128 @@ Or install it yourself as:
25
24
  ## Table of Contents
26
25
 
27
26
  * [Setup](#setup)
28
- * [Simple](#simple)
29
- * [Complex](#complex)
30
- * [Procedure](#procedure)
31
- * [Extensions](#extensions)
27
+ * [Execution](#execution)
28
+ * [Context](#context)
29
+ * [Internals](#Internals)
30
+ * [Generator](#generator)
32
31
 
33
32
  ## Setup
34
33
 
35
- `rails g command NAME` will generate the following file:
36
-
37
- ```erb
38
- app/commands/[NAME]_command.rb
39
- ```
40
-
41
- If a `ApplicationCommand` file in the `app/commands` directory is available, the
42
- generator will create file that inherit from `ApplicationCommand` if not it will
43
- fallback to `Lite::Command::Complex`.
44
-
45
- ## Simple
46
-
47
- Simple commands are a traditional command service call objects.
48
- It only exposes a `call` method that returns a value.
49
-
50
- **Setup**
34
+ Defining a command is as simple as adding a call method.
51
35
 
52
36
  ```ruby
53
- class CalculatePower < Lite::Command::Simple
37
+ class CalculatePower < Lite::Command::Base
54
38
 
55
- # NOTE: This `execute` class method is required to use with call
56
- def self.execute(a, b)
57
- a**b
39
+ def call
40
+ # TODO: implement calculator
58
41
  end
59
42
 
60
43
  end
61
44
  ```
62
45
 
63
- **Callers**
64
-
65
- ```ruby
66
- CalculatePower.execute(2, 2) #=> 4
67
-
68
- # - or -
69
-
70
- CalculatePower.call(2, 3) #=> 8
71
- ```
72
-
73
- ## Complex
74
-
75
- Complex commands are powerful command service call objects.
76
- It can be extended to use error, memoization, and propagation mixins.
77
-
78
- **Setup**
79
-
80
- ```ruby
81
- class SearchMovies < Lite::Command::Complex
82
-
83
- attr_reader :name
84
-
85
- def initialize(name)
86
- @name = name
87
- end
88
-
89
- # NOTE: This `execute` instance method is required to use with call
90
- def execute
91
- { generate_fingerprint => movies_by_name }
92
- end
93
-
94
- private
95
-
96
- def movies_by_name
97
- HTTP.get("http://movies.com?title=#{name}")
98
- end
46
+ ## Execution
99
47
 
100
- def generate_fingerprint
101
- Digest::MD5.hexdigest(movies_by_name)
102
- end
103
-
104
- end
105
- ```
48
+ Executing a command can be done as an instance or class call.
49
+ It returns the command instance in a forzen state.
50
+ These will never call will never raise an execption, but will
51
+ be kept track of in its internal state.
106
52
 
107
- **Caller**
53
+ **NOTE:** Class calls is the prefered format due to its readability.
108
54
 
109
55
  ```ruby
110
- SearchMovies.execute('Toy Story') #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
111
-
112
- # - or -
113
-
114
- command = SearchMovies.new('Toy Story')
115
- command.called? #=> false
116
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
117
- command.called? #=> true
56
+ # Class call
57
+ CalculatePower.call(..args)
118
58
 
119
- # - or -
59
+ # Instance call
60
+ caculator = CalculatePower.new(..args).call
120
61
 
121
- command = SearchMovies.call('Toy Story')
122
- command.called? #=> true
123
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
62
+ #=> <CalculatePower ...>
124
63
  ```
125
64
 
126
- **Result**
65
+ Commands can be called with a `!` bang method to raise a
66
+ `Lite::Command::Fault` based exception or the original
67
+ `StandardError` based exception.
127
68
 
128
69
  ```ruby
129
- command = SearchMovies.new('Toy Story')
130
- command.result #=> nil
131
-
132
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
133
- command.result #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
134
-
135
- command.recall! #=> Clears the `call`, `cache`, `errors` variables and then re-performs the call
136
- command.result #=> { 'fingerprint_2' => [ 'Toy Story 2', ... ] }
70
+ CalculatePower.call!(..args)
71
+ #=> raises Lite::Command::Fault
137
72
  ```
138
73
 
139
- ## Procedure
140
-
141
- Procedures are used to run a collection of commands. It uses the the complex procedure API
142
- so it has access to all the methods. The `execute` method is already defined to
143
- handle most common procedure steps. It can be use directly or subclassed.
74
+ ## Context
144
75
 
145
- **Setup**
146
-
147
- ```ruby
148
- class SearchChannels < Lite::Command::Procedure; end
149
- ```
76
+ Accessing the call arguments can be done through its internal context.
77
+ It can be used as internal storage to be accessed by it self and any
78
+ of its children commands.
150
79
 
151
80
  ```ruby
152
- commands = [DisneyChannel.new, EspnChannel.new(current_station), MtvChannel.new]
153
-
154
- procedure = SearchChannels.call(*commands)
155
- procedure.result #=> ['disney: #3', 'espn: #59', 'mtv: #212']
156
- procedure.steps #=> [<DisneyChannel @result="...">, <EspnChannel @result="...">, <MtvChannel @result="...">]
157
-
158
- # - or -
159
-
160
- # If the errors extension is added you can stop the procedure at first failure.
161
- procedure = SearchChannels.new(*commands)
162
- procedure.exit_on_failure = true
163
- procedure.call
164
- procedure.result #=> ['disney: #3']
165
- procedure.failed_steps #=> [{ index: 1, step: 2, name: 'ErrorChannel', args: [current_station], errors: ['field error message'] }]
166
- ```
167
-
168
- ## Extensions
81
+ class CalculatePower < Lite::Command::Base
169
82
 
170
- Extend complex (and procedures) base command with any of the following extensions:
171
-
172
- ### Errors (optional)
173
-
174
- Learn more about using [Lite::Errors](https://github.com/drexed/lite-errors)
175
-
176
- **Setup**
177
-
178
- ```ruby
179
- class SearchMovies < Lite::Command::Complex
180
- include Lite::Command::Extensions::Errors
181
-
182
- # ... ommited ...
183
-
184
- private
185
-
186
- # Add a explicit and/or exception errors to the error pool
187
- def generate_fingerprint
188
- if movies_by_name.nil?
189
- errors.add(:fingerprint, 'invalid md5 request value')
190
- else
191
- Digest::MD5.hexdigest(movies_by_name)
192
- end
193
- rescue ArgumentError => exception
194
- merge_exception!(exception, key: :custom_error_key)
83
+ def call
84
+ context.result = context.a ** context.b
195
85
  end
196
86
 
197
87
  end
198
- ```
199
-
200
- **Instance Callers**
201
-
202
- ```ruby
203
- command = SearchMovies.call('Toy Story')
204
- command.errors #=> Lite::Errors::Messages object
205
-
206
- command.validate! #=> Raises Lite::Command::ValidationError if it has any errors
207
- command.valid? #=> Alias for validate!
208
-
209
- command.errored? #=> false
210
- command.success? #=> true
211
- command.failure? #=> Checks that it has been called and has errors
212
- command.status #=> :failure
213
-
214
- command.result! #=> Raises Lite::Command::ValidationError if it has any errors, if not it returns the result
215
-
216
- # Use the following to merge errors from other commands or models
217
- # with the default direction being `:from`
218
- command.merge_errors!(command_2)
219
- user_model.merge_errors!(command, direction: :to)
220
- ```
221
-
222
- **Block Callers**
223
-
224
- ```ruby
225
- # Useful for controllers or actions that depend on states.
226
- SearchMovies.perform('Toy Story') do |result, success, failure|
227
- success.call { redirect_to(movie_path, notice: "Movie can be found at: #{result}") }
228
- failure.call { redirect_to(root_path, notice: "Movie cannot be found at: #{result}") }
229
- end
230
- ```
231
-
232
- ### Propagation (optional)
233
-
234
- Propagation methods help you perform an action on an object. If successful is
235
- returns the result else it adds the object errors to the form object. Available
236
- propagation methods are:
237
- - `assign_and_return!(object, params)`
238
- - `create_and_return!(klass, params)`
239
- - `update_and_return!(object, params)`
240
- - `destroy_and_return!(object)`
241
- - `archive_and_return!(object)` (if using Lite::Archive)
242
- - `save_and_return!(object)`
243
-
244
- **Setup**
245
-
246
- ```ruby
247
- class SearchMovies < Lite::Command::Complex
248
- include Lite::Command::Extensions::Errors
249
- include Lite::Command::Extensions::Propagation
250
-
251
- # ... ommited ...
252
-
253
- def execute
254
- create_and_return!(User, name: 'John Doe')
255
- end
256
88
 
257
- end
89
+ command = CalculatePower.call(a: 2, b: 3)
90
+ command.context.result #=> 8
258
91
  ```
259
92
 
260
- ### Memoize (optional)
261
-
262
- Learn more about using [Lite::Memoize](https://github.com/drexed/lite-memoize)
93
+ ## Internals
94
+
95
+ #### States
96
+ State represents the state of the executable code. Once `execute`
97
+ is ran, it will always `complete` or `dnf` if a fault is thrown by a
98
+ child command.
99
+
100
+ - `pending`
101
+ - Command objects that have been initialized.
102
+ - `executing`
103
+ - Command objects actively executing code.
104
+ - `complete`
105
+ - Command objects that executed to completion.
106
+ - `dnf`
107
+ - Command objects that could NOT be executed to completion.
108
+ This could be as a result of a fault/exception on the
109
+ object itself or one of its children.
110
+
111
+ #### Statuses
112
+
113
+ Status represents the state of the callable code. If no fault
114
+ is thrown then a status of `success` is returned even if `call`
115
+ has not been executed. The list of status include (by severity):
116
+
117
+ - `success`
118
+ - No fault or exception
119
+ - `noop`
120
+ - Noop represents skipping completion of call execution early
121
+ an unsatisfied condition or logic check where there is no
122
+ point on proceeding.
123
+ - **eg:** account is sample: skip since its a non-alterable record
124
+ - `invalid`
125
+ - Invalid represents a stoppage of call execution due to
126
+ missing, bad, or corrupt data.
127
+ - **eg:** user not found: stop since rest of the call cant be executed
128
+ - `failure`
129
+ - Failure represents a stoppage of call execution due to
130
+ an unsatisfied condition or logic check where it blocks
131
+ proceeding any further.
132
+ - **eg:** record not found: stop since there is nothing todo
133
+ - `error`
134
+ - Error represents a caught exception for a call execution
135
+ that could not complete.
136
+ - **eg:** ApiServerError: stop since there was a 3rd party issue
137
+
138
+ ## Generator
263
139
 
264
- **Setup**
265
-
266
- ```ruby
267
- class SearchMovies < Lite::Command::Complex
268
- include Lite::Command::Extensions::Memoize
269
-
270
- # ... ommited ...
271
-
272
- private
273
-
274
- # Sets the value in the cache
275
- # Subsequent method calls gets the cached value
276
- # This saves you the extra external HTTP.get call
277
- def movies_by_name
278
- cache.memoize { HTTP.get("http://movies.com?title=#{name}") }
279
- end
280
-
281
- # Gets the value in the cache
282
- def generate_fingerprint
283
- Digest::MD5.hexdigest(movies_by_name)
284
- end
140
+ `rails g command NAME` will generate the following file:
285
141
 
286
- end
142
+ ```erb
143
+ app/commands/[NAME]_command.rb
287
144
  ```
288
145
 
289
- **Callers**
290
-
291
- ```ruby
292
- command = SearchMovies.call('Toy Story')
293
- command.cache #=> Lite::Memoize::Instance object
294
- ```
146
+ If a `ApplicationCommand` file in the `app/commands` directory is available, the
147
+ generator will create file that inherit from `ApplicationCommand` if not it will
148
+ fallback to `Lite::Command::Base`.
295
149
 
296
150
  ## Development
297
151
 
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
data/bin/console CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
- require 'lite/command'
4
+ require "bundler/setup"
5
+ require "lite/command"
6
6
 
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
@@ -11,5 +11,5 @@ require 'lite/command'
11
11
  # require "pry"
12
12
  # Pry.start
13
13
 
14
- require 'irb'
14
+ require "irb"
15
15
  IRB.start(__FILE__)
@@ -3,12 +3,12 @@
3
3
  module Rails
4
4
  class CommandGenerator < Rails::Generators::NamedBase
5
5
 
6
- source_root File.expand_path('../templates', __FILE__)
7
- check_class_collision suffix: 'Command'
6
+ source_root File.expand_path("../templates", __FILE__)
7
+ check_class_collision suffix: "Command"
8
8
 
9
9
  def copy_files
10
- path = File.join('app', 'commands', class_path, "#{file_name}_command.rb")
11
- template('command.rb.tt', path)
10
+ path = File.join("app", "commands", class_path, "#{file_name}_command.rb")
11
+ template("command.rb.tt", path)
12
12
  end
13
13
 
14
14
  private
@@ -18,7 +18,7 @@ module Rails
18
18
  end
19
19
 
20
20
  def remove_possible_suffix(name)
21
- name.sub(/_?command$/i, '')
21
+ name.sub(/_?command$/i, "")
22
22
  end
23
23
 
24
24
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>Command < <%= ApplicationCommand.to_s rescue 'Lite::Command::Complex' %>
4
+ class <%= class_name %>Command < <%= ApplicationCommand.to_s rescue 'Lite::Command::Base' %>
5
5
  end
6
6
  <% end -%>
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class Base
6
+
7
+ def self.inherited(base)
8
+ super
9
+
10
+ base.include Lite::Command::Internals::Callable
11
+ base.include Lite::Command::Internals::Executable
12
+ base.include Lite::Command::Internals::Resultable
13
+
14
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
+ # eg: Users::ResetPassword::Fault
16
+ class #{base}::Fault < Lite::Command::Fault; end
17
+ RUBY
18
+
19
+ FAULTS.each do |f|
20
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
21
+ # eg: Users::ResetPassword::Noop < Users::ResetPassword::Fault
22
+ class #{base}::#{f.capitalize} < #{base}::Fault; end
23
+ RUBY
24
+ end
25
+ end
26
+
27
+ attr_reader :context
28
+
29
+ def initialize(context = {})
30
+ @context = Lite::Command::Context.build(context)
31
+ end
32
+
33
+ private
34
+
35
+ def additional_result_data
36
+ {} # Define in your class to add additional info to result hash
37
+ end
38
+
39
+ def on_before_execution
40
+ # Define in your class to run code before execution
41
+ end
42
+
43
+ def on_after_execution
44
+ # Define in your class to run code after execution
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct" unless defined?(OpenStruct)
4
+
5
+ module Lite
6
+ module Command
7
+ class Context < OpenStruct
8
+
9
+ def self.init(attributes = {})
10
+ # To save memory and speed up the access to an attribute, the accessor methods
11
+ # of an attribute are lazy loaded at certain points. This means that the methods
12
+ # are defined only when a set of defined actions are triggered. This allows context
13
+ # to only define the minimum amount of required methods to make your data structure work
14
+ os = new(attributes)
15
+ os.methods(false)
16
+ os
17
+ end
18
+
19
+ def self.build(attributes = {})
20
+ return attributes if attributes.is_a?(self) && !attributes.frozen?
21
+
22
+ init(attributes.to_h)
23
+ end
24
+
25
+ def merge!(attributes = {})
26
+ attrs = attributes.is_a?(self.class) ? attributes.to_h : attributes
27
+ attrs.each { |k, v| self[k] = v }
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+
6
+ # Fault represent a stoppage of a call execution. This error should
7
+ # not be raised directly since it wont provide any context. Use
8
+ # `Noop`, `Invalid`, `Failure`, and `Error` to signify severity.
9
+ class Fault < StandardError
10
+
11
+ attr_reader :faulter, :thrower, :reason
12
+
13
+ def initialize(faulter, thrower, reason)
14
+ super(reason)
15
+
16
+ @faulter = faulter
17
+ @thrower = thrower
18
+ @reason = reason
19
+ end
20
+
21
+ def fault_klass
22
+ @fault_klass ||= self.class.name.split("::").last
23
+ end
24
+
25
+ def fault_name
26
+ @fault_name ||= fault_klass.downcase
27
+ end
28
+
29
+ end
30
+
31
+ # Noop represents skipping completion of call execution early
32
+ # an unsatisfied condition or logic check where there is no
33
+ # point on proceeding.
34
+ # eg: account is sample: skip since its a non-alterable record
35
+ class Noop < Fault; end
36
+
37
+ # Invalid represents a stoppage of call execution due to
38
+ # missing, bad, or corrupt data.
39
+ # eg: user not found: stop since rest of the call cant be executed
40
+ class Invalid < Fault; end
41
+
42
+ # Failure represents a stoppage of call execution due to
43
+ # an unsatisfied condition or logic check where it blocks
44
+ # proceeding any further.
45
+ # eg: record not found: stop since there is nothing todo
46
+ class Failure < Fault; end
47
+
48
+ # Error represents a caught exception for a call execution
49
+ # that could not complete.
50
+ # eg: ApiServerError: stop since there was a 3rd party issue
51
+ class Error < Fault; end
52
+
53
+ end
54
+ end