crossbeam 0.1.0

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