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 +4 -4
- data/README.md +89 -96
- data/lib/generators/application_service_generator.rb +26 -0
- data/lib/generators/service_base/install_generator.rb +12 -0
- data/lib/generators/types_generator.rb +27 -0
- data/lib/service_base/service.rb +0 -2
- data/lib/service_base/types.rb +14 -0
- data/lib/service_base/version.rb +1 -1
- data/lib/service_base.rb +1 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17af4f8d7a5f86ee24adbbbba2118e7b47134f2730e4e04fb0c0107c86cfa168
|
4
|
+
data.tar.gz: 1ad5a04d75c99bbc061d1ab53ce67d0d8864c132b8be85ebeb4da3b1f759d996
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
```
|
18
|
+
```sh
|
15
19
|
$ bundle install
|
16
20
|
```
|
17
21
|
|
18
22
|
Or install it yourself as:
|
19
|
-
```
|
23
|
+
```sh
|
20
24
|
$ gem install service_base
|
21
25
|
```
|
22
26
|
|
23
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
179
|
-
|
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 <
|
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
|
-
|
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 <
|
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
|
-
|
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/
|
210
|
-
module
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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(:
|
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
|
-
|
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(:
|
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
|
-
|
286
|
-
`yield`
|
287
|
-
|
288
|
-
|
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,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
|
data/lib/service_base/service.rb
CHANGED
@@ -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
|
data/lib/service_base/version.rb
CHANGED
data/lib/service_base.rb
CHANGED
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.
|
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-
|
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:
|