crossbeam 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f3e5a2f8df4b98114b1f734e78e22332078ea4f9f933fea9fe745f4113e1dab9
4
+ data.tar.gz: aae8768c3bd775c9e0939ae73b738b0ae2b00f50e2c1719116870f15e7fb3da4
5
+ SHA512:
6
+ metadata.gz: 0c9c6b21bcf8058137a42317c0e18b24ec6cb9aaeb06ad34dacd318c38931c29bed9d657e66390f41e2df3462dbb5fd955c5438d3c6f10603bb94429099b0888
7
+ data.tar.gz: 80d02b81f62a3d9670802576cb27794050925ef15335b9b2472816813d5ba83286147855596c5ad73a388c7a9c4193c7ba894d5e873353a5a03f6e2988e2f6c5
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2022-06-18
9
+
10
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Brandon Hicks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # Crossbeam
2
+
3
+ Crossbeam is a gem to making it easy to create and run ruby service objects. It allows you to use validations, errors, etc to take away the troubles associated with service classes.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```shell
10
+ bundle add crossbeam
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```shell
16
+ gem install crossbeam
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ In order to too use the Crossbeam service class you will need to add `include Crossbeam` to the class you wish to make a service object.
22
+
23
+ ### Initializers
24
+
25
+ You can call and initialize a Crossbeam service call like any other ruby object and
26
+
27
+ ```ruby
28
+ class ServiceClass
29
+ include Crossbeam
30
+
31
+ def initialize(name, age, other: nil)
32
+ @age = age
33
+ @name = name
34
+ @other = other
35
+ end
36
+
37
+ def call
38
+ do_something
39
+ end
40
+
41
+ private
42
+
43
+ def do_something
44
+ # .....
45
+ end
46
+ end
47
+
48
+ # Calling the service class
49
+ ServiceClass.call('James', 12)
50
+ ```
51
+
52
+ Crossbeam also includes [dry-initializer], which allows you to quickly initialize object parameters.
53
+ This allows you to bypass having to setup an initialize method in order to assign all attributes to instance variables.
54
+
55
+ ```ruby
56
+ class OtherService
57
+ include Crossbeam
58
+
59
+ param :name, proc(&:to_s)
60
+ param :age, proc(&:to_i)
61
+ option :other, default: proc { nil }
62
+
63
+ def call
64
+ do_something
65
+ end
66
+
67
+ private
68
+
69
+ def do_something
70
+ # .....
71
+ return "#{@name} is a minor" if @age < 18
72
+
73
+ "#{@name} is #{@age}"
74
+ end
75
+ end
76
+
77
+ # Calling the service class
78
+ OtherService.call('James', 12)
79
+ ```
80
+
81
+ ### Output
82
+
83
+ If you want skip assigning the last attribute returned from call to results you can specify a specific attribute to result reference after `#call` has been ran. This can be done by assigning an attribute to be assigned as the results with `output :attribute_name`.
84
+
85
+ ```ruby
86
+ class OutputSample
87
+ include Crossbeam
88
+
89
+ param :name, proc(&:to_s)
90
+ param :age, default: proc { 10 }
91
+
92
+ # Attribute/Instance Variable to return
93
+ output :age
94
+
95
+ def call
96
+ @age += 1
97
+ "Hello, #{name}! You are #{age} years old."
98
+ end
99
+ end
100
+
101
+ output = OutputSample.call('James', 12)
102
+ output.results
103
+ ```
104
+
105
+ ### Callbacks
106
+
107
+ Similar to Rails actions or models Crossbeam allows you to have before/after callbacks for before `call` is ran. They are completely optional and either one can be used without the other. They before/after references can either by a symbol or a block.
108
+
109
+ ```ruby
110
+ class SampleClass
111
+ include Crossbeam
112
+
113
+ # Callbacks that will be called before/after #call is referenced
114
+ before do
115
+ # setup for #call
116
+ end
117
+
118
+ after :cleanup_script
119
+
120
+ def call
121
+ # .....
122
+ end
123
+
124
+ private
125
+
126
+ def cleanup_script
127
+ # .....
128
+ end
129
+ end
130
+
131
+ SampleClass.call
132
+ ```
133
+
134
+ ### Errors and Validations
135
+
136
+ #### Errors
137
+
138
+ ```ruby
139
+ class ErrorClass
140
+ include Crossbeam
141
+
142
+ def initialize(name, age)
143
+ @name = name
144
+ @age = age
145
+ end
146
+
147
+ def call
148
+ errors.add(:age, "#{@name} is a minor") if @age < 18
149
+ errors.add(:base, 'something something something')
150
+ end
151
+ end
152
+
153
+ test = ErrorClass.call('James', 10)
154
+ test.errors
155
+ # => {:age=>["James is a minor"], :base=>["something something something"]}
156
+ test.errors.full_messages
157
+ # => ["Age James is a minor", "something something something"]
158
+ test.errors.to_s
159
+ # => Age James is a minor
160
+ # => something something something
161
+ ```
162
+
163
+ #### Validations
164
+
165
+ ```ruby
166
+ require_relative 'crossbeam'
167
+
168
+ class AgeCheck
169
+ include Crossbeam
170
+
171
+ option :age, default: proc { 0 }
172
+
173
+ validates :age, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 65 }
174
+
175
+ def call
176
+ return 'Minor' unless valid?
177
+
178
+ 'Adult'
179
+ end
180
+ end
181
+
182
+ puts AgeCheck.call(age: 15).errors.full_messages
183
+ # => ["Age must be greater than or equal to 18"]
184
+ puts AgeCheck.call(age: 20).results
185
+ # => Adult
186
+ ```
187
+
188
+ ```ruby
189
+ require_relative 'crossbeam'
190
+
191
+ class Bar
192
+ include Crossbeam
193
+
194
+ option :age, default: proc { 0 }
195
+ option :drink
196
+ option :description, default: proc { '' }
197
+
198
+ validates :age, numericality: { greater_than_or_equal_to: 21, less_than_or_equal_to: 65 }
199
+ validates :drink, inclusion: { in: %w(beer wine whiskey) }
200
+ validates :description, length: { minimum: 7, message: 'is required' }
201
+
202
+ def call
203
+ return 'Minor' unless valid?
204
+
205
+ 'Adult'
206
+ end
207
+ end
208
+
209
+ after_hours = Bar.call(age: 15, drink: 'tran')
210
+ puts after_hours.errors.full_messages if after_hours.errors?
211
+ # => Age must be greater than or equal to 21
212
+ # => Drink is not included in the list
213
+ # => Description is required
214
+ ```
215
+
216
+ ### Fail
217
+
218
+ If a particular condition is come across you may want to cause a service call to fail. This causes any further action within the service call to not be called and the classes result to be set as nil.
219
+
220
+ ```ruby
221
+ class Something
222
+ include Crossbeam
223
+
224
+ def call
225
+ fail!('1 is less than 2') unless 1 > 2
226
+
227
+ true
228
+ end
229
+ end
230
+
231
+ test = Something.call
232
+ test.failure? # => true
233
+ puts test.errors.full_messages # => ['1 is less than 2']
234
+ test.result # => nil
235
+ ```
236
+
237
+ When calling `fail!` you need to supply a message/context very similar to an exception description. And when the service call is forced to fail no results should be returned.
238
+
239
+ ### Generators (Rails)
240
+
241
+ The Crossbeam service class generator is only available when used with a rails application.
242
+
243
+ When running the generator you will specify the class name for the service object.
244
+
245
+ ```shell
246
+ rails g crossbeam AgeCheck
247
+ ```
248
+
249
+ Running this will generate a file `app/services/age_check.rb` with the following contents
250
+
251
+ ```ruby
252
+ # frozen_string_literal: true
253
+
254
+ class AgeCheck
255
+ include Crossbeam
256
+
257
+ def call
258
+ # ...
259
+ end
260
+ end
261
+ ```
262
+
263
+ You can also specify attributes that you want use with the class.
264
+
265
+ `rails g crossbeam IdentityCheck address age dob name`
266
+
267
+ ```ruby
268
+ class IdentityCheck
269
+ include Crossbeam
270
+
271
+ option :address
272
+ option :age
273
+ option :dob
274
+ option :name
275
+
276
+ def call
277
+ # ...
278
+ end
279
+ end
280
+ ```
281
+
282
+ ## Contributing
283
+
284
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tarellel/crossbeam.
285
+
286
+ ## License
287
+
288
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
289
+
290
+ This project is intended to be a safe, welcoming space for collaboration, and everyone interacting in the project’s codebase and issue tracker is expected to adhere to the [Contributor Covenant code of conduct](https://github.com/tarellel/crossbeam/main/CODE_OF_CONDUCT.md).
291
+
292
+ ## Inspired by
293
+
294
+ * [Actor](https://github.com/sunny/actor)
295
+ * [Callee](https://github.com/dreikanter/callee)
296
+ * [CivilService](https://github.com/actblue/civil_service)
297
+ * [SimpleCommand](https://github.com/nebulab/simple_command)
298
+ * [u-case](https://github.com/serradura/u-case)
299
+
300
+ [dry-initializer]: <https://github.com/dry-rb/dry-initializer>
data/defs.rbi ADDED
@@ -0,0 +1,294 @@
1
+ # typed: strong
2
+ # Crossbeam module to include with your service classes
3
+ module Crossbeam
4
+ VERSION = T.let('0.1.0', T.untyped)
5
+
6
+ # Used to include/extend modules into the current class
7
+ #
8
+ # _@param_ `base`
9
+ sig { params(base: Object).void }
10
+ def self.included(base); end
11
+
12
+ # Force the job to raise an exception and stop the rest of the service call
13
+ #
14
+ # _@param_ `error`
15
+ sig { params(error: String).void }
16
+ def fail!(error); end
17
+
18
+ # Used to return a list of errors for the current call
19
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
20
+ def errors; end
21
+
22
+ # Methods to load in the service object as class methods
23
+ module ClassMethods
24
+ # Used to initiate and service call
25
+ #
26
+ # _@param_ `params`
27
+ #
28
+ # _@param_ `options`
29
+ sig { params(params: T::Array[Object], options: T::Hash[Symbol, Object], block: T.untyped).returns(T.any(Struct, Object)) }
30
+ def call(*params, **options, &block); end
31
+
32
+ # Call the klass's before/after callbacks, process errors, and call @klass
33
+ sig { params(block: T.untyped).void }
34
+ def run_callbacks_and_klass(&block); end
35
+
36
+ # Process the error that was thrown by the object call
37
+ #
38
+ # _@param_ `error` — error generated by the object call
39
+ sig { params(error: Object).returns(T.any(Object, Crossbeam::Result)) }
40
+ def process_error(error); end
41
+ end
42
+
43
+ # Error message that is raised to flag a Crossbeam service error
44
+ # used to create error messages for Crossbeam
45
+ # @private
46
+ class Error < StandardError
47
+ # Used to initializee an error message
48
+ #
49
+ # _@param_ `context`
50
+ sig { params(context: T.nilable(T.any(String, T::Hash[T.untyped, T.untyped]))).void }
51
+ def initialize(context = nil); end
52
+
53
+ # _@return_ — Error message associated with StandardError
54
+ sig { returns(String) }
55
+ attr_reader :context
56
+ end
57
+
58
+ # Used to generate `ArguementError` exception
59
+ # @private
60
+ class ArguementError < Crossbeam::Error
61
+ end
62
+
63
+ # Used to generate `Failure` exception
64
+ # @private
65
+ class Failure < Crossbeam::Error
66
+ end
67
+
68
+ # Used to generate `NotImplementedError` exception
69
+ # @private
70
+ class NotImplementedError < Crossbeam::Error
71
+ end
72
+
73
+ # Used to generate `UndefinedMethod` exception
74
+ # @private
75
+ class UndefinedMethod < Crossbeam::Error
76
+ end
77
+
78
+ # Used to allow adding errors to the service call similar to ActiveRecord errors
79
+ class Errors < Hash
80
+ # Add an error to the list of errors
81
+ #
82
+ # _@param_ `key` — the key/attribute for the error. (Usually ends up being :base)
83
+ #
84
+ # _@param_ `value` — a description of the error
85
+ #
86
+ # _@param_ `_opts` — additional attributes that get ignored
87
+ sig { params(key: T.any(String, Symbol), value: T.any(String, Symbol), _opts: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
88
+ def add(key, value, _opts = {}); end
89
+
90
+ # Add multiple errors to the error hash
91
+ #
92
+ # _@param_ `errors`
93
+ #
94
+ # _@return_ — , Array]
95
+ sig { params(errors: T::Hash[String, Symbol]).returns(T::Hash[T.untyped, T.untyped]) }
96
+ def add_multiple_errors(errors); end
97
+
98
+ # Look through and return a list of all errorr messages
99
+ sig { returns(T.any(T::Hash[T.untyped, T.untyped], T::Array[T.untyped])) }
100
+ def each; end
101
+
102
+ # Return a full list of error messages
103
+ sig { returns(T::Array[String]) }
104
+ def full_messages; end
105
+
106
+ # Used to convert the list of errors to a string
107
+ sig { returns(String) }
108
+ def to_s; end
109
+
110
+ # Convert the message to a full error message
111
+ #
112
+ # _@param_ `attribute`
113
+ #
114
+ # _@param_ `message`
115
+ sig { params(attribute: T.any(String, Symbol), message: String).returns(String) }
116
+ def full_message(attribute, message); end
117
+ end
118
+
119
+ # For forcing specific output after `#call`
120
+ module Output
121
+ # Used to include/extend modules into the current class
122
+ #
123
+ # _@param_ `base`
124
+ sig { params(base: Object).void }
125
+ def self.included(base); end
126
+
127
+ # Methods to load in the service object as class methods
128
+ module ClassMethods
129
+ CB_ALLOWED_OUTPUTS = T.let([NilClass, String, Symbol].freeze, T.untyped)
130
+
131
+ # Used to specify an attribute/instance variable that should be used too return instead of @result
132
+ #
133
+ # _@param_ `param`
134
+ sig { params(param: T.any(String, Symbol)).returns(T.any(String, Symbol)) }
135
+ def output(param); end
136
+
137
+ # Determine if the output attribute type is allowed
138
+ #
139
+ # _@param_ `output_type`
140
+ sig { params(output_type: String).returns(T::Boolean) }
141
+ def allowed_output?(output_type); end
142
+
143
+ # Used to determine if a output parameter has been set or not
144
+ sig { returns(T::Boolean) }
145
+ def output?; end
146
+
147
+ # Determine the output to return if the instance_variable exists in the klass
148
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
149
+ def set_results_output; end
150
+
151
+ # Add errors to @result.errors
152
+ sig { void }
153
+ def build_error_list; end
154
+
155
+ # Reassign result, unless errors
156
+ sig { void }
157
+ def reassign_results; end
158
+
159
+ # Does the klass have an assigned output
160
+ sig { returns(T::Boolean) }
161
+ def specified_output?; end
162
+
163
+ # Used hold the parameter which can/will be used instead of @result
164
+ sig { returns(T.nilable(T.any(String, Symbol))) }
165
+ def output_param; end
166
+ end
167
+ end
168
+
169
+ # Used as a data container to hold a service calls results, errors, etc.
170
+ # class Result < Struct.new(:called, :errors, :failure, :results)
171
+ class Result < Struct
172
+ # Used to initialize a service calls results, errors, and stats
173
+ #
174
+ # _@param_ `called`
175
+ #
176
+ # _@param_ `errors`
177
+ #
178
+ # _@param_ `failure`
179
+ #
180
+ # _@param_ `results`
181
+ sig do
182
+ params(
183
+ called: T::Boolean,
184
+ errors: T.nilable(T::Hash[T.untyped, T.untyped]),
185
+ failure: T::Boolean,
186
+ results: T.nilable(Object)
187
+ ).void
188
+ end
189
+ def initialize(called: false, errors: nil, failure: false, results: nil); end
190
+
191
+ # The serivce can't officially be a failure/pass if it hasn't been "called"
192
+ sig { returns(T::Boolean) }
193
+ def called?; end
194
+
195
+ # Determine if the service call has any errors
196
+ sig { returns(T::Boolean) }
197
+ def errors?; end
198
+
199
+ # Determine if the service call has failed to uccessfully complete
200
+ sig { returns(T::Boolean) }
201
+ def failure?; end
202
+
203
+ # Determine if the service call has successfully ran
204
+ sig { returns(T::Boolean) }
205
+ def success?; end
206
+
207
+ # Return if the service has any issues (errors or failure)
208
+ sig { returns(T::Boolean) }
209
+ def issues?; end
210
+
211
+ # Returns the value of attribute called
212
+ sig { returns(Object) }
213
+ attr_accessor :called
214
+
215
+ # Returns the value of attribute errors
216
+ sig { returns(Object) }
217
+ attr_accessor :errors
218
+
219
+ # Returns the value of attribute failure
220
+ sig { returns(Object) }
221
+ attr_accessor :failure
222
+
223
+ # Returns the value of attribute results
224
+ sig { returns(Object) }
225
+ attr_accessor :results
226
+ end
227
+
228
+ # Callbacks before/after the services `call` is called
229
+ module Callbacks
230
+ # Used to include/extend modules into the current class
231
+ #
232
+ # _@param_ `base`
233
+ sig { params(base: Object).void }
234
+ def self.included(base); end
235
+
236
+ # Methods to load in the service object as class methods
237
+ module ClassMethods
238
+ # Add callback `before` method or block
239
+ #
240
+ # _@param_ `callbacks`
241
+ sig { params(callbacks: T::Hash[T.untyped, T.untyped], block: T.untyped).void }
242
+ def before(*callbacks, &block); end
243
+
244
+ # Add callback `after` method or block
245
+ #
246
+ # _@param_ `callbacks`
247
+ sig { params(callbacks: T::Hash[T.untyped, T.untyped], block: T.untyped).void }
248
+ def after(*callbacks, &block); end
249
+
250
+ # Call all callbacks before `#call` is referenced (methods, blocks, etc.)
251
+ sig { void }
252
+ def run_before_callbacks; end
253
+
254
+ # Call and run all callbacks after `#call` has ran
255
+ sig { void }
256
+ def run_after_callbacks; end
257
+
258
+ # Create a list of `after` callback methods and/or blocks
259
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
260
+ def after_callbacks; end
261
+
262
+ # Create a list of `before` callback methods and/or blocks
263
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
264
+ def before_callbacks; end
265
+
266
+ # Loopthrough and run all the classes listed callback methods
267
+ #
268
+ # _@param_ `callbacks` — a list of methods to be called
269
+ sig { params(callbacks: T::Array[T.any(String, Symbol)]).void }
270
+ def run_callbacks(callbacks); end
271
+
272
+ # Run callback m method or block
273
+ #
274
+ # _@param_ `callback`
275
+ #
276
+ # _@param_ `options`
277
+ sig { params(callback: Symbol, options: T::Hash[T.untyped, T.untyped]).void }
278
+ def run_callback(callback, *options); end
279
+ end
280
+ end
281
+ end
282
+
283
+ # Used to generate a Rails service object
284
+ class CrossbeamGenerator < Rails::Generators::Base
285
+ sig { void }
286
+ def generate_service; end
287
+
288
+ sig { void }
289
+ def generate_test; end
290
+
291
+ # Returns a string to use as the service classes file name
292
+ sig { returns(String) }
293
+ def filename; end
294
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './error'
4
+
5
+ module Crossbeam
6
+ # Callbacks before/after the services `call` is called
7
+ module Callbacks
8
+ # Used to include/extend modules into the current class
9
+ #
10
+ # @param base [Object]
11
+ # @return [void]
12
+ def self.included(base)
13
+ base.class_eval do
14
+ extend(ClassMethods)
15
+
16
+ attr_accessor :klass
17
+ end
18
+ end
19
+
20
+ # Methods to load in the service object as class methods
21
+ module ClassMethods
22
+ # Add callback `before` method or block
23
+ #
24
+ # @param callbacks [Hash]
25
+ # @return [void]
26
+ # @yield An optional block to instance_exec(&block) || instance_eval(&block)
27
+ def before(*callbacks, &block)
28
+ callbacks << block if block
29
+ callbacks.each { |callback| before_callbacks << callback }
30
+ end
31
+
32
+ # Add callback `after` method or block
33
+ #
34
+ # @param callbacks [Hash]
35
+ # @return [void]
36
+ # @yield An optional block to instance_exec(&block) || instance_eval(&block)
37
+ def after(*callbacks, &block)
38
+ callbacks << block if block
39
+ callbacks.each { |callback| after_callbacks << callback }
40
+ end
41
+
42
+ # Call all callbacks before `#call` is referenced (methods, blocks, etc.)
43
+ #
44
+ # @return [void]
45
+ def run_before_callbacks
46
+ # run_callbacks(self.class.before_callbacks)
47
+ run_callbacks(before_callbacks)
48
+ end
49
+
50
+ # Call and run all callbacks after `#call` has ran
51
+ #
52
+ # @return [void]
53
+ def run_after_callbacks
54
+ run_callbacks(after_callbacks)
55
+ end
56
+
57
+ # Create a list of `after` callback methods and/or blocks
58
+ #
59
+ # @return [Hash]
60
+ def after_callbacks
61
+ @after_callbacks ||= []
62
+ end
63
+
64
+ # Create a list of `before` callback methods and/or blocks
65
+ #
66
+ # @return [Hash]
67
+ def before_callbacks
68
+ @before_callbacks ||= []
69
+ end
70
+
71
+ # Loopthrough and run all the classes listed callback methods
72
+ #
73
+ # @param callbacks [Array<String, Symbol>] a list of methods to be called
74
+ # @return [void]
75
+ def run_callbacks(callbacks)
76
+ callbacks.each { |callback| run_callback(callback) }
77
+ end
78
+
79
+ private
80
+
81
+ # Run callback m method or block
82
+ #
83
+ # @param callback [Symbol]
84
+ # @param options [Hash]
85
+ # @return [void]
86
+ def run_callback(callback, *options)
87
+ # Ensure the initialize instance class has been called and passed
88
+ return unless @klass
89
+
90
+ # callback.is_a?(Symbol) ? send(callback, *options) : instance_exec(*options, &callback)
91
+ if [String, Symbol].include?(callback.class) && @klass.respond_to?(callback.to_sym)
92
+ @klass.send(callback, *options)
93
+ elsif callback.is_a?(Proc)
94
+ @klass.instance_exec(*options, &callback)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crossbeam
4
+ # Error message that is raised to flag a Crossbeam service error
5
+ # used to create error messages for Crossbeam
6
+ # @private
7
+ class Error < StandardError
8
+ # @return [String] Error message associated with StandardError
9
+ attr_reader :context
10
+
11
+ # Used to initializee an error message
12
+ #
13
+ # @param context [String, Hash]
14
+ # @return [Object]
15
+ def initialize(context = nil)
16
+ @context = context
17
+ super
18
+ end
19
+ end
20
+
21
+ # Used to generate `ArguementError` exception
22
+ # @private
23
+ class ArguementError < Error; end
24
+ # Used to generate `Failure` exception
25
+ # @private
26
+ class Failure < Error; end
27
+ # Used to generate `NotImplementedError` exception
28
+ # @private
29
+ class NotImplementedError < Error; end
30
+ # Used to generate `UndefinedMethod` exception
31
+ # @private
32
+ class UndefinedMethod < Error; end
33
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './error'
4
+
5
+ module Crossbeam
6
+ # Very similar to ActiveModel errors
7
+ # https://github.com/rails/rails/blob/main/activemodel/lib/active_model/validations.rb
8
+
9
+ # Used to allow adding errors to the service call similar to ActiveRecord errors
10
+ class Errors < Hash
11
+ # Add an error to the list of errors
12
+ #
13
+ # @param key [String, Symbol] the key/attribute for the error. (Usually ends up being :base)
14
+ # @param value [String, Symbol] a description of the error
15
+ # @param _opts [Hash] additional attributes that get ignored
16
+ # @return [Hash]
17
+ def add(key, value, _opts = {})
18
+ self[key] ||= []
19
+ self[key] << value
20
+ self[key].uniq!
21
+ end
22
+
23
+ # Add multiple errors to the error hash
24
+ #
25
+ # @param errors [Hash<String, Symbol>]
26
+ # @return [Hash], Array]
27
+ def add_multiple_errors(errors)
28
+ errors.each do |key, values|
29
+ if values.is_a?(String)
30
+ add(key, values)
31
+ elsif [Array, Hash].include?(values.class)
32
+ values.each { |value| add(key, value) }
33
+ end
34
+ end
35
+ end
36
+
37
+ # Look through and return a list of all errorr messages
38
+ #
39
+ # @return [Hash, Array]
40
+ def each
41
+ each_key do |field|
42
+ self[field].each { |message| yield field, message }
43
+ end
44
+ end
45
+
46
+ # Return a full list of error messages
47
+ #
48
+ # @return [Array<String>]
49
+ def full_messages
50
+ map { |attribute, message| full_message(attribute, message) }.freeze
51
+ end
52
+
53
+ # Used to convert the list of errors to a string
54
+ #
55
+ # @return [String]
56
+ def to_s
57
+ return '' unless self&.any?
58
+
59
+ full_messages.join("\n")
60
+ end
61
+
62
+ private
63
+
64
+ # Convert the message to a full error message
65
+ #
66
+ # @param attribute [String, Symbol]
67
+ # @param message [String]
68
+ # @return [String]
69
+ def full_message(attribute, message)
70
+ return message if attribute == :base
71
+
72
+ attr_name = attribute.to_s.tr('.', '_').capitalize
73
+ format('%<attr>s %<msg>s', attr: attr_name, msg: message)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './error'
4
+
5
+ module Crossbeam
6
+ # For forcing specific output after `#call`
7
+ module Output
8
+ # Used to include/extend modules into the current class
9
+ #
10
+ # @param base [Object]
11
+ # @return [void]
12
+ def self.included(base)
13
+ base.class_eval do
14
+ extend(ClassMethods)
15
+
16
+ attr_accessor :output_param
17
+ end
18
+ end
19
+
20
+ # Methods to load in the service object as class methods
21
+ module ClassMethods
22
+ # @return [Hash]
23
+ CB_ALLOWED_OUTPUTS = [NilClass, String, Symbol].freeze
24
+ # Used to specify an attribute/instance variable that should be used too return instead of @result
25
+ #
26
+ # @param param [String, Symbol]
27
+ # @return [String, Symbol]
28
+ def output(param)
29
+ raise(ArguementError, 'A string or symbol is require for output') unless allowed_output?(param)
30
+
31
+ @output_param = param
32
+ end
33
+
34
+ # Determine if the output attribute type is allowed
35
+ #
36
+ # @param output_type [String]
37
+ # @return [Boolean]
38
+ def allowed_output?(output_type)
39
+ CB_ALLOWED_OUTPUTS.include?(output_type.class)
40
+ end
41
+
42
+ # Used to determine if a output parameter has been set or not
43
+ #
44
+ # @return [Boolean]
45
+ def output?
46
+ !output_param.nil?
47
+ end
48
+
49
+ # Determine the output to return if the instance_variable exists in the klass
50
+ #
51
+ # @return [Hash]
52
+ def set_results_output
53
+ return unless @klass
54
+ return unless output? && @klass.instance_variable_defined?("@#{output_param}")
55
+
56
+ @result.results = @klass.instance_variable_get("@#{output_param}")
57
+ end
58
+
59
+ # Add errors to @result.errors
60
+ #
61
+ # @return [Void]
62
+ def build_error_list
63
+ return unless @klass && @klass&.errors&.any?
64
+
65
+ @klass.errors.each do |error|
66
+ # options is usually passed with an ActiveRecord validation error
67
+ # @example:
68
+ # <attribute=age, type=greater_than_or_equal_to, options={:value=>15, :count=>18}>
69
+ @result.errors.add(error.attribute, error.message)
70
+ end
71
+ @klass.errors.clear
72
+ reassign_results
73
+ end
74
+
75
+ # Reassign result, unless errors
76
+ #
77
+ # @return [Void]
78
+ def reassign_results
79
+ @result.results = nil
80
+ @result.results = @klass.instance_variable_get("@#{output_param}") if specified_output?
81
+ end
82
+
83
+ # Does the klass have an assigned output
84
+ #
85
+ # @return [Boolean]
86
+ def specified_output?
87
+ output? && @klass.instance_variable_defined?("@#{output_param}")
88
+ end
89
+
90
+ # Used hold the parameter which can/will be used instead of @result
91
+ #
92
+ # @return [String, Symbol, nil]
93
+ def output_param
94
+ @output_param ||= nil
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ # Crossbeam files
5
+ require_relative './error'
6
+ require_relative './errors'
7
+
8
+ module Crossbeam
9
+ # Used as a data container to hold a service calls results, errors, etc.
10
+ # class Result < Struct.new(:called, :errors, :failure, :results)
11
+ Result = Struct.new(:called, :errors, :failure, :results) do
12
+ # extend ActiveModel::Naming
13
+ # include ActiveModel::Conversion
14
+ # attr_accessor :called, :errors, :failure, :results
15
+
16
+ # Used to initialize a service calls results, errors, and stats
17
+ #
18
+ # @param called [Boolean]
19
+ # @param errors [Hash]
20
+ # @param failure [Boolean]
21
+ # @param results [Object]
22
+ def initialize(called: false, errors: nil, failure: false, results: nil)
23
+ super(called, errors, failure, results)
24
+
25
+ self.called = called
26
+ self.errors = Crossbeam::Errors.new
27
+ self.failure = failure
28
+ self.results = nil
29
+ end
30
+
31
+ # The serivce can't officially be a failure/pass if it hasn't been "called"
32
+ #
33
+ # @return [Boolean]
34
+ def called?
35
+ called || false
36
+ end
37
+
38
+ # Determine if the service call has any errors
39
+ #
40
+ # @return [Boolean]
41
+ def errors?
42
+ (errors.is_a?(Hash) && errors.any?) || false
43
+ end
44
+
45
+ # Determine if the service call has failed to uccessfully complete
46
+ #
47
+ # @return [Boolean]
48
+ def failure?
49
+ called? && issues?
50
+ end
51
+
52
+ # Determine if the service call has successfully ran
53
+ #
54
+ # @return [Boolean]
55
+ def success?
56
+ called? && !issues?
57
+ end
58
+
59
+ private
60
+
61
+ # Return if the service has any issues (errors or failure)
62
+ #
63
+ # @return [Boolean]
64
+ def issues?
65
+ errors? || failure
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crossbeam
4
+ # @return [String]
5
+ VERSION = '0.1.0'
6
+ end
data/lib/crossbeam.rb ADDED
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'dry-initializer'
5
+
6
+ # Crossbeam required modules
7
+ require_relative 'crossbeam/callbacks'
8
+ require_relative 'crossbeam/error' # exceptions
9
+ require_relative 'crossbeam/errors' # errors
10
+ # For forcing specific output after `#call`
11
+ require_relative 'crossbeam/output'
12
+ require_relative 'crossbeam/result'
13
+ require_relative 'crossbeam/version'
14
+
15
+ require 'debug'
16
+
17
+ # Crossbeam module to include with your service classes
18
+ module Crossbeam
19
+ # Used to include/extend modules into the current class
20
+ #
21
+ # @param base [Object]
22
+ # @return [void]
23
+ def self.included(base)
24
+ base.class_eval do
25
+ include(ActiveModel::Validations) # Used to add validation errors to a class
26
+ extend(Dry::Initializer) # Used to allow for easy attribute initializations
27
+ extend(ClassMethods) # Class methods used to initialize the service call
28
+ include(Output)
29
+ include(Callbacks) # Callbacks that get called before/after `.call` is referenced
30
+
31
+ # Holds the service call results and current class call for Crossbeam Instance
32
+ attr_reader :result, :klass
33
+ end
34
+ end
35
+
36
+ # Methods to load in the service object as class methods
37
+ module ClassMethods
38
+ # Used to initiate and service call
39
+ #
40
+ # @param params [Array<Object>]
41
+ # @param options [Hash<Symbol, Object>]
42
+ # @return [Struct, Object]
43
+ def call(*params, **options, &block)
44
+ @result = Crossbeam::Result.new(called: true)
45
+ @klass = new(*params, **options)
46
+ run_callbacks_and_klass(&block)
47
+ @result
48
+ rescue Crossbeam::Failure => e
49
+ process_error(e)
50
+ end
51
+
52
+ # Call the klass's before/after callbacks, process errors, and call @klass
53
+ #
54
+ # @return [Void]
55
+ def run_callbacks_and_klass(&block)
56
+ # Call all the classes `before`` callbacks
57
+ run_before_callbacks
58
+ # Initialize and run the classes call method
59
+ @result.results = @klass.call(&block)
60
+ # build @result.errors if errors were added
61
+ build_error_list if @klass.errors.any?
62
+ # re-assign @result if output was set
63
+ set_results_output
64
+ # Call all the classes `after` callbacks
65
+ run_after_callbacks
66
+ end
67
+
68
+ # Process the error that was thrown by the object call
69
+ #
70
+ # @param error [Object] error generated by the object call
71
+ # @return [Object, Crossbeam::Result]
72
+ def process_error(error)
73
+ @result.failure = true
74
+ @result.errors.add(:failure, error.to_s)
75
+ @result.results = nil
76
+ @result
77
+ end
78
+ end
79
+
80
+ # Force the job to raise an exception and stop the rest of the service call
81
+ #
82
+ # @param error [String]
83
+ # @return [void]
84
+ def fail!(error)
85
+ raise(Crossbeam::Failure, error)
86
+ end
87
+
88
+ %w[called failure errors issues success].each do |attr|
89
+ # Used to determine the current state of the service call
90
+ #
91
+ # @return [Boolean]
92
+ define_method("#{attr}?") do
93
+ return false unless @result
94
+
95
+ attr = attr.to_s
96
+ @result.send("#{attr}?".to_sym) || false
97
+ end
98
+ end
99
+
100
+ # Used to return a list of errors for the current call
101
+ #
102
+ # @return [Hash]
103
+ def errors
104
+ return {} unless @result
105
+
106
+ @result.errors
107
+ end
108
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Only define the generators if part of rails app to prevent causing an excceeption
4
+ if defined?(Rails)
5
+ require 'rails/generators'
6
+
7
+ # Used to generate a Rails service object
8
+ class CrossbeamGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path(File.join('.', 'templates'), File.dirname(__FILE__))
10
+
11
+ argument :class_name, type: :string
12
+ argument :attributes, type: :array, default: [], banner: 'field[:type]'
13
+ desc 'Generate a Crossbeam service class'
14
+
15
+ # @return [void]
16
+ def generate_service
17
+ template 'service_class.rb.tt', "app/services/#{filename}.rb", force: true
18
+ end
19
+
20
+ # @return [void]
21
+ def generate_test
22
+ return unless Rails&.application&.config&.generators&.test_framework == :rspec
23
+
24
+ template 'service_spec.rb.tt', "spec/services/#{filename}_spec.rb", force: true
25
+ end
26
+
27
+ private
28
+
29
+ # Returns a string to use as the service classes file name
30
+ #
31
+ # @return [String]
32
+ def filename
33
+ class_name.underscore
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name.classify %>
4
+ include Crossbeam
5
+ <%- attributes.each do |attribute| -%>
6
+ option :<%= attribute.parameterize(separator: '_') %>
7
+ <%- end -%>
8
+
9
+ def call
10
+ # ...
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= class_name.classify %>, type: :helper do
6
+ subject { described_class.call }
7
+
8
+ describe 'an example' do
9
+ it 'is a pending example'
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crossbeam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brandon Hicks
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-initializer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-performance
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: An easy way to create and run service objects with callbacks, validations,
98
+ errors, and responses
99
+ email:
100
+ - tarellel@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files:
104
+ - CHANGELOG.md
105
+ - README.md
106
+ - LICENSE.txt
107
+ files:
108
+ - CHANGELOG.md
109
+ - LICENSE.txt
110
+ - README.md
111
+ - defs.rbi
112
+ - lib/crossbeam.rb
113
+ - lib/crossbeam/callbacks.rb
114
+ - lib/crossbeam/error.rb
115
+ - lib/crossbeam/errors.rb
116
+ - lib/crossbeam/output.rb
117
+ - lib/crossbeam/result.rb
118
+ - lib/crossbeam/version.rb
119
+ - lib/generators/crossbeam_generator.rb
120
+ - lib/generators/templates/service_class.rb.tt
121
+ - lib/generators/templates/service_spec.rb.tt
122
+ homepage: https://github.com/tarellel/crossbeam
123
+ licenses:
124
+ - MIT
125
+ metadata:
126
+ bug_tracker_uri: https://github.com/tarellel/crossbeam/issue
127
+ changelog_uri: https://github.com/tarellel/crossbeam/blob/master/CHANGELOG.md
128
+ homepage_uri: https://github.com/tarellel/crossbeam
129
+ source_code_uri: https://github.com/tarellel/crossbeam
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '2.7'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.3.15
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: An easy way to create and run service objects with callbacks, validations,
149
+ errors, and responses
150
+ test_files: []