service_base 1.0.0 → 1.0.2
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 +47 -24
- data/lib/service_base/argument_type_annotations.rb +84 -86
- data/lib/service_base/service.rb +81 -85
- data/lib/service_base/version.rb +1 -1
- data/lib/service_base.rb +0 -1
- metadata +2 -3
- data/lib/service_base/types.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79ca15b6d1d305ea1dea4ab6633e63725de9f5d5f951c4221b9b59bef38b03ab
|
4
|
+
data.tar.gz: 3bb44ec63bd53452ac09c3fc0f531bf25218dcbb1434b5e8bd0a1dc3f8f75be0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a730fb152af189eb68c07bf5a55a31529605af58a4a8980afbf3369769ec0d3dcf81ab3e6d89ade5b9903dd547fa3f06b2db62edb9621d4ef8edfbab46c2d81
|
7
|
+
data.tar.gz: 2f666d22c5368a817de07145a1e27eb32faf98b326128c9b6c8192fa48eecda9d3d0dd6d36b0eaf8848c7f9c5340b770e726777c4e7f78f2550e0393208ffe94
|
data/README.md
CHANGED
@@ -20,6 +20,13 @@ Or install it yourself as:
|
|
20
20
|
$ gem install service_base
|
21
21
|
```
|
22
22
|
|
23
|
+
To follow convention in a Rails application, it's recommended that you create an `ApplicationService` subclass.
|
24
|
+
|
25
|
+
```rb
|
26
|
+
class ApplicationService < ServiceBase::Service
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
23
30
|
# Base Service Pattern
|
24
31
|
|
25
32
|
The general concept of a Service Pattern is useful when a you need to execute a set of
|
@@ -158,13 +165,10 @@ The positional name and type arguments are required, the other options
|
|
158
165
|
are as follows.
|
159
166
|
`argument(:name, String, optional: true, description: "The User's name")`
|
160
167
|
|
161
|
-
If an argument is optional and has a default value, simply set
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
`.freeze` any mutable default values, ie.
|
166
|
-
`default: {}.freeze`. Failure to do so will raise an
|
167
|
-
`ArgumentError`.
|
168
|
+
If an argument is optional and has a default value, simply set `default: your_value` but do not also specify `optional: true`.
|
169
|
+
Doing so will raise an `ArgumentError`.
|
170
|
+
Additionally, be sure to `.freeze` any mutable default values, ie. `default: {}.freeze`.
|
171
|
+
Failure to do so will raise an `ArgumentError`.
|
168
172
|
|
169
173
|
Empty strings attempted to coerce into integers will throw an error.
|
170
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,37 +179,58 @@ A service should also define a `description`. This is
|
|
175
179
|
recommended for self-documentation, ie.
|
176
180
|
|
177
181
|
```ruby
|
178
|
-
class MyService < Service
|
182
|
+
class MyService < ServiceBase::Service
|
179
183
|
description("Does a lot of cool things")
|
180
184
|
end
|
181
185
|
```
|
182
186
|
|
183
|
-
To get the full hash of `argument`s passed into a service,
|
184
|
-
use `
|
187
|
+
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
|
185
189
|
services that update an object. For example
|
186
190
|
|
187
191
|
```ruby
|
188
|
-
class User::UpdateService < Service
|
192
|
+
class User::UpdateService < ServiceBase::Service
|
193
|
+
argument(:name, String)
|
194
|
+
|
189
195
|
def call
|
190
|
-
user.update(
|
196
|
+
user.update(arguments)
|
191
197
|
end
|
192
198
|
end
|
193
199
|
```
|
194
200
|
|
195
|
-
|
201
|
+
## Types
|
202
|
+
|
203
|
+
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
|
+
You may also add custom types as outlined in [Dry.rb Custom Types](https://dry-rb.org/gems/dry-types/1.2/custom-types/).
|
196
205
|
|
197
|
-
|
206
|
+
It is recommended that you define your own `Type` module and include it in your `ServiceBase` subclass, as so.
|
198
207
|
|
199
|
-
|
208
|
+
```rb
|
209
|
+
# app/models/types.rb
|
210
|
+
module Types
|
211
|
+
ApplicationRecord = Types.Instance(ApplicationRecord)
|
212
|
+
ControllerParams = ServiceBase::Types.Instance(ActionController::Parameters)
|
213
|
+
end
|
200
214
|
|
201
|
-
|
215
|
+
# app/services/application_service.rb
|
216
|
+
class ApplicationService < ServiceBase::Service
|
217
|
+
include Types
|
218
|
+
end
|
202
219
|
|
203
|
-
|
220
|
+
# app/services/example_service.rb
|
221
|
+
class ExampleService < ApplicationService
|
222
|
+
argument(:user, ApplicationRecord, description: "The user to update")
|
223
|
+
argument(:params, ControllerParams, description: "The attributes to update")
|
224
|
+
end
|
225
|
+
```
|
204
226
|
|
205
|
-
|
227
|
+
You can also limit the type of `ApplicationRecord` record via
|
228
|
+
|
229
|
+
`argument(:user, Types.Instance(User))`
|
230
|
+
|
231
|
+
Or defining `User = Types.Instance(User)`
|
206
232
|
|
207
|
-
|
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,
|
233
|
+
Note: In order to access constants outside of the dry.rb namespace,
|
209
234
|
or to access a type that collides with one of our defined types, you
|
210
235
|
must include `::` to allow a global constant search.
|
211
236
|
|
@@ -215,11 +240,9 @@ Ie. `::ApplicationRecord...`
|
|
215
240
|
powerful and recommended for automatic parsing of inputs, ie. controller
|
216
241
|
parameters.
|
217
242
|
|
218
|
-
For example `argument(:number, Params::Integer)` will
|
219
|
-
convert `"12"` ⇒ `12`
|
243
|
+
For example `argument(:number, Params::Integer)` will convert `"12"` ⇒ `12`.
|
220
244
|
|
221
|
-
Entire hash structures may also be validated and automatically
|
222
|
-
parsed, for example:
|
245
|
+
Entire hash structures may also be validated and automatically parsed, for example:
|
223
246
|
|
224
247
|
```ruby
|
225
248
|
argument(
|
@@ -1,106 +1,104 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module ServiceBase
|
4
|
+
module ArgumentTypeAnnotations
|
5
|
+
class << self
|
6
|
+
def extended(klass)
|
7
|
+
if !klass.is_a?(Class) || !klass.ancestors.include?(Dry::Struct)
|
8
|
+
raise(TypeError, "#{name} should be extended on a Dry::Struct subclass")
|
9
|
+
end
|
8
10
|
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def included(klass)
|
13
|
+
if !klass.singleton_class? || !klass.attached_object.ancestors.include?(Dry::Struct)
|
14
|
+
raise(TypeError, "#{name} should be included on the singleton class of a Dry::Struct subclass")
|
15
|
+
end
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
# `Types` overrides default types to help shorthand Type::String.
|
18
|
+
# To access Ruby's native types within a service, use `::`, ie. `::String`
|
19
|
+
klass.attached_object.include(Types)
|
18
20
|
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
21
|
end
|
24
|
-
end
|
25
22
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
23
|
+
# Defines an argument using the ServiceBase DSL.
|
24
|
+
# Under the hood, this uses dry-struct's attribute DSL.
|
25
|
+
def argument(name, type, configuration = {}, &)
|
26
|
+
description = configuration[:description] == "" ? nil : configuration[:description]
|
27
|
+
type = type.meta(description:)
|
28
|
+
|
29
|
+
default = configuration[:default]
|
30
|
+
validate_frozen_default!(name:, default:)
|
31
|
+
|
32
|
+
optional = configuration.fetch(:optional, false)
|
33
|
+
validate_optional_or_default!(optional:, default:, name:)
|
34
|
+
|
35
|
+
type = set_default(type:, default:)
|
36
|
+
|
37
|
+
if optional
|
38
|
+
# attribute? allows the key to be omitted.
|
39
|
+
# .optional allows the value to be nil.
|
40
|
+
# https://dry-rb.org/gems/dry-types/1.2/optional-values/
|
41
|
+
# https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L141-L169
|
42
|
+
attribute?(name, type.optional, &)
|
43
|
+
else
|
44
|
+
# https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L30-L104
|
45
|
+
attribute(name, type, &)
|
46
|
+
end
|
49
47
|
end
|
50
|
-
end
|
51
48
|
|
52
|
-
|
49
|
+
private
|
53
50
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
51
|
+
# Raises a warning from dry-types to avoid memory sharing.
|
52
|
+
# https://github.com/dry-rb/dry-types/blob/master/lib/dry/types/builder.rb#L71-L81
|
53
|
+
def validate_frozen_default!(name:, default:)
|
54
|
+
return if default.frozen?
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
56
|
+
raise(
|
57
|
+
ArgumentError,
|
58
|
+
"#{default} provided as a default value for #{name} is mutable. " \
|
59
|
+
"Please `.freeze` your `default:` input.",
|
60
|
+
)
|
61
|
+
end
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
# Do not allow setting both a default value and optional: true. If both
|
64
|
+
# are specified, the default will not be used.
|
65
|
+
def validate_optional_or_default!(optional:, default:, name:)
|
66
|
+
return unless optional && !default.nil?
|
70
67
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
raise(
|
69
|
+
ArgumentError,
|
70
|
+
"#{name} cannot specify both a default value and optional: true. " \
|
71
|
+
"Only specify a default value if the value is optional.",
|
72
|
+
)
|
73
|
+
end
|
77
74
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
75
|
+
# Ensures that provided args are declared as `argument`s
|
76
|
+
def validate_args!(args:)
|
77
|
+
invalid_args = (args.keys - attribute_names)
|
78
|
+
return if invalid_args.empty?
|
82
79
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
80
|
+
raise(
|
81
|
+
ArgumentError,
|
82
|
+
"#{self} provided invalid arguments: #{invalid_args.join(', ')}",
|
83
|
+
)
|
84
|
+
end
|
88
85
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
86
|
+
# Sets the default value on the type.
|
87
|
+
# For primitive types, the default can be set after initialization.
|
88
|
+
# For enums, the default must be set during initialization. Therefore,
|
89
|
+
# we must check the type of the enum and then reconstruct the enum with
|
90
|
+
# the default value being set.
|
91
|
+
# See "Note" in https://dry-rb.org/gems/dry-types/1.2/enum/
|
92
|
+
def set_default(type:, default:)
|
93
|
+
return type if default.nil?
|
94
|
+
|
95
|
+
if type.is_a?(Dry::Types::Enum)
|
96
|
+
values = type.values
|
97
|
+
type_class = "Types::#{values.first.class}".constantize
|
98
|
+
type_class.default(default).enum(*values)
|
99
|
+
else
|
100
|
+
type.default(default)
|
101
|
+
end
|
104
102
|
end
|
105
103
|
end
|
106
104
|
end
|
data/lib/service_base/service.rb
CHANGED
@@ -1,116 +1,112 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'dry-matcher'
|
4
|
-
require 'dry-struct'
|
5
|
-
require 'dry/matcher/result_matcher'
|
6
3
|
require 'dry/monads'
|
7
4
|
require 'dry/monads/do'
|
5
|
+
require 'dry/matcher/result_matcher'
|
6
|
+
require 'dry-types'
|
7
|
+
require 'dry-struct'
|
8
|
+
require 'dry-matcher'
|
8
9
|
require 'memery'
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
module ServiceBase
|
12
|
+
class Service < Dry::Struct
|
13
|
+
extend Dry::Monads::Result::Mixin::Constructors
|
14
|
+
include Dry::Monads::Do.for(:call)
|
15
|
+
include Dry::Monads[:result, :do]
|
16
|
+
include Dry.Types()
|
14
17
|
|
15
|
-
|
16
|
-
|
18
|
+
extend ArgumentTypeAnnotations
|
19
|
+
include Memery
|
17
20
|
|
18
|
-
|
19
|
-
|
21
|
+
class ServiceNotSuccessful < StandardError
|
22
|
+
attr_reader(:failure)
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
def initialize(failure)
|
25
|
+
super('Failed to call service')
|
26
|
+
@failure = failure
|
27
|
+
end
|
24
28
|
end
|
25
|
-
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
class << self
|
31
|
+
# The public class call method.
|
32
|
+
#
|
33
|
+
# The default empty hash is important to prevent an argument error when
|
34
|
+
# passing no arguments to a service that defines defaults for every argument.
|
35
|
+
def call(args = {}, &block)
|
36
|
+
validate_args!(args: args)
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
+
result = new(args).call
|
39
|
+
match_result(result, &block)
|
40
|
+
end
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
def call!(*args)
|
43
|
+
result = call(*args)
|
44
|
+
raise(ServiceNotSuccessful, result.failure) if result.failure?
|
42
45
|
|
43
|
-
|
44
|
-
|
46
|
+
result
|
47
|
+
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
# Pretty prints (pp) the description of the service, ie. `MyService.pp`
|
50
|
+
def pp
|
51
|
+
logger = Logger.new($stdout)
|
52
|
+
logger.info("#{name}: #{service_description}")
|
53
|
+
logger.info('Arguments')
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
schema_definition.each do |arg|
|
56
|
+
logger.info(" #{arg[:name]} (#{arg[:type]}): #{arg[:description]}")
|
57
|
+
end
|
54
58
|
end
|
55
|
-
end
|
56
59
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
60
|
+
# @description getter
|
61
|
+
def service_description
|
62
|
+
@description || 'No description'
|
63
|
+
end
|
61
64
|
|
62
|
-
|
65
|
+
private
|
63
66
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
67
|
+
# Set the description on the service
|
68
|
+
def description(text)
|
69
|
+
@description = text
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
# Employs ResultMatcher to unwrap values using `on.success` & `on.failure`
|
73
|
+
# syntax. If not using block form to extract the result of a service,
|
74
|
+
# ie. `MyService.call.fmap { |result| result + 2 }`, ensure you explictly
|
75
|
+
# handle Failures. See https://dry-rb.org/gems/dry-monads/1.3/result/
|
76
|
+
def match_result(result, &block)
|
77
|
+
# https://medium.com/swlh/better-rails-service-objects-with-dry-rb-702687394e3d
|
78
|
+
if block
|
79
|
+
# raises Dry::Matcher::NonExhaustiveMatchError: cases +failure+ not handled
|
80
|
+
# if `on.failure` is not declared
|
81
|
+
Dry::Matcher::ResultMatcher.call(result, &block)
|
82
|
+
else
|
83
|
+
result
|
84
|
+
end
|
81
85
|
end
|
82
|
-
end
|
83
86
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
87
|
+
# Introspects the arguments DSL to extract information on each argument
|
88
|
+
def schema_definition
|
89
|
+
attribute_names.each_with_object([]) do |attribute_name, list|
|
90
|
+
dry_type = schema.key(attribute_name)
|
91
|
+
list << {
|
92
|
+
name: attribute_name,
|
93
|
+
description: dry_type.meta[:description],
|
94
|
+
type: dry_type.type.name
|
95
|
+
}
|
96
|
+
end
|
93
97
|
end
|
94
98
|
end
|
95
|
-
end
|
96
|
-
|
97
|
-
private
|
98
99
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
100
|
+
# Returns a hash of all arguments and their values
|
101
|
+
def arguments
|
102
|
+
attributes
|
103
|
+
end
|
103
104
|
|
104
|
-
|
105
|
-
def locale(selector, args = {})
|
106
|
-
class_name = self.class.name.gsub('::', '.').underscore
|
107
|
-
I18n.t(".#{selector}", scope: "services.#{class_name}", **args)
|
108
|
-
end
|
105
|
+
private
|
109
106
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
super(ResponseError.new(message: message, code: code), trace)
|
107
|
+
# The call method that must be defined by every inheriting service class
|
108
|
+
def call
|
109
|
+
raise(NotImplementedError)
|
114
110
|
end
|
115
111
|
end
|
116
112
|
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.2
|
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-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-matcher
|
@@ -94,7 +94,6 @@ files:
|
|
94
94
|
- lib/service_base/rspec.rb
|
95
95
|
- lib/service_base/rspec/service_support.rb
|
96
96
|
- lib/service_base/service.rb
|
97
|
-
- lib/service_base/types.rb
|
98
97
|
- lib/service_base/version.rb
|
99
98
|
homepage: https://github.com/kleinjm/service_base
|
100
99
|
licenses:
|
data/lib/service_base/types.rb
DELETED
@@ -1,18 +0,0 @@
|
|
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
|