lite-command 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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