service_base 1.0.2 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79ca15b6d1d305ea1dea4ab6633e63725de9f5d5f951c4221b9b59bef38b03ab
4
- data.tar.gz: 3bb44ec63bd53452ac09c3fc0f531bf25218dcbb1434b5e8bd0a1dc3f8f75be0
3
+ metadata.gz: 17af4f8d7a5f86ee24adbbbba2118e7b47134f2730e4e04fb0c0107c86cfa168
4
+ data.tar.gz: 1ad5a04d75c99bbc061d1ab53ce67d0d8864c132b8be85ebeb4da3b1f759d996
5
5
  SHA512:
6
- metadata.gz: 4a730fb152af189eb68c07bf5a55a31529605af58a4a8980afbf3369769ec0d3dcf81ab3e6d89ade5b9903dd547fa3f06b2db62edb9621d4ef8edfbab46c2d81
7
- data.tar.gz: 2f666d22c5368a817de07145a1e27eb32faf98b326128c9b6c8192fa48eecda9d3d0dd6d36b0eaf8848c7f9c5340b770e726777c4e7f78f2550e0393208ffe94
6
+ metadata.gz: 559196375ec8a48520b076feb989287c43e84cf7a759b91d0db753448f9199fe02f3febff0a06e9d797523f5774e58d26e4745f921820f94298519873d4a5732
7
+ data.tar.gz: ec37a4775094ebf67fa9e09b3db800206f31f8cfa5775866675f0213f9f188f5b228bc503e6e2ff7b3827bcfae039cc1dc0285796e96831ef990f182174a5709
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Service Base
2
2
 
3
- A base service class for Ruby applications that provides common functionality and argument type annotations.
3
+ A base service class for Ruby applications that provides common functionality and argument type annotations DSL.
4
+
5
+ ## Dependencies
6
+
7
+ Simply Ruby. This gem can be used in a standalone fashion and Rails is not a dependency. This gem does, however, work very nicely with Rails conventions and includes generators for getting set up quickly.
4
8
 
5
9
  ## Installation
6
10
 
@@ -11,18 +15,23 @@ gem "service_base"
11
15
  ```
12
16
 
13
17
  And then execute:
14
- ```bash
18
+ ```sh
15
19
  $ bundle install
16
20
  ```
17
21
 
18
22
  Or install it yourself as:
19
- ```bash
23
+ ```sh
20
24
  $ gem install service_base
21
25
  ```
22
26
 
23
- To follow convention in a Rails application, it's recommended that you create an `ApplicationService` subclass.
27
+ For Rails projects, then run:
28
+ ```sh
29
+ rails g service_base:install
30
+ ```
24
31
 
32
+ Installing the gem in a Rails project will create an `ApplicationService` subclass, following Rails conventions.
25
33
  ```rb
34
+ # app/services/application_service.rb
26
35
  class ApplicationService < ServiceBase::Service
27
36
  end
28
37
  ```
@@ -47,46 +56,37 @@ on Rails pattern: Service Objects](https://dev.to/joker666/ruby-on-rails-pattern
47
56
 
48
57
  ## Advantages
49
58
 
50
- - The action of a service should read as a list of steps which makes
51
- reading and maintaining the service easy.
59
+ - The action of a service should read as a list of steps which makes reading and maintaining the service easy.
52
60
  - Instantiation of a service object allows fine grained control over
53
61
  the arguments being passed in and reduces the need to pass arguments
54
62
  between methods in the same instance.
55
63
  - Encapsulation of logic in a service makes for reusable code, simpler
56
64
  testing, and extracts logic from other objects that should not be
57
65
  responsible for handling that logic.
66
+ - Removes the need for ActiveRecord callbacks and consolidates logic of related models into one place in the codebase.
58
67
  - Verb-naming makes the intention of the service explicit.
59
68
  - Single service actions reveal a single public interface.
60
69
 
61
70
  ## What defines a service?
62
71
 
63
- - The main difference between a model and a service is that a model
64
- “models” **what** something is while a service lists
65
- **how** an action is performed.
72
+ - The main difference between a model and a service is that a model “models” **what** something is while a service lists **how** an action is performed.
66
73
  - A service has a single public method, ie. `call`
67
- - A model is a noun, a service is a verb’ed noun that does the one
68
- thing the name implies
69
- - Ie. `User` (model) versus `User::CreatorService` (service)
70
- - Ie. `StripeResponse` (model) versus `PaymentHistoryFetcherService` (service)
74
+ - A model is a noun, a service is a verb or verb’ed noun that does the one thing the name implies
75
+ - Ie. `User` (model) versus `User::CreatorService` (service)
76
+ - Ie. `StripeResponse` (model) versus `PaymentHistoryFetcherService` (service)
71
77
 
72
78
  ## Naming
73
79
 
74
- One of the best ways to use the service pattern is for CRUD services - Ie. `ActiveRecordModel` +
75
- `::CreateService`, `::UpdateService`,
76
- `::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.
80
+ One of the best ways to use the service pattern is for CRUD services - Ie. `ActiveRecordModel` + `::CreateService`, `::UpdateService`, `::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 of execution.
77
81
 
78
82
  ## Returning a Result
79
83
 
80
- Each service inheriting from BaseService must define
81
- `#call` and return a `Success` or
82
- `Failure`. These types are `Result` Monads from
83
- the dry-monads gem. Both `Result` types may take any value as
84
- input, ie. `Success(user)`,
85
- `Failure(:not_found)`
84
+ Each service inheriting from BaseService must define `#call` and return a `Success` or `Failure`. These types are `Result` Monads from
85
+ the [dry-monads gem](https://dry-rb.org/gems/dry-monads/1.3/). Both `Result` types may take any value as input, ie. `Success(user)`, `Failure(:not_found)`
86
86
 
87
87
  `Failure` can return any value you’d like the caller to have in order to understand the failure.
88
88
 
89
- The caller of service can unwrap the Success, Failure or like so
89
+ The caller of service can unwrap the `Success` or `Failure`.
90
90
 
91
91
  ```ruby
92
92
  MyService.call(name: user.name) do |on|
@@ -95,8 +95,7 @@ MyService.call(name: user.name) do |on|
95
95
  end
96
96
  ```
97
97
 
98
- To match different expected values of success or failure, pass the
99
- value as an argument.
98
+ To match different expected values of success or failure, pass the value as an argument when unwrapping it.
100
99
 
101
100
  ```ruby
102
101
  MyService.call(name: user.name) do |on|
@@ -107,36 +106,23 @@ MyService.call(name: user.name) do |on|
107
106
  end
108
107
  ```
109
108
 
110
- Note that you must define both `on.success` and
111
- `on.failure` or else an error will be raised in the
112
- caller.
109
+ Note that you must define both `on.success` and `on.failure` or else an error will be raised in the caller.
113
110
 
114
- Note that `raise`ing an error requires an error class
115
- unless the error itself is an instance of an error class.
111
+ Note that `raise`ing an error requires an error class unless the error itself is an instance of an error class.
116
112
 
117
- Please see [result](https://dry-rb.org/gems/dry-monads/1.3/result/) for
118
- additional mechanisms used for chaining results and handling
119
- success/failure values.
120
-
121
- A recommended pattern within services is to return a
122
- `Success` and/or `Failure` from each method and
123
- yield the result in the caller. This forces you to consider how each
124
- method could fail and allows for automatic bubbling up of the
125
- `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)
113
+ Please see [result](https://dry-rb.org/gems/dry-monads/1.3/result/) for additional mechanisms used for chaining results and handling success/failure values.
126
114
 
127
115
  ## Failures vs Exceptions
128
116
 
129
- Failure = a known error case that may happen and should be gracefully
130
- handled
117
+ Failure = a known error case that may happen and should be gracefully handled
131
118
 
132
- Raising = an unexpected exception (exceptional circumstances)
119
+ Raising = an **unexpected** exception (exceptional circumstances)
133
120
 
134
121
  Any call that `raise`s is not rescued by default and will
135
- behave as a typical Ruby exception. This is a good thing. We will be
122
+ behave as a typical Ruby exception. This is a good thing. You will be
136
123
  alerted when exceptional circumstances arise.
137
124
 
138
- Return a `Failure` instead when you know of a potential
139
- failure case.
125
+ Return a `Failure` instead when you know of a potential failure case.
140
126
 
141
127
  Avoid rescuing major error/exception superclasses such as
142
128
  `StandardError`. Doing so will rescue all subclasses of that
@@ -161,35 +147,34 @@ end
161
147
  ## Arguments
162
148
 
163
149
  Arguments to a service are defined via the `argument` DSL.
164
- The positional name and type arguments are required, the other options
165
- are as follows.
166
- `argument(:name, String, optional: true, description: "The User's name")`
150
+ The positional name and type arguments are required, the other options are as follows.
151
+ `argument(:name, Type::String, optional: true, description: "The User's name")`
167
152
 
168
153
  If an argument is optional and has a default value, simply set `default: your_value` but do not also specify `optional: true`.
169
154
  Doing so will raise an `ArgumentError`.
155
+
170
156
  Additionally, be sure to `.freeze` any mutable default values, ie. `default: {}.freeze`.
171
157
  Failure to do so will raise an `ArgumentError`.
172
158
 
173
- Empty strings attempted to coerce into integers will throw an error.
174
- See [https://github.com/dry-rb/dry-types/issues/344#issuecomment-518743661](https://github.com/dry-rb/dry-types/issues/344#issuecomment-518743661)
175
- To instead accept `nil`, do the following:
176
- `argument(:some_integer, Params::Nil | Params::Integer)`
159
+ To allow multiple types as arguments, use `|`. For example,
177
160
 
178
- A service should also define a `description`. This is
179
- recommended for self-documentation, ie.
161
+ ```rb
162
+ argument(:value, Type::String | Type::Integer)
163
+ ```
164
+
165
+ A service should also define a `description`. This is recommended for self-documentation, ie.
180
166
 
181
167
  ```ruby
182
- class MyService < ServiceBase::Service
168
+ class MyService < ApplicationService
183
169
  description("Does a lot of cool things")
184
170
  end
185
171
  ```
186
172
 
187
173
  To get the full hash of `argument`'s keys and values passed into a service,
188
- use `arguments`. This is a very useful technique for
189
- services that update an object. For example
174
+ call `arguments`. This is a very useful technique for services that update an object. For example
190
175
 
191
176
  ```ruby
192
- class User::UpdateService < ServiceBase::Service
177
+ class User::UpdateService < ApplicationService
193
178
  argument(:name, String)
194
179
 
195
180
  def call
@@ -198,60 +183,68 @@ class User::UpdateService < ServiceBase::Service
198
183
  end
199
184
  ```
200
185
 
186
+ ### Nil
187
+
188
+ Empty strings attempted to coerce into integers will throw an error.
189
+ See [this GH issue for an explaination](https://github.com/dry-rb/dry-types/issues/344#issuecomment-518743661)
190
+ To instead accept `nil`, do the following:
191
+ `argument(:some_integer, Type::Params::Nil | Type::Params::Integer)`
192
+
193
+
201
194
  ## Types
202
195
 
203
196
  Argument types come from, [Dry.rb’s Types](https://dry-rb.org/gems/dry-types/1.2/built-in-types/), which can be extended.
204
197
  You may also add custom types as outlined in [Dry.rb Custom Types](https://dry-rb.org/gems/dry-types/1.2/custom-types/).
205
198
 
206
- It is recommended that you define your own `Type` module and include it in your `ServiceBase` subclass, as so.
199
+ The Rails generators will create a Type module, which includes `ServiceBase::Types`, which includes `Dry.Types`. Therefore, all types defined in Dry.rb's Types are available to you.
207
200
 
208
201
  ```rb
209
- # app/models/types.rb
210
- module Types
211
- ApplicationRecord = Types.Instance(ApplicationRecord)
212
- ControllerParams = ServiceBase::Types.Instance(ActionController::Parameters)
213
- end
214
-
215
- # app/services/application_service.rb
216
- class ApplicationService < ServiceBase::Service
217
- include Types
202
+ # app/models/type.rb
203
+ module Type
204
+ include ServiceBase::Types
205
+
206
+ # Any ApplicationRecord subclass
207
+ ApplicationRecord = Dry.Types.Instance(ApplicationRecord)
208
+ User = Dry.Types.Instance(User)
209
+ Project = Dry.Types.Instance(Project)
210
+
211
+ # Controller params are an ActionController::Parameters instance or a hash (easier for testing)
212
+ ControllerParams = Dry.Types.Instance(ActionController::Parameters) | Dry.Types.Instance(Hash)
213
+
214
+ # Customer param hashes
215
+ AddressParams = Dry::Types['hash'].schema(
216
+ address: Dry::Types['string'],
217
+ address2: Dry::Types['string'],
218
+ city: Dry::Types['string'],
219
+ state: Dry::Types['string'],
220
+ zip: Dry::Types['string']
221
+ )
218
222
  end
219
223
 
220
224
  # app/services/example_service.rb
221
225
  class ExampleService < ApplicationService
222
- argument(:user, ApplicationRecord, description: "The user to update")
223
- argument(:params, ControllerParams, description: "The attributes to update")
226
+ argument(:any_model, Type::ApplicationRecord, description: "The model to update")
227
+ argument(:params, Type::ControllerParams, description: "The attributes to update")
228
+ argument(:user, Type::User, description: "A cool user that relates to the model")
229
+ argument(:project, Type::Project, description: "A project that the user is working on")
230
+ argument(:address, Type::AddressParams, description: "The user's address")
224
231
  end
225
232
  ```
226
233
 
227
- You can also limit the type of `ApplicationRecord` record via
234
+ Dry.rb's `Coercible` and `Params` Types are very powerful and recommended for automatic parsing of inputs, ie. controller parameters.
228
235
 
229
- `argument(:user, Types.Instance(User))`
230
-
231
- Or defining `User = Types.Instance(User)`
232
-
233
- Note: In order to access constants outside of the dry.rb namespace,
234
- or to access a type that collides with one of our defined types, you
235
- must include `::` to allow a global constant search.
236
-
237
- Ie. `::ApplicationRecord...`
238
-
239
- `Coercible` and `Params` Types are very
240
- powerful and recommended for automatic parsing of inputs, ie. controller
241
- parameters.
242
-
243
- For example `argument(:number, Params::Integer)` will convert `"12"` ⇒ `12`.
236
+ For example `argument(:number, Type::Params::Integer)` will convert `"12"` ⇒ `12`.
244
237
 
245
238
  Entire hash structures may also be validated and automatically parsed, for example:
246
239
 
247
240
  ```ruby
248
241
  argument(
249
242
  :line_items,
250
- Array(
251
- Hash.schema(
252
- vintage_year: Params::Integer,
253
- number_of_credits: Params::Integer,
254
- price_dollars_usd: Params::Float,
243
+ Type::Array(
244
+ Type::Hash.schema(
245
+ vintage_year: Type::Params::Integer,
246
+ number_of_credits: Type::Params::Integer,
247
+ price_dollars_usd: Type::Params::Float,
255
248
  ),
256
249
  ),
257
250
  ```
@@ -282,10 +275,10 @@ roll the transaction back](https://www.loyalty.dev/posts/returning-from-transact
282
275
 
283
276
  ## Internal Method Result
284
277
 
285
- The Railway Pattern can be used internally within services via
286
- `yield` and `do` notation. This forces the
287
- programmer to think about the success and failure cases within each
288
- method. See [the dry-monads gem](https://dry-rb.org/gems/dry-monads/1.3/) for more details.
278
+ A recommended pattern within services is to return a `Success` and/or `Failure` from each method and
279
+ `yield` the result in the caller. This forces you to consider how each
280
+ method could fail and allows for automatic bubbling up of the
281
+ `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)
289
282
 
290
283
  If the internal methods of the service need to unwrap values, those specific methods need to be registered with the result matcher like so.
291
284
 
@@ -339,4 +332,4 @@ Bug reports and pull requests are welcome on GitHub.
339
332
 
340
333
  ## License
341
334
 
342
- The gem is available as open source under the terms of the MIT License.
335
+ The gem is available as open source under the terms of the MIT License.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationServiceGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ def create_application_service_file
7
+ service_path = 'app/services/application_service.rb'
8
+
9
+ if File.exist?(service_path)
10
+ # File exists, check if it needs to be updated
11
+ content = File.read(service_path)
12
+
13
+ unless content.include?('class ApplicationService < ServiceBase::Service')
14
+ # Update the class definition
15
+ new_content = content.sub(/class ApplicationService.*\n/, "class ApplicationService < ServiceBase::Service\n")
16
+ File.write(service_path, new_content)
17
+ end
18
+ else
19
+ # Create new file with template
20
+ create_file service_path, <<~RUBY
21
+ class ApplicationService < ServiceBase::Service
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceBase
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def run_generators
8
+ generate 'application_service'
9
+ generate 'types'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TypesGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ def create_types_file
7
+ types_path = 'app/models/type.rb'
8
+
9
+ if File.exist?(types_path)
10
+ # File exists, check if it needs the include statement
11
+ content = File.read(types_path)
12
+
13
+ unless content.include?('include ServiceBase::Types')
14
+ # Add include statement at the top of the module
15
+ new_content = content.sub(/module Type\s*\n/, "module Type\n include ServiceBase::Types\n")
16
+ File.write(types_path, new_content)
17
+ end
18
+ else
19
+ # Create new file with template
20
+ create_file types_path, <<~RUBY
21
+ module Type
22
+ include ServiceBase::Types
23
+ end
24
+ RUBY
25
+ end
26
+ end
27
+ end
@@ -3,7 +3,6 @@
3
3
  require 'dry/monads'
4
4
  require 'dry/monads/do'
5
5
  require 'dry/matcher/result_matcher'
6
- require 'dry-types'
7
6
  require 'dry-struct'
8
7
  require 'dry-matcher'
9
8
  require 'memery'
@@ -13,7 +12,6 @@ module ServiceBase
13
12
  extend Dry::Monads::Result::Mixin::Constructors
14
13
  include Dry::Monads::Do.for(:call)
15
14
  include Dry::Monads[:result, :do]
16
- include Dry.Types()
17
15
 
18
16
  extend ArgumentTypeAnnotations
19
17
  include Memery
@@ -0,0 +1,14 @@
1
+ require 'dry-types'
2
+
3
+ module ServiceBase
4
+ module Types
5
+ include Dry.Types()
6
+
7
+ def self.included(base)
8
+ constants.each { |constant| base.const_set(constant, const_get("#{self}::#{constant}")) }
9
+ end
10
+
11
+ # Alias Bool -> Boolean
12
+ Boolean = Dry::Types['bool']
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceBase
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end
data/lib/service_base.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'service_base/version'
4
4
  require_relative 'service_base/argument_type_annotations'
5
5
  require_relative 'service_base/service'
6
+ require_relative 'service_base/types'
6
7
 
7
8
  module ServiceBase
8
9
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: service_base
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Klein
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-02 00:00:00.000000000 Z
10
+ date: 2025-04-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-matcher
@@ -89,11 +89,15 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - LICENSE.txt
91
91
  - README.md
92
+ - lib/generators/application_service_generator.rb
93
+ - lib/generators/service_base/install_generator.rb
94
+ - lib/generators/types_generator.rb
92
95
  - lib/service_base.rb
93
96
  - lib/service_base/argument_type_annotations.rb
94
97
  - lib/service_base/rspec.rb
95
98
  - lib/service_base/rspec/service_support.rb
96
99
  - lib/service_base/service.rb
100
+ - lib/service_base/types.rb
97
101
  - lib/service_base/version.rb
98
102
  homepage: https://github.com/kleinjm/service_base
99
103
  licenses: