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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d20e049647526253612ef6cf134c73ce8ab076c69e9a1f3a5524586f035f8ae3
4
- data.tar.gz: 8fe635ae9d66b5358030440828f45e0706a2cad092e7eb45f9738af5c53c9896
3
+ metadata.gz: 79ca15b6d1d305ea1dea4ab6633e63725de9f5d5f951c4221b9b59bef38b03ab
4
+ data.tar.gz: 3bb44ec63bd53452ac09c3fc0f531bf25218dcbb1434b5e8bd0a1dc3f8f75be0
5
5
  SHA512:
6
- metadata.gz: '0595ddabc674a039b1dfb3189f4f7a6d8aa2e3a6c694391b94c26235180f7b67475685b3c5e19283c07f8b8e141b6c99b109d35659af6ef7d4e436f74ab0a832'
7
- data.tar.gz: a38e09b26353baa8a4b37edb97522d33a2bd95a9209f9b930c109f67dbd5d305623c57aa5508c82bdb574150536c37048a5399cb25eed4df5f5c22838456aac4
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
- `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
+ 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 `attributes`. This is a very useful technique for
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(attributes)
196
+ user.update(arguments)
191
197
  end
192
198
  end
193
199
  ```
194
200
 
195
- ### ApplicationRecord Args
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
- If you add `ApplicationRecord = Types.Instance(ApplicationRecord)` in a Rails project, you can accept any `ApplicationRecord` via
206
+ It is recommended that you define your own `Type` module and include it in your `ServiceBase` subclass, as so.
198
207
 
199
- `argument(:my_record, ApplicationRecord)`
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
- You can also limit the type of AR record via
215
+ # app/services/application_service.rb
216
+ class ApplicationService < ServiceBase::Service
217
+ include Types
218
+ end
202
219
 
203
- `argument(:my_record, Types.Instance(MyRecord))`
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
- ## Types
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
- 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,
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 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")
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
- # `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
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
- 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")
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
- # 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, &)
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
- private
49
+ private
53
50
 
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?
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
- raise(
60
- ArgumentError,
61
- "#{default} provided as a default value for #{name} is mutable. " \
62
- "Please `.freeze` your `default:` input.",
63
- )
64
- end
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
- # 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?
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
- 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
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
- # 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?
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
- raise(
84
- ArgumentError,
85
- "#{self} provided invalid arguments: #{invalid_args.join(', ')}",
86
- )
87
- end
80
+ raise(
81
+ ArgumentError,
82
+ "#{self} provided invalid arguments: #{invalid_args.join(', ')}",
83
+ )
84
+ end
88
85
 
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)
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
@@ -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
- class Service < Dry::Struct
11
- extend Dry::Monads::Result::Mixin::Constructors
12
- include Dry::Monads::Do.for(:call)
13
- include Dry::Monads[:result, :do]
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
- extend ArgumentTypeAnnotations
16
- include Memery
18
+ extend ArgumentTypeAnnotations
19
+ include Memery
17
20
 
18
- class ServiceNotSuccessful < StandardError
19
- attr_reader(:failure)
21
+ class ServiceNotSuccessful < StandardError
22
+ attr_reader(:failure)
20
23
 
21
- def initialize(failure)
22
- super('Failed to call service')
23
- @failure = failure
24
+ def initialize(failure)
25
+ super('Failed to call service')
26
+ @failure = failure
27
+ end
24
28
  end
25
- end
26
29
 
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)
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
- result = new(args).call
36
- match_result(result, &block)
37
- end
38
+ result = new(args).call
39
+ match_result(result, &block)
40
+ end
38
41
 
39
- def call!(*args)
40
- result = call(*args)
41
- raise(ServiceNotSuccessful, result.failure) if result.failure?
42
+ def call!(*args)
43
+ result = call(*args)
44
+ raise(ServiceNotSuccessful, result.failure) if result.failure?
42
45
 
43
- result
44
- end
46
+ result
47
+ end
45
48
 
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')
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
- schema_definition.each do |arg|
53
- logger.info(" #{arg[:name]} (#{arg[:type]}): #{arg[:description]}")
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
- # @description getter
58
- def service_description
59
- @description || 'No description'
60
- end
60
+ # @description getter
61
+ def service_description
62
+ @description || 'No description'
63
+ end
61
64
 
62
- private
65
+ private
63
66
 
64
- # Set the description on the service
65
- def description(text)
66
- @description = text
67
- end
67
+ # Set the description on the service
68
+ def description(text)
69
+ @description = text
70
+ end
68
71
 
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
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
- # 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
- }
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
- # The call method that must be defined by every inheriting service class
100
- def call
101
- raise(NotImplementedError)
102
- end
100
+ # Returns a hash of all arguments and their values
101
+ def arguments
102
+ attributes
103
+ end
103
104
 
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
105
+ private
109
106
 
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)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceBase
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
data/lib/service_base.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'service_base/version'
4
- require_relative 'service_base/types'
5
4
  require_relative 'service_base/argument_type_annotations'
6
5
  require_relative 'service_base/service'
7
6
 
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.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-01 00:00:00.000000000 Z
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:
@@ -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