lite-command 1.4.1 → 2.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.
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 mixins for handling errors and memoization to improve your object workflow productivity.
8
7
 
9
8
  ## Installation
10
9
 
@@ -25,260 +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 build quick class based calls but cannot be extended.
48
- This is more of a traditional command service call as it only exposes a `call` method.
34
+ Defining a command is as simple as adding a call method.
49
35
 
50
36
  ```ruby
51
- class SearchMovies < Lite::Command::Simple
37
+ class CalculatePower < Lite::Command::Base
52
38
 
53
- # NOTE: This class method is required
54
- def self.execute(*args)
55
- { generate_fingerprint => movies_by_name }
39
+ def call
40
+ # TODO: implement calculator
56
41
  end
57
42
 
58
43
  end
59
44
  ```
60
45
 
61
- **Caller**
46
+ ## Execution
62
47
 
63
- ```ruby
64
- SearchMovies.call('Toy Story')
65
- ```
66
-
67
- ## Complex
68
-
69
- Complex commands can be used in instance and class based calls and
70
- extended with access to errors and memoization.
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.
71
52
 
72
- You will then need to fill this class with the required `execute` method as shown below:
53
+ **NOTE:** Class calls is the prefered format due to its readability.
73
54
 
74
55
  ```ruby
75
- class SearchMovies < Lite::Command::Complex
76
-
77
- def initialize(name)
78
- @name = name
79
- end
56
+ # Class call
57
+ CalculatePower.call(..args)
80
58
 
81
- # NOTE: This instance method is required
82
- def execute
83
- { generate_fingerprint => movies_by_name }
84
- end
85
-
86
- private
59
+ # Instance call
60
+ caculator = CalculatePower.new(..args).call
87
61
 
88
- def movies_by_name
89
- HTTP.get("http://movies.com?title=#{title}")
90
- end
91
-
92
- def generate_fingerprint
93
- Digest::MD5.hexdigest(movies_by_name)
94
- end
95
-
96
- end
97
- ```
98
-
99
- **Caller**
100
-
101
- ```ruby
102
- command = SearchMovies.new('Toy Story')
103
- command.called? #=> false
104
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
105
- command.called? #=> true
106
-
107
- # - or -
108
-
109
- command = SearchMovies.call('Toy Story')
110
- command.called? #=> true
111
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
112
-
113
- # - or -
114
-
115
- # Useful when you are not using the Errors mixin as its a one time access call.
116
- # Very similar to the simple command builder.
117
- SearchMovies.execute('Toy Story') #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
62
+ #=> <CalculatePower ...>
118
63
  ```
119
64
 
120
- **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.
121
68
 
122
69
  ```ruby
123
- command = SearchMovies.new('Toy Story')
124
- command.result #=> nil
125
-
126
- command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
127
- command.result #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
128
-
129
- command.recall! #=> Clears the call, cache, errors, and then re-performs the call
130
- command.result #=> { 'fingerprint_2' => [ 'Toy Story 2', ... ] }
131
- ```
132
-
133
- ## Procedure
134
-
135
- Procedures run a collection of commands. It uses the the complex procedure API
136
- so it has access to all the methods. The `execute` method is already defined to
137
- handle most common procedure steps. It can be use directly or subclassed.
138
-
139
- ```ruby
140
- class SearchChannels < Lite::Command::Procedure; end
141
-
142
- procedure = SearchChannels.call(
143
- DisneyChannel.new,
144
- EspnChannel.new(current_station),
145
- MtvChannel.new
146
- )
147
-
148
- procedure.result #=> ['disney: #3', 'espn: #59', 'mtv: #212']
149
- procedure.steps #=> [<DisneyChannel @result="...">, <EspnChannel @result="...">, <MtvChannel @result="...">]
150
-
151
- # If the errors extension is added you can stop the procedure at first failure.
152
- procedure = SearchChannels.new(
153
- DisneyChannel.new,
154
- ErrorChannel.new(current_station),
155
- MtvChannel.new
156
- )
157
-
158
- procedure.exit_on_failure = true
159
- procedure.call
160
- procedure.result #=> ['disney: #3']
161
- procedure.failed_steps #=> [{ index: 1, step: 2, name: 'ErrorChannel', args: [current_station], errors: ['field error message'] }]
70
+ CalculatePower.call!(..args)
71
+ #=> raises Lite::Command::Fault
162
72
  ```
163
73
 
164
- ## Extensions
165
-
166
- Extend complex (and procedures) base command with any of the following extensions:
74
+ ## Context
167
75
 
168
- ### Errors (optional)
169
-
170
- Learn more about using [Lite::Errors](https://github.com/drexed/lite-errors)
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.
171
79
 
172
80
  ```ruby
173
- class SearchMovies < Lite::Command::Complex
174
- include Lite::Command::Extensions::Errors
175
-
176
- # ... ommited ...
81
+ class CalculatePower < Lite::Command::Base
177
82
 
178
- private
179
-
180
- # Add a fingerprint error to the error pool
181
- def generate_fingerprint
182
- errors.add(:fingerprint, 'invalid md5 request value') if movies_by_name.nil?
183
- Digest::MD5.hexdigest(movies_by_name)
184
- rescue ArgumentError => e
185
- merge_exception!(e, key: :custom_key)
83
+ def call
84
+ context.result = context.a ** context.b
186
85
  end
187
86
 
188
87
  end
189
- ```
190
-
191
- **Callers**
192
-
193
- ```ruby
194
- # Useful for controllers or actions that depend on states.
195
- SearchMovies.perform('Toy Story') do |result, success, failure|
196
- success.call { redirect_to(movie_path, notice: "Movie can be found at: #{result}") }
197
- failure.call { redirect_to(root_path, notice: "Movie cannot be found at: #{result}") }
198
- end
199
- ```
200
-
201
- **Methods**
202
-
203
- ```ruby
204
- command = SearchMovies.call('Toy Story')
205
- command.errors #=> Lite::Errors::Messages object
206
-
207
- command.validate! #=> Raises Lite::Command::ValidationError if it has any errors
208
- command.valid? #=> Alias for validate!
209
-
210
- command.errored? #=> false
211
- command.success? #=> true
212
- command.failure? #=> Checks that it has been called and has errors
213
- command.status #=> :failure
214
-
215
- command.result! #=> Raises Lite::Command::ValidationError if it has any errors, if not it returns the result
216
-
217
- # Use the following to merge errors from other commands or models
218
- # with the default direction being `:from`
219
- command.merge_errors!(command_2)
220
- user_model.merge_errors!(command, direction: :to)
221
- ```
222
-
223
- ### Propagation (optional)
224
-
225
- Propagation methods help you perform an action on an object. If successful is
226
- returns the result else it adds the object errors to the form object. Available
227
- propagation methods are:
228
- - `assign_and_return!(object, params)`
229
- - `create_and_return!(klass, params)`
230
- - `update_and_return!(object, params)`
231
- - `destroy_and_return!(object)`
232
- - `archive_and_return!(object)` (if using Lite::Archive)
233
- - `save_and_return!(object)`
234
-
235
- ```ruby
236
- class SearchMovies < Lite::Command::Complex
237
- include Lite::Command::Extensions::Errors
238
- include Lite::Command::Extensions::Propagation
239
-
240
- # ... ommited ...
241
88
 
242
- def execute
243
- create_and_return!(User, name: 'John Doe')
244
- end
245
-
246
- end
89
+ command = CalculatePower.call(a: 2, b: 3)
90
+ command.context.result #=> 8
247
91
  ```
248
92
 
249
- ### Memoize (optional)
250
-
251
- Learn more about using [Lite::Memoize](https://github.com/drexed/lite-memoize)
252
-
253
- ```ruby
254
- class SearchMovies < Lite::Command::Complex
255
- include Lite::Command::Extensions::Memoize
256
-
257
- # ... ommited ...
258
-
259
- private
260
-
261
- # Sets the value in the cache
262
- # Subsequent method calls gets the cached value
263
- # This saves you the extra external HTTP.get call
264
- def movies_by_name
265
- cache.memoize { HTTP.get("http://movies.com?title=#{title}") }
266
- end
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
267
139
 
268
- # Gets the value in the cache
269
- def generate_fingerprint
270
- Digest::MD5.hexdigest(movies_by_name)
271
- end
140
+ `rails g command NAME` will generate the following file:
272
141
 
273
- end
142
+ ```erb
143
+ app/commands/[NAME]_command.rb
274
144
  ```
275
145
 
276
- **Methods**
277
-
278
- ```ruby
279
- command = SearchMovies.call('Toy Story')
280
- command.cache #=> Lite::Memoize::Instance object
281
- ```
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`.
282
149
 
283
150
  ## Development
284
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