service_base 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d20e049647526253612ef6cf134c73ce8ab076c69e9a1f3a5524586f035f8ae3
4
+ data.tar.gz: 8fe635ae9d66b5358030440828f45e0706a2cad092e7eb45f9738af5c53c9896
5
+ SHA512:
6
+ metadata.gz: '0595ddabc674a039b1dfb3189f4f7a6d8aa2e3a6c694391b94c26235180f7b67475685b3c5e19283c07f8b8e141b6c99b109d35659af6ef7d4e436f74ab0a832'
7
+ data.tar.gz: a38e09b26353baa8a4b37edb97522d33a2bd95a9209f9b930c109f67dbd5d305623c57aa5508c82bdb574150536c37048a5399cb25eed4df5f5c22838456aac4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Your Name
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # Service Base
2
+
3
+ A base service class for Ruby applications that provides common functionality and argument type annotations.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "service_base"
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle install
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```bash
20
+ $ gem install service_base
21
+ ```
22
+
23
+ # Base Service Pattern
24
+
25
+ The general concept of a Service Pattern is useful when a you need to execute a set of
26
+ sequential steps. The service encapsulates those steps into a single class with a single action to trigger the steps.
27
+
28
+ The Base Service Pattern uses a modified [Railway
29
+ Pattern](https://fsharpforfunandprofit.com/posts/recipe-part2/) set up and enforced by the `Service` class,
30
+ which every service inherits from.
31
+
32
+ ## Recommended resources
33
+
34
+ - Highly recommended video inspiring this pattern: [Service Objects with
35
+ Dry.rb](https://www.youtube.com/watch?v=YXiqzHMmv_o)
36
+ - [Essential
37
+ RubyOnRails patterns — part 1: Service Objects](https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1)
38
+ - [Ruby
39
+ on Rails pattern: Service Objects](https://dev.to/joker666/ruby-on-rails-pattern-service-objects-b19)
40
+
41
+ ## Advantages
42
+
43
+ - The action of a service should read as a list of steps which makes
44
+ reading and maintaining the service easy.
45
+ - Instantiation of a service object allows fine grained control over
46
+ the arguments being passed in and reduces the need to pass arguments
47
+ between methods in the same instance.
48
+ - Encapsulation of logic in a service makes for reusable code, simpler
49
+ testing, and extracts logic from other objects that should not be
50
+ responsible for handling that logic.
51
+ - Verb-naming makes the intention of the service explicit.
52
+ - Single service actions reveal a single public interface.
53
+
54
+ ## What defines a service?
55
+
56
+ - The main difference between a model and a service is that a model
57
+ “models” **what** something is while a service lists
58
+ **how** an action is performed.
59
+ - A service has a single public method, ie. `call`
60
+ - A model is a noun, a service is a verb’ed noun that does the one
61
+ thing the name implies
62
+ - Ie. `User` (model) versus `User::CreatorService` (service)
63
+ - Ie. `StripeResponse` (model) versus `PaymentHistoryFetcherService` (service)
64
+
65
+ ## Naming
66
+
67
+ One of the best ways to use the service pattern is for CRUD services - Ie. `ActiveRecordModel` +
68
+ `::CreateService`, `::UpdateService`,
69
+ `::DeleteService`. This avoids the use of callbacks, mystery guests, and unexpected side effects because all the steps to do a CRUD action are in one place and in order.
70
+
71
+ ## Returning a Result
72
+
73
+ Each service inheriting from BaseService must define
74
+ `#call` and return a `Success` or
75
+ `Failure`. These types are `Result` Monads from
76
+ the dry-monads gem. Both `Result` types may take any value as
77
+ input, ie. `Success(user)`,
78
+ `Failure(:not_found)`
79
+
80
+ `Failure` can return any value you’d like the caller to have in order to understand the failure.
81
+
82
+ The caller of service can unwrap the Success, Failure or like so
83
+
84
+ ```ruby
85
+ MyService.call(name: user.name) do |on|
86
+ on.success { |value| some_method(value) }
87
+ on.failure { |error| log_error(error) }
88
+ end
89
+ ```
90
+
91
+ To match different expected values of success or failure, pass the
92
+ value as an argument.
93
+
94
+ ```ruby
95
+ MyService.call(name: user.name) do |on|
96
+ on.success(:created) { notify_created! }
97
+ on.failure(ActiveRecord::NotFound) { log_not_found }
98
+ on.failure(:invalid) { render(code: 422) }
99
+ on.failure { |error| raise(RuntimeError, error) }
100
+ end
101
+ ```
102
+
103
+ Note that you must define both `on.success` and
104
+ `on.failure` or else an error will be raised in the
105
+ caller.
106
+
107
+ Note that `raise`ing an error requires an error class
108
+ unless the error itself is an instance of an error class.
109
+
110
+ Please see [result](https://dry-rb.org/gems/dry-monads/1.3/result/) for
111
+ additional mechanisms used for chaining results and handling
112
+ success/failure values.
113
+
114
+ A recommended pattern within services is to return a
115
+ `Success` and/or `Failure` from each method and
116
+ yield the result in the caller. This forces you to consider how each
117
+ method could fail and allows for automatic bubbling up of the
118
+ `Failure` via railway-style programming. Examples at [https://dry-rb.org/gems/dry-monads/1.3/do-notation/#adding-batteries](https://dry-rb.org/gems/dry-monads/1.3/do-notation/#adding-batteries)
119
+
120
+ ## Failures vs Exceptions
121
+
122
+ Failure = a known error case that may happen and should be gracefully
123
+ handled
124
+
125
+ Raising = an unexpected exception (exceptional circumstances)
126
+
127
+ Any call that `raise`s is not rescued by default and will
128
+ behave as a typical Ruby exception. This is a good thing. We will be
129
+ alerted when exceptional circumstances arise.
130
+
131
+ Return a `Failure` instead when you know of a potential
132
+ failure case.
133
+
134
+ Avoid rescuing major error/exception superclasses such as
135
+ `StandardError`. Doing so will rescue all subclasses of that
136
+ error class. If you need to raise an error for control flow, favor a
137
+ specific error or custom error class.
138
+
139
+ ```ruby
140
+ # bad
141
+ rescue StandardError => e
142
+ Failure(e)
143
+ end
144
+
145
+ # good - known failure case
146
+ return Failure("Number #{num} must be positive") if arg.negative?
147
+
148
+ # good - exception required for control flow
149
+ rescue ActiveRecord::Rollback
150
+ Failure("Record invalid: #{record.inspect}")
151
+ end
152
+ ```
153
+
154
+ ## Arguments
155
+
156
+ Arguments to a service are defined via the `argument` DSL.
157
+ The positional name and type arguments are required, the other options
158
+ are as follows.
159
+ `argument(:name, String, optional: true, description: "The User's name")`
160
+
161
+ If an argument is optional and has a default value, simply set
162
+ `default: your_value` but do not also specify
163
+ `optional: true`. Doing so will raise an
164
+ `ArgumentError`. Additionally, be sure to
165
+ `.freeze` any mutable default values, ie.
166
+ `default: {}.freeze`. Failure to do so will raise an
167
+ `ArgumentError`.
168
+
169
+ Empty strings attempted to coerce into integers will throw an error.
170
+ See [https://github.com/dry-rb/dry-types/issues/344#issuecomment-518743661](https://github.com/dry-rb/dry-types/issues/344#issuecomment-518743661)
171
+ To instead accept `nil`, do the following:
172
+ `argument(:some_integer, Params::Nil | Params::Integer)`
173
+
174
+ A service should also define a `description`. This is
175
+ recommended for self-documentation, ie.
176
+
177
+ ```ruby
178
+ class MyService < Service
179
+ description("Does a lot of cool things")
180
+ end
181
+ ```
182
+
183
+ To get the full hash of `argument`s passed into a service,
184
+ use `attributes`. This is a very useful technique for
185
+ services that update an object. For example
186
+
187
+ ```ruby
188
+ class User::UpdateService < Service
189
+ def call
190
+ user.update(attributes)
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### ApplicationRecord Args
196
+
197
+ If you add `ApplicationRecord = Types.Instance(ApplicationRecord)` in a Rails project, you can accept any `ApplicationRecord` via
198
+
199
+ `argument(:my_record, ApplicationRecord)`
200
+
201
+ You can also limit the type of AR record via
202
+
203
+ `argument(:my_record, Types.Instance(MyRecord))`
204
+
205
+ ## Types
206
+
207
+ Argument types are defined in Types, which can be extended, ie. app/models/types.rb, and are an extension of [Dry.rb’s
208
+ Types](https://dry-rb.org/gems/dry-types/1.2/built-in-types/). In order to access constants outside of the dry.rb namespace,
209
+ or to access a type that collides with one of our defined types, you
210
+ must include `::` to allow a global constant search.
211
+
212
+ Ie. `::ApplicationRecord...`
213
+
214
+ `Coercible` and `Params` Types are very
215
+ powerful and recommended for automatic parsing of inputs, ie. controller
216
+ parameters.
217
+
218
+ For example `argument(:number, Params::Integer)` will
219
+ convert `"12"` ⇒ `12`
220
+
221
+ Entire hash structures may also be validated and automatically
222
+ parsed, for example:
223
+
224
+ ```ruby
225
+ argument(
226
+ :line_items,
227
+ Array(
228
+ Hash.schema(
229
+ vintage_year: Params::Integer,
230
+ number_of_credits: Params::Integer,
231
+ price_dollars_usd: Params::Float,
232
+ ),
233
+ ),
234
+ ```
235
+
236
+ ## Working with transactions
237
+
238
+ ⚠️  If your service makes more than one write call to the DB, you
239
+ should wrap all operations in a single transaction with
240
+ `::ApplicationRecord.transaction`.
241
+
242
+ According to the [Dry
243
+ RB docs](https://dry-rb.org/gems/dry-monads/1.3/do-notation/#transaction-safety):
244
+
245
+ > Under the hood, Do uses exceptions to halt unsuccessful
246
+ operations…Since yield internally uses exceptions to
247
+ control the flow, the exception will be detected by
248
+ the transaction call and the whole operation will be rolled
249
+ back.
250
+ >
251
+
252
+ Therefore, `yield`ing a `Failure` will roll
253
+ back the transaction without having to add any explicit exception
254
+ handling via `rescue`.
255
+
256
+ In Rails 7, using `return` inside a transaction [will
257
+ roll the transaction back](https://www.loyalty.dev/posts/returning-from-transactions-in-rails). Therefore,
258
+ `return Failure(...)` within a transaction will roll back, as well as `yield`ing a `Failure` within a transaction.
259
+
260
+ ## Internal Method Result
261
+
262
+ The Railway Pattern can be used internally within services via
263
+ `yield` and `do` notation. This forces the
264
+ programmer to think about the success and failure cases within each
265
+ method. See [the dry-monads gem](https://dry-rb.org/gems/dry-monads/1.3/) for more details.
266
+
267
+ If the internal methods of the service need to unwrap values, those specific methods need to be registered with the result matcher like so.
268
+
269
+ ```ruby
270
+ include Dry::Matcher.for(:method_name, with: Dry::Matcher::ResultMatcher)
271
+ ```
272
+
273
+ Within the service, the registered method can then be pattern matched and unwrapped.
274
+
275
+ ```ruby
276
+ method_name(order:) do |on|
277
+ on.success(:deleted) { true }
278
+ on.success(:cancelled) { destroy_order(order:) }
279
+ on.failure { |error| raise(RuntimeError, error) }
280
+ end
281
+ ```
282
+
283
+ ## Gotchas
284
+
285
+ - `yield`ing does not work inside `concerning`
286
+ blocks or other sub-modules. See [https://github.com/dry-rb/dry-monads/issues/68#issuecomment-1042372398](https://github.com/dry-rb/dry-monads/issues/68#issuecomment-1042372398)
287
+
288
+ ## Misc
289
+
290
+ - To get a pretty printed description of a service and it’s args, run `ServiceClass.pp`
291
+
292
+ ## Test Support
293
+
294
+ The following methods are made available by including the base service testing in your test suite.
295
+
296
+ ```ruby
297
+ require "service_base/rspec"
298
+ ```
299
+
300
+ ```ruby
301
+ stub_service_success(User::CreateService)
302
+ stub_service_success(User::CreateService, success: true)
303
+ stub_service_success(User::CreateService, success: create(:user))
304
+
305
+ stub_service_failure(User::CreateService, failure: "error")
306
+ stub_service_failure(User::CreateService failure: :invalid_email, matched: true)
307
+ ```
308
+
309
+ ## Development
310
+
311
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rspec` to run the tests.
312
+
313
+ ## Contributing
314
+
315
+ Bug reports and pull requests are welcome on GitHub.
316
+
317
+ ## License
318
+
319
+ The gem is available as open source under the terms of the MIT License.
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArgumentTypeAnnotations
4
+ class << self
5
+ def extended(klass)
6
+ if !klass.is_a?(Class) || !klass.ancestors.include?(Dry::Struct)
7
+ raise(TypeError, "#{name} should be extended on a Dry::Struct subclass")
8
+ end
9
+
10
+ # `Types` overrides default types to help shorthand Type::String.
11
+ # To access Ruby's native types within a service, use `::`, ie. `::String`
12
+ klass.include(Types)
13
+ end
14
+
15
+ def included(klass)
16
+ if !klass.singleton_class? || !klass.attached_object.ancestors.include?(Dry::Struct)
17
+ raise(TypeError, "#{name} should be included on the singleton class of a Dry::Struct subclass")
18
+ end
19
+
20
+ # `Types` overrides default types to help shorthand Type::String.
21
+ # To access Ruby's native types within a service, use `::`, ie. `::String`
22
+ klass.attached_object.include(Types)
23
+ end
24
+ end
25
+
26
+ # Defines an argument using the ServiceBase DSL.
27
+ # Under the hood, this uses dry-struct's attribute DSL.
28
+ def argument(name, type, configuration = {}, &)
29
+ description = configuration[:description] == "" ? nil : configuration[:description]
30
+ type = type.meta(description:)
31
+
32
+ default = configuration[:default]
33
+ validate_frozen_default!(name:, default:)
34
+
35
+ optional = configuration.fetch(:optional, false)
36
+ validate_optional_or_default!(optional:, default:, name:)
37
+
38
+ type = set_default(type:, default:)
39
+
40
+ if optional
41
+ # attribute? allows the key to be omitted.
42
+ # .optional allows the value to be nil.
43
+ # https://dry-rb.org/gems/dry-types/1.2/optional-values/
44
+ # https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L141-L169
45
+ attribute?(name, type.optional, &)
46
+ else
47
+ # https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L30-L104
48
+ attribute(name, type, &)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Raises a warning from dry-types to avoid memory sharing.
55
+ # https://github.com/dry-rb/dry-types/blob/master/lib/dry/types/builder.rb#L71-L81
56
+ def validate_frozen_default!(name:, default:)
57
+ return if default.frozen?
58
+
59
+ raise(
60
+ ArgumentError,
61
+ "#{default} provided as a default value for #{name} is mutable. " \
62
+ "Please `.freeze` your `default:` input.",
63
+ )
64
+ end
65
+
66
+ # Do not allow setting both a default value and optional: true. If both
67
+ # are specified, the default will not be used.
68
+ def validate_optional_or_default!(optional:, default:, name:)
69
+ return unless optional && !default.nil?
70
+
71
+ raise(
72
+ ArgumentError,
73
+ "#{name} cannot specify both a default value and optional: true. " \
74
+ "Only specify a default value if the value is optional.",
75
+ )
76
+ end
77
+
78
+ # Ensures that provided args are declared as `argument`s
79
+ def validate_args!(args:)
80
+ invalid_args = (args.keys - attribute_names)
81
+ return if invalid_args.empty?
82
+
83
+ raise(
84
+ ArgumentError,
85
+ "#{self} provided invalid arguments: #{invalid_args.join(', ')}",
86
+ )
87
+ end
88
+
89
+ # Sets the default value on the type.
90
+ # For primitive types, the default can be set after initialization.
91
+ # For enums, the default must be set during initialization. Therefore,
92
+ # we must check the type of the enum and then reconstruct the enum with
93
+ # the default value being set.
94
+ # See "Note" in https://dry-rb.org/gems/dry-types/1.2/enum/
95
+ def set_default(type:, default:)
96
+ return type if default.nil?
97
+
98
+ if type.is_a?(Dry::Types::Enum)
99
+ values = type.values
100
+ type_class = "Types::#{values.first.class}".constantize
101
+ type_class.default(default).enum(*values)
102
+ else
103
+ type.default(default)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceSupport
4
+ # Note that you must have at least one `on.success` and one `on.failure`
5
+ # matcher for each block-style service call
6
+ def stub_service_success(service_class, success: nil)
7
+ block = double(:on)
8
+ allow(block).to receive(:failure)
9
+ if success.present?
10
+ allow(block).to receive(:success).and_yield(success)
11
+ else
12
+ allow(block).to receive(:success)
13
+ end
14
+ allow(service_class).to receive(:call).and_yield(block)
15
+ end
16
+
17
+ # Note that you must have at least one `on.success` and one `on.failure`
18
+ # matcher for each block-style service call.
19
+ # Set `matched: true` for specific `on.failure(:error)` blocks.
20
+ # Set `matched: true` for catch-all `on.failure` blocks.
21
+ def stub_service_failure(service_class, failure:, matched: false)
22
+ block = double(:on)
23
+ allow(block).to receive(:success)
24
+ if matched # on.failure(:some_error)
25
+ allow(block).to receive(:failure) # ignore unmatched on.failure
26
+ allow(block).to receive(:failure).with(failure).and_yield(failure)
27
+ else # on.failure
28
+ # ignore matched on.failure(:some_error)
29
+ allow(block).to receive(:failure).with(anything)
30
+ allow(block).to receive(:failure).with(no_args).and_yield(failure)
31
+ end
32
+ allow(service_class).to receive(:call).and_yield(block)
33
+ end
34
+ end
35
+
36
+ RSpec.configure do |config|
37
+ config.include ServiceSupport
38
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # relative-require all rspec files
4
+ Dir[File.dirname(__FILE__) + '/rspec/*.rb'].each do |file|
5
+ require 'service_base/rspec/' + File.basename(file, File.extname(file))
6
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-matcher'
4
+ require 'dry-struct'
5
+ require 'dry/matcher/result_matcher'
6
+ require 'dry/monads'
7
+ require 'dry/monads/do'
8
+ require 'memery'
9
+
10
+ class Service < Dry::Struct
11
+ extend Dry::Monads::Result::Mixin::Constructors
12
+ include Dry::Monads::Do.for(:call)
13
+ include Dry::Monads[:result, :do]
14
+
15
+ extend ArgumentTypeAnnotations
16
+ include Memery
17
+
18
+ class ServiceNotSuccessful < StandardError
19
+ attr_reader(:failure)
20
+
21
+ def initialize(failure)
22
+ super('Failed to call service')
23
+ @failure = failure
24
+ end
25
+ end
26
+
27
+ class << self
28
+ # The public class call method.
29
+ #
30
+ # The default empty hash is important to prevent an argument error when
31
+ # passing no arguments to a service that defines defaults for every argument.
32
+ def call(args = {}, &block)
33
+ validate_args!(args: args)
34
+
35
+ result = new(args).call
36
+ match_result(result, &block)
37
+ end
38
+
39
+ def call!(*args)
40
+ result = call(*args)
41
+ raise(ServiceNotSuccessful, result.failure) if result.failure?
42
+
43
+ result
44
+ end
45
+
46
+ # Pretty prints (pp) the description of the service, ie. `MyService.pp`
47
+ def pp
48
+ logger = Logger.new($stdout)
49
+ logger.info("#{name}: #{service_description}")
50
+ logger.info('Arguments')
51
+
52
+ schema_definition.each do |arg|
53
+ logger.info(" #{arg[:name]} (#{arg[:type]}): #{arg[:description]}")
54
+ end
55
+ end
56
+
57
+ # @description getter
58
+ def service_description
59
+ @description || 'No description'
60
+ end
61
+
62
+ private
63
+
64
+ # Set the description on the service
65
+ def description(text)
66
+ @description = text
67
+ end
68
+
69
+ # Employs ResultMatcher to unwrap values using `on.success` & `on.failure`
70
+ # syntax. If not using block form to extract the result of a service,
71
+ # ie. `MyService.call.fmap { |result| result + 2 }`, ensure you explictly
72
+ # handle Failures. See https://dry-rb.org/gems/dry-monads/1.3/result/
73
+ def match_result(result, &block)
74
+ # https://medium.com/swlh/better-rails-service-objects-with-dry-rb-702687394e3d
75
+ if block
76
+ # raises Dry::Matcher::NonExhaustiveMatchError: cases +failure+ not handled
77
+ # if `on.failure` is not declared
78
+ Dry::Matcher::ResultMatcher.call(result, &block)
79
+ else
80
+ result
81
+ end
82
+ end
83
+
84
+ # Introspects the arguments DSL to extract information on each argument
85
+ def schema_definition
86
+ attribute_names.each_with_object([]) do |attribute_name, list|
87
+ dry_type = schema.key(attribute_name)
88
+ list << {
89
+ name: attribute_name,
90
+ description: dry_type.meta[:description],
91
+ type: dry_type.type.name
92
+ }
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # The call method that must be defined by every inheriting service class
100
+ def call
101
+ raise(NotImplementedError)
102
+ end
103
+
104
+ # A locale lookup helper that uses the name of the service
105
+ def locale(selector, args = {})
106
+ class_name = self.class.name.gsub('::', '.').underscore
107
+ I18n.t(".#{selector}", scope: "services.#{class_name}", **args)
108
+ end
109
+
110
+ # Structured Monad Result Failure type for returning a ResponseError
111
+ class ResponseFailure < Dry::Monads::Result::Failure
112
+ def initialize(message, code, trace = Dry::Monads::RightBiased::Left.trace_caller)
113
+ super(ResponseError.new(message: message, code: code), trace)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Defines Dry Types. These Types are included in the ServiceBase for type
4
+ # enforcement when defining `argument`s.
5
+ #
6
+ # For example, you may want to add `ApplicationRecord = Types.Instance(ApplicationRecord)`
7
+ #
8
+ # Add custom types as outlined in
9
+ # https://dry-rb.org/gems/dry-types/1.2/custom-types/
10
+
11
+ require 'dry-types'
12
+
13
+ module Types
14
+ include Dry.Types()
15
+
16
+ UpCasedString = Types::String.constructor(&:upcase)
17
+ Boolean = Bool # alias the built in type, Bool
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceBase
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'service_base/version'
4
+ require_relative 'service_base/types'
5
+ require_relative 'service_base/argument_type_annotations'
6
+ require_relative 'service_base/service'
7
+
8
+ module ServiceBase
9
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: service_base
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - James Klein
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-04-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-matcher
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.8.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.8.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: dry-monads
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-struct
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: dry-types
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.7'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.7'
68
+ - !ruby/object:Gem::Dependency
69
+ name: memery
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.7'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.7'
82
+ description: A base service class for Ruby applications with argument type annotations
83
+ and railway-oriented programming
84
+ email:
85
+ - kleinjm007@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE.txt
91
+ - README.md
92
+ - lib/service_base.rb
93
+ - lib/service_base/argument_type_annotations.rb
94
+ - lib/service_base/rspec.rb
95
+ - lib/service_base/rspec/service_support.rb
96
+ - lib/service_base/service.rb
97
+ - lib/service_base/types.rb
98
+ - lib/service_base/version.rb
99
+ homepage: https://github.com/kleinjm/service_base
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ homepage_uri: https://github.com/kleinjm/service_base
104
+ source_code_uri: https://github.com/kleinjm/service_base
105
+ changelog_uri: https://github.com/kleinjm/service_base/blob/main/README.md
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 2.6.0
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.6.2
121
+ specification_version: 4
122
+ summary: A base service class for Ruby applications
123
+ test_files: []