service_base 1.0.0 → 1.0.1

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: b9c9ee7f7424ddd348b82d0708fa79703e7869771284b7018f7e3cb41828770b
4
+ data.tar.gz: 92032edd435bc81747cbd0a15670e3210716cefd51d8fc7598f7fca5194f5127
5
5
  SHA512:
6
- metadata.gz: '0595ddabc674a039b1dfb3189f4f7a6d8aa2e3a6c694391b94c26235180f7b67475685b3c5e19283c07f8b8e141b6c99b109d35659af6ef7d4e436f74ab0a832'
7
- data.tar.gz: a38e09b26353baa8a4b37edb97522d33a2bd95a9209f9b930c109f67dbd5d305623c57aa5508c82bdb574150536c37048a5399cb25eed4df5f5c22838456aac4
6
+ metadata.gz: a95dd087bca43238c0ed3e6c94773f61e05758def00c38e9ac4bfd0f258f0eb3514092e404c9f1fd14a9dc47f6f08b4529869d8fa5e489967848396b57347910
7
+ data.tar.gz: a524e1751b728e6cce6fd723c78b3f5ccb7369e8db0f27a412b42d9be6d4cb35a17e7b6926bae291300ed56e5585b5a97b12dea4b3fdc5efa01f88c86a8be007
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
@@ -175,7 +182,7 @@ A service should also define a `description`. This is
175
182
  recommended for self-documentation, ie.
176
183
 
177
184
  ```ruby
178
- class MyService < Service
185
+ class MyService < ServiceBase::Service
179
186
  description("Does a lot of cool things")
180
187
  end
181
188
  ```
@@ -185,7 +192,7 @@ use `attributes`. This is a very useful technique for
185
192
  services that update an object. For example
186
193
 
187
194
  ```ruby
188
- class User::UpdateService < Service
195
+ class User::UpdateService < ServiceBase::Service
189
196
  def call
190
197
  user.update(attributes)
191
198
  end
@@ -1,106 +1,108 @@
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
10
+
11
+ # `Types` overrides default types to help shorthand Type::String.
12
+ # To access Ruby's native types within a service, use `::`, ie. `::String`
13
+ klass.include(Types)
8
14
  end
9
15
 
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
16
+ def included(klass)
17
+ if !klass.singleton_class? || !klass.attached_object.ancestors.include?(Dry::Struct)
18
+ raise(TypeError, "#{name} should be included on the singleton class of a Dry::Struct subclass")
19
+ end
14
20
 
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")
21
+ # `Types` overrides default types to help shorthand Type::String.
22
+ # To access Ruby's native types within a service, use `::`, ie. `::String`
23
+ klass.attached_object.include(Types)
18
24
  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
25
  end
24
- end
25
26
 
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, &)
27
+ # Defines an argument using the ServiceBase DSL.
28
+ # Under the hood, this uses dry-struct's attribute DSL.
29
+ def argument(name, type, configuration = {}, &)
30
+ description = configuration[:description] == "" ? nil : configuration[:description]
31
+ type = type.meta(description:)
32
+
33
+ default = configuration[:default]
34
+ validate_frozen_default!(name:, default:)
35
+
36
+ optional = configuration.fetch(:optional, false)
37
+ validate_optional_or_default!(optional:, default:, name:)
38
+
39
+ type = set_default(type:, default:)
40
+
41
+ if optional
42
+ # attribute? allows the key to be omitted.
43
+ # .optional allows the value to be nil.
44
+ # https://dry-rb.org/gems/dry-types/1.2/optional-values/
45
+ # https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L141-L169
46
+ attribute?(name, type.optional, &)
47
+ else
48
+ # https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L30-L104
49
+ attribute(name, type, &)
50
+ end
49
51
  end
50
- end
51
52
 
52
- private
53
+ private
53
54
 
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?
55
+ # Raises a warning from dry-types to avoid memory sharing.
56
+ # https://github.com/dry-rb/dry-types/blob/master/lib/dry/types/builder.rb#L71-L81
57
+ def validate_frozen_default!(name:, default:)
58
+ return if default.frozen?
58
59
 
59
- raise(
60
- ArgumentError,
61
- "#{default} provided as a default value for #{name} is mutable. " \
62
- "Please `.freeze` your `default:` input.",
63
- )
64
- end
60
+ raise(
61
+ ArgumentError,
62
+ "#{default} provided as a default value for #{name} is mutable. " \
63
+ "Please `.freeze` your `default:` input.",
64
+ )
65
+ end
65
66
 
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?
67
+ # Do not allow setting both a default value and optional: true. If both
68
+ # are specified, the default will not be used.
69
+ def validate_optional_or_default!(optional:, default:, name:)
70
+ return unless optional && !default.nil?
70
71
 
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
72
+ raise(
73
+ ArgumentError,
74
+ "#{name} cannot specify both a default value and optional: true. " \
75
+ "Only specify a default value if the value is optional.",
76
+ )
77
+ end
77
78
 
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?
79
+ # Ensures that provided args are declared as `argument`s
80
+ def validate_args!(args:)
81
+ invalid_args = (args.keys - attribute_names)
82
+ return if invalid_args.empty?
82
83
 
83
- raise(
84
- ArgumentError,
85
- "#{self} provided invalid arguments: #{invalid_args.join(', ')}",
86
- )
87
- end
84
+ raise(
85
+ ArgumentError,
86
+ "#{self} provided invalid arguments: #{invalid_args.join(', ')}",
87
+ )
88
+ end
88
89
 
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)
90
+ # Sets the default value on the type.
91
+ # For primitive types, the default can be set after initialization.
92
+ # For enums, the default must be set during initialization. Therefore,
93
+ # we must check the type of the enum and then reconstruct the enum with
94
+ # the default value being set.
95
+ # See "Note" in https://dry-rb.org/gems/dry-types/1.2/enum/
96
+ def set_default(type:, default:)
97
+ return type if default.nil?
98
+
99
+ if type.is_a?(Dry::Types::Enum)
100
+ values = type.values
101
+ type_class = "Types::#{values.first.class}".constantize
102
+ type_class.default(default).enum(*values)
103
+ else
104
+ type.default(default)
105
+ end
104
106
  end
105
107
  end
106
108
  end
@@ -7,110 +7,112 @@ require 'dry/monads'
7
7
  require 'dry/monads/do'
8
8
  require 'memery'
9
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]
10
+ module ServiceBase
11
+ class Service < Dry::Struct
12
+ extend Dry::Monads::Result::Mixin::Constructors
13
+ include Dry::Monads::Do.for(:call)
14
+ include Dry::Monads[:result, :do]
14
15
 
15
- extend ArgumentTypeAnnotations
16
- include Memery
16
+ extend ArgumentTypeAnnotations
17
+ include Memery
17
18
 
18
- class ServiceNotSuccessful < StandardError
19
- attr_reader(:failure)
19
+ class ServiceNotSuccessful < StandardError
20
+ attr_reader(:failure)
20
21
 
21
- def initialize(failure)
22
- super('Failed to call service')
23
- @failure = failure
22
+ def initialize(failure)
23
+ super('Failed to call service')
24
+ @failure = failure
25
+ end
24
26
  end
25
- end
26
27
 
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)
28
+ class << self
29
+ # The public class call method.
30
+ #
31
+ # The default empty hash is important to prevent an argument error when
32
+ # passing no arguments to a service that defines defaults for every argument.
33
+ def call(args = {}, &block)
34
+ validate_args!(args: args)
34
35
 
35
- result = new(args).call
36
- match_result(result, &block)
37
- end
36
+ result = new(args).call
37
+ match_result(result, &block)
38
+ end
38
39
 
39
- def call!(*args)
40
- result = call(*args)
41
- raise(ServiceNotSuccessful, result.failure) if result.failure?
40
+ def call!(*args)
41
+ result = call(*args)
42
+ raise(ServiceNotSuccessful, result.failure) if result.failure?
42
43
 
43
- result
44
- end
44
+ result
45
+ end
45
46
 
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')
47
+ # Pretty prints (pp) the description of the service, ie. `MyService.pp`
48
+ def pp
49
+ logger = Logger.new($stdout)
50
+ logger.info("#{name}: #{service_description}")
51
+ logger.info('Arguments')
51
52
 
52
- schema_definition.each do |arg|
53
- logger.info(" #{arg[:name]} (#{arg[:type]}): #{arg[:description]}")
53
+ schema_definition.each do |arg|
54
+ logger.info(" #{arg[:name]} (#{arg[:type]}): #{arg[:description]}")
55
+ end
54
56
  end
55
- end
56
57
 
57
- # @description getter
58
- def service_description
59
- @description || 'No description'
60
- end
58
+ # @description getter
59
+ def service_description
60
+ @description || 'No description'
61
+ end
61
62
 
62
- private
63
+ private
63
64
 
64
- # Set the description on the service
65
- def description(text)
66
- @description = text
67
- end
65
+ # Set the description on the service
66
+ def description(text)
67
+ @description = text
68
+ end
68
69
 
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
70
+ # Employs ResultMatcher to unwrap values using `on.success` & `on.failure`
71
+ # syntax. If not using block form to extract the result of a service,
72
+ # ie. `MyService.call.fmap { |result| result + 2 }`, ensure you explictly
73
+ # handle Failures. See https://dry-rb.org/gems/dry-monads/1.3/result/
74
+ def match_result(result, &block)
75
+ # https://medium.com/swlh/better-rails-service-objects-with-dry-rb-702687394e3d
76
+ if block
77
+ # raises Dry::Matcher::NonExhaustiveMatchError: cases +failure+ not handled
78
+ # if `on.failure` is not declared
79
+ Dry::Matcher::ResultMatcher.call(result, &block)
80
+ else
81
+ result
82
+ end
81
83
  end
82
- end
83
84
 
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
- }
85
+ # Introspects the arguments DSL to extract information on each argument
86
+ def schema_definition
87
+ attribute_names.each_with_object([]) do |attribute_name, list|
88
+ dry_type = schema.key(attribute_name)
89
+ list << {
90
+ name: attribute_name,
91
+ description: dry_type.meta[:description],
92
+ type: dry_type.type.name
93
+ }
94
+ end
93
95
  end
94
96
  end
95
- end
96
97
 
97
- private
98
+ 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
+ # The call method that must be defined by every inheriting service class
101
+ def call
102
+ raise(NotImplementedError)
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
+ # A locale lookup helper that uses the name of the service
106
+ def locale(selector, args = {})
107
+ class_name = self.class.name.gsub('::', '.').underscore
108
+ I18n.t(".#{selector}", scope: "services.#{class_name}", **args)
109
+ end
109
110
 
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)
111
+ # Structured Monad Result Failure type for returning a ResponseError
112
+ class ResponseFailure < Dry::Monads::Result::Failure
113
+ def initialize(message, code, trace = Dry::Monads::RightBiased::Left.trace_caller)
114
+ super(ResponseError.new(message: message, code: code), trace)
115
+ end
114
116
  end
115
117
  end
116
118
  end
@@ -10,9 +10,11 @@
10
10
 
11
11
  require 'dry-types'
12
12
 
13
- module Types
14
- include Dry.Types()
13
+ module ServiceBase
14
+ module Types
15
+ include Dry.Types()
15
16
 
16
- UpCasedString = Types::String.constructor(&:upcase)
17
- Boolean = Bool # alias the built in type, Bool
17
+ UpCasedString = Types::String.constructor(&:upcase)
18
+ Boolean = Bool # alias the built in type, Bool
19
+ end
18
20
  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.1'
5
5
  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.0
4
+ version: 1.0.1
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