service_actor 4.0.0 → 5.0.0

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: 86aa1f5411838fa24961e50eff4138d414c86aa99c7bdc5907953f7643406431
4
- data.tar.gz: 1e0c82c0ba0e3b3c8278fa7d286454f891266972d34144b2ee3aa18a2bddd20d
3
+ metadata.gz: 971f7ed20085d9eb94a796326b5a9f06d9080d765fbea5b3f80e7f311eb48a3e
4
+ data.tar.gz: 38c6168b68139741f7c3209ec8cbceb90028c1ce0029b4a22d5e0535e0c544da
5
5
  SHA512:
6
- metadata.gz: ed8bfce6061647dee123bfdecc2a89f92cb7b4fc09664bef3671d722e59605787acfe3ceb9822b785a4a0ed651b315a9c27a38366516cbeadef9403d92c49709
7
- data.tar.gz: 9dec2bb8b9d5815c1c8d39108432003575f2f5a9a9431c93c69228c2c13f776eea60794696328e2d9ebc6e13849e2fa3e6db3e6e3ac337c03a39d29b24c578ea
6
+ metadata.gz: 8a26e9cac7270f392e7cb58b96cd6a899f92f36f603ad175e9ec9965449b4a1c0ad58cef053252bc5c31d2dc3b570e3bc77a64a88dc9f5d9890f05268f17b1c2
7
+ data.tar.gz: bc08967e46472cc15f71f516290f67dd01dd244786ea4881fc9df82db30b2e199171bf88f2a4ad4497d51274cbff24bcd2434de66647f2035cb7d0de785ad28b
data/README.md CHANGED
@@ -4,7 +4,7 @@ This Ruby gem lets you move your application logic into small composable
4
4
  service objects. It is a lightweight framework that helps you keep your models
5
5
  and controllers thin.
6
6
 
7
- ![Photo of theater seats](https://user-images.githubusercontent.com/132/78340166-e7567000-7595-11ea-97c0-b3e5da2de7a1.png)
7
+ ![Photo of red and black theater seats with lighting from the top](https://user-images.githubusercontent.com/132/78340166-e7567000-7595-11ea-97c0-b3e5da2de7a1.png)
8
8
 
9
9
  ## Contents
10
10
 
@@ -24,6 +24,7 @@ and controllers thin.
24
24
  - [Conditions](#conditions)
25
25
  - [Types](#types)
26
26
  - [Custom input errors](#custom-input-errors)
27
+ - [Custom type validations](#custom-type-validations)
27
28
  - [Testing](#testing)
28
29
  - [FAQ](#faq)
29
30
  - [Thanks](#thanks)
@@ -40,19 +41,8 @@ bundle add service_actor
40
41
 
41
42
  ### Extensions
42
43
 
43
- For **Rails generators**, you can use the
44
- [service_actor-rails](https://github.com/sunny/actor-rails) gem:
45
-
46
- ```sh
47
- bundle add service_actor-rails
48
- ```
49
-
50
- For **TTY prompts**, you can use the
51
- [service_actor-promptable](https://github.com/pboling/service_actor-promptable) gem:
52
-
53
- ```sh
54
- bundle add service_actor-promptable
55
- ```
44
+ - Rails generators: [service_actor-rails](https://github.com/sunny/actor-rails)
45
+ - TTY prompts: [service_actor-promptable](https://github.com/pboling/service_actor-promptable)
56
46
 
57
47
  ## Usage
58
48
 
@@ -75,9 +65,9 @@ Trigger them in your application with `.call`:
75
65
  SendNotification.call # => <ServiceActor::Result…>
76
66
  ```
77
67
 
78
- When called, an actor returns a result. Reading and writing to this result allows
79
- actors to accept and return multiple arguments. Let’s find out how to do that
80
- and then we’ll see how to
68
+ When called, an actor returns a result. Reading and writing to this result
69
+ allows actors to accept and return multiple arguments. Let’s find out how to do
70
+ that and then we’ll see how to
81
71
  [chain multiple actors together](#play-actors-in-a-sequence).
82
72
 
83
73
  ### Inputs
@@ -181,6 +171,11 @@ class UsersController < ApplicationController
181
171
  end
182
172
  ```
183
173
 
174
+ > [!WARNING]
175
+ > If you specify the type option for output fields, it will not be enforced for
176
+ > failed actors.
177
+ > As a result, their output might not match the specified type.
178
+
184
179
  ## Play actors in a sequence
185
180
 
186
181
  To help you create actors that are small, single-responsibility actions, an
@@ -338,15 +333,15 @@ actor = BuildGreeting.call(name: "Siobhan", adjective: "elegant")
338
333
  actor.greeting # => "Have an elegant week, Siobhan!"
339
334
  ```
340
335
 
341
- While lambdas are the preferred way to specify defaults, you can also provide
342
- a default value without using lambdas by using an immutable object.
336
+ While lambdas are the preferred way to specify defaults, you can also provide a
337
+ default value without using lambdas by using an immutable object.
343
338
 
344
339
  ```rb
345
340
  # frozen_string_literal: true
346
341
 
347
342
  class ExampleActor < Actor
348
343
  input :options, default: {
349
- names: {male: "Iaroslav", female: "Anna"}.freeze,
344
+ names: {man: "Iaroslav", woman: "Anna"}.freeze,
350
345
  country_codes: %w[gb ru].freeze
351
346
  }.freeze
352
347
  end
@@ -357,7 +352,8 @@ are references to mutable objects, e.g.
357
352
 
358
353
  ```rb
359
354
  class ExampleActor < Actor
360
- input :options, default: -> { Registry::DEFAULT_OPTIONS } # `Registry::DEFAULT_OPTIONS` is not frozen
355
+ # `Registry::DEFAULT_OPTIONS` is not frozen
356
+ input :options, default: -> { Registry::DEFAULT_OPTIONS }
361
357
 
362
358
  def call
363
359
  options[:names] = nil
@@ -529,6 +525,50 @@ end
529
525
 
530
526
  </details>
531
527
 
528
+ ### Custom type validations
529
+
530
+ This gem provides a minimal API for checking the types of `input` and `output`
531
+ values:
532
+
533
+ - A direct class match: `input :age, type: Integer`
534
+ - A choice between classes: `output :height, type: [Integer, Float]`
535
+
536
+ More complex type checks are outside the scope of this gem. However, type
537
+ checking is performed using Ruby’s `===` method.
538
+ This means you can define a custom class with a `===` method to implement your
539
+ own type logic.
540
+
541
+ For example, to define a “positive integer” type, you can create a custom class:
542
+
543
+ ```ruby
544
+ class PositiveInteger
545
+ class << self
546
+ def ===(value)
547
+ value.is_a?(Integer) && value.positive?
548
+ end
549
+ end
550
+ end
551
+ ```
552
+
553
+ Then you can use it in an actor:
554
+
555
+ ```ruby
556
+ class AgeActor < Actor
557
+ input :age, type: PositiveInteger
558
+ end
559
+
560
+ AgeActor.call(age: 25)
561
+ # => #<ServiceActor::Result {age: 25}>
562
+ AgeActor.call(age: -42)
563
+ # ServiceActor::ArgumentError: The "age" input on "AgeActor" must be of type
564
+ # "PositiveInteger" but was "Integer" (ServiceActor::ArgumentError)
565
+ ```
566
+
567
+ This approach also allows you to define adapters for third-party validation
568
+ gems, providing the flexibility to integrate custom type checks.
569
+
570
+ See [more examples](./docs/examples/custom_types).
571
+
532
572
  ## Testing
533
573
 
534
574
  In your application, add automated testing to your actors as you would do to any
@@ -4,8 +4,11 @@ module ServiceActor::ArgumentsValidator
4
4
  module_function
5
5
 
6
6
  def validate_origin_name(name, origin:)
7
- return if name.to_sym == :error
8
- return unless ServiceActor::Result.instance_methods.include?(name.to_sym)
7
+ name = name.to_sym
8
+ return if name == :error
9
+
10
+ methods = ServiceActor::Core.instance_methods + ServiceActor::Result.instance_methods
11
+ return unless methods.include?(name)
9
12
 
10
13
  raise ArgumentError,
11
14
  "#{origin} `#{name}` overrides `ServiceActor::Result` instance method"
@@ -23,11 +23,15 @@ module ServiceActor::Attributable
23
23
 
24
24
  def input(name, **arguments)
25
25
  ServiceActor::ArgumentsValidator.validate_origin_name(
26
- name, origin: :input
26
+ name,
27
+ origin: :input,
27
28
  )
28
29
 
29
30
  ServiceActor::ArgumentsValidator.validate_default_value(
30
- arguments[:default], actor: self, origin_type: :input, origin_name: name
31
+ arguments[:default],
32
+ actor: self,
33
+ origin_type: :input,
34
+ origin_name: name,
31
35
  )
32
36
 
33
37
  inputs[name] = arguments
@@ -48,11 +52,15 @@ module ServiceActor::Attributable
48
52
 
49
53
  def output(name, **arguments)
50
54
  ServiceActor::ArgumentsValidator.validate_origin_name(
51
- name, origin: :output
55
+ name,
56
+ origin: :output,
52
57
  )
53
58
 
54
59
  ServiceActor::ArgumentsValidator.validate_default_value(
55
- arguments[:default], actor: self, origin_type: :output, origin_name: name
60
+ arguments[:default],
61
+ actor: self,
62
+ origin_type: :output,
63
+ origin_name: name,
56
64
  )
57
65
 
58
66
  outputs[name] = arguments
@@ -28,7 +28,15 @@ class ServiceActor::Checks::InclusionCheck < ServiceActor::Checks::Base
28
28
  private_constant :DEFAULT_MESSAGE
29
29
 
30
30
  class << self
31
- def check(check_name:, input_key:, actor:, conditions:, result:, **)
31
+ def check(
32
+ check_name:,
33
+ input_key:,
34
+ actor:,
35
+ conditions:,
36
+ result:,
37
+ input_options:,
38
+ **
39
+ )
32
40
  # DEPRECATED: `in` is deprecated in favor of `inclusion`.
33
41
  return unless %i[inclusion in].include?(check_name)
34
42
 
@@ -37,42 +45,47 @@ class ServiceActor::Checks::InclusionCheck < ServiceActor::Checks::Base
37
45
  actor: actor,
38
46
  inclusion: conditions,
39
47
  value: result[input_key],
48
+ input_options: input_options,
40
49
  ).check
41
50
  end
42
51
  end
43
52
 
44
- def initialize(input_key:, actor:, inclusion:, value:)
53
+ def initialize(input_key:, actor:, inclusion:, value:, input_options:)
45
54
  super()
46
55
 
47
56
  @input_key = input_key
48
57
  @actor = actor
49
58
  @inclusion = inclusion
50
59
  @value = value
60
+ @input_options = input_options
51
61
  end
52
62
 
53
63
  def check
54
64
  inclusion_in, message = define_inclusion_and_message
55
65
 
56
66
  return if inclusion_in.nil?
57
- return if inclusion_in.include?(@value)
67
+ return if inclusion_in.include?(value)
68
+ return if input_options[:allow_nil] && value.nil?
58
69
 
59
70
  add_argument_error(
60
71
  message,
61
- input_key: @input_key,
62
- actor: @actor,
72
+ input_key: input_key,
73
+ actor: actor,
63
74
  inclusion_in: inclusion_in,
64
- value: @value,
75
+ value: value,
65
76
  )
66
77
  end
67
78
 
68
79
  private
69
80
 
81
+ attr_reader :value, :inclusion, :input_key, :actor, :input_options
82
+
70
83
  def define_inclusion_and_message
71
- if @inclusion.is_a?(Hash)
72
- @inclusion[:message] ||= DEFAULT_MESSAGE
73
- @inclusion.values_at(:in, :message)
84
+ if inclusion.is_a?(Hash)
85
+ inclusion[:message] ||= DEFAULT_MESSAGE
86
+ inclusion.values_at(:in, :message)
74
87
  else
75
- [@inclusion, DEFAULT_MESSAGE]
88
+ [inclusion, DEFAULT_MESSAGE]
76
89
  end
77
90
  end
78
91
  end
@@ -34,7 +34,15 @@ class ServiceActor::Checks::MustCheck < ServiceActor::Checks::Base
34
34
  private_constant :DEFAULT_MESSAGE
35
35
 
36
36
  class << self
37
- def check(check_name:, input_key:, actor:, conditions:, result:, **)
37
+ def check(
38
+ check_name:,
39
+ input_key:,
40
+ actor:,
41
+ conditions:,
42
+ result:,
43
+ input_options:,
44
+ **
45
+ )
38
46
  return unless check_name == :must
39
47
 
40
48
  new(
@@ -42,47 +50,56 @@ class ServiceActor::Checks::MustCheck < ServiceActor::Checks::Base
42
50
  actor: actor,
43
51
  nested_checks: conditions,
44
52
  value: result[input_key],
53
+ input_options: input_options,
45
54
  ).check
46
55
  end
47
56
  end
48
57
 
49
- def initialize(input_key:, actor:, nested_checks:, value:)
58
+ def initialize(input_key:, actor:, nested_checks:, value:, input_options:)
50
59
  super()
51
60
 
52
61
  @input_key = input_key
53
62
  @actor = actor
54
63
  @nested_checks = nested_checks
55
64
  @value = value
65
+ @input_options = input_options
56
66
  end
57
67
 
58
68
  def check
59
- @nested_checks.each do |nested_check_name, nested_check_conditions|
60
- message = prepared_message_with(nested_check_name, nested_check_conditions) # rubocop:disable Layout/LineLength
69
+ return if input_options[:allow_nil] && value.nil?
70
+
71
+ nested_checks.each do |nested_check_name, nested_check_conditions|
72
+ message = prepared_message_with(
73
+ nested_check_name,
74
+ nested_check_conditions,
75
+ )
61
76
 
62
77
  next unless message
63
78
 
64
79
  add_argument_error(
65
80
  message,
66
- input_key: @input_key,
67
- actor: @actor,
81
+ input_key: input_key,
82
+ actor: actor,
68
83
  check_name: nested_check_name,
69
- value: @value,
84
+ value: value,
70
85
  )
71
86
  end
72
87
 
73
- @argument_errors
88
+ argument_errors
74
89
  end
75
90
 
76
91
  private
77
92
 
93
+ attr_reader :input_key, :actor, :nested_checks, :value, :input_options
94
+
78
95
  def prepared_message_with(nested_check_name, nested_check_conditions)
79
96
  check, message = define_check_and_message_from(nested_check_conditions)
80
97
 
81
- return if check.call(@value)
98
+ return if check.call(value)
82
99
 
83
100
  message
84
101
  rescue StandardError => e
85
- "The \"#{@input_key}\" input on \"#{@actor}\" has an error in the code " \
102
+ "The \"#{input_key}\" input on \"#{actor}\" has an error in the code " \
86
103
  "inside \"#{nested_check_name}\": [#{e.class}] #{e.message}"
87
104
  end
88
105
 
@@ -75,23 +75,25 @@ class ServiceActor::Checks::NilCheck < ServiceActor::Checks::Base
75
75
  end
76
76
 
77
77
  def check
78
- return unless @value.nil?
78
+ return unless value.nil?
79
79
 
80
80
  allow_nil, message =
81
- define_allow_nil_and_message_from(@input_options[:allow_nil])
81
+ define_allow_nil_and_message_from(input_options[:allow_nil])
82
82
 
83
83
  return if allow_nil?(allow_nil)
84
84
 
85
85
  add_argument_error(
86
86
  message,
87
- origin: @origin,
88
- input_key: @input_key,
89
- actor: @actor,
87
+ origin: origin,
88
+ input_key: input_key,
89
+ actor: actor,
90
90
  )
91
91
  end
92
92
 
93
93
  private
94
94
 
95
+ attr_reader :origin, :input_key, :input_options, :actor, :allow_nil, :value
96
+
95
97
  def define_allow_nil_and_message_from(allow_nil)
96
98
  if allow_nil.is_a?(Hash)
97
99
  allow_nil[:message] ||= DEFAULT_MESSAGE
@@ -104,10 +106,10 @@ class ServiceActor::Checks::NilCheck < ServiceActor::Checks::Base
104
106
  def allow_nil?(tmp_allow_nil)
105
107
  return tmp_allow_nil unless tmp_allow_nil.nil?
106
108
 
107
- if @input_options.key?(:default) && @input_options[:default].nil?
109
+ if input_options.key?(:default) && input_options[:default].nil?
108
110
  return true
109
111
  end
110
112
 
111
- !@input_options[:type]
113
+ !input_options[:type]
112
114
  end
113
115
  end
@@ -67,36 +67,39 @@ class ServiceActor::Checks::TypeCheck < ServiceActor::Checks::Base
67
67
  end
68
68
 
69
69
  def check
70
- return if @type_definition.nil?
71
- return if @given_type.nil?
70
+ return if type_definition.nil?
71
+ return if given_type.nil?
72
72
 
73
73
  types, message = define_types_and_message
74
74
 
75
- return if types.any? { |type| type === @given_type }
75
+ return if types.any? { |type| type === given_type }
76
76
 
77
77
  add_argument_error(
78
78
  message,
79
- origin: @origin,
80
- input_key: @input_key,
81
- actor: @actor,
79
+ origin: origin,
80
+ input_key: input_key,
81
+ actor: actor,
82
82
  expected_type: types.map(&:name).join(", "),
83
- given_type: @given_type.class,
83
+ given_type: given_type.class,
84
84
  )
85
85
  end
86
86
 
87
87
  private
88
88
 
89
+ attr_reader :origin, :input_key, :actor, :type_definition, :given_type
90
+
89
91
  def define_types_and_message
90
- if @type_definition.is_a?(Hash)
91
- @type_definition[:message] ||= DEFAULT_MESSAGE
92
+ definition = type_definition
93
+
94
+ if definition.is_a?(Hash)
95
+ definition[:message] ||= DEFAULT_MESSAGE
92
96
 
93
- @type_definition, message =
94
- @type_definition.values_at(:is, :message)
97
+ definition, message = definition.values_at(:is, :message)
95
98
  else
96
99
  message = DEFAULT_MESSAGE
97
100
  end
98
101
 
99
- types = Array(@type_definition).map do |name|
102
+ types = Array(definition).map do |name|
100
103
  name.is_a?(String) ? Object.const_get(name) : name
101
104
  end
102
105
 
@@ -19,6 +19,7 @@ class ServiceActor::Result < BasicObject
19
19
  instance_variables
20
20
  is_a?
21
21
  kind_of?
22
+ method
22
23
  methods
23
24
  nil?
24
25
  object_id
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceActor
4
- VERSION = "4.0.0"
4
+ VERSION = "5.0.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: service_actor
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sunny Ripert
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-24 00:00:00.000000000 Z
10
+ date: 2025-09-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '18.2'
74
+ version: '24.0'
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '18.2'
81
+ version: '24.0'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: standard-rubocop-lts
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -219,6 +219,34 @@ dependencies:
219
219
  - - ">="
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0.0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: irb
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '1.14'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '1.14'
236
+ - !ruby/object:Gem::Dependency
237
+ name: rdoc
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '6.10'
243
+ type: :development
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '6.10'
222
250
  description: Service objects for your application logic
223
251
  email:
224
252
  - sunny@sunfox.org
@@ -267,7 +295,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
267
295
  requirements:
268
296
  - - ">="
269
297
  - !ruby/object:Gem::Version
270
- version: '3.0'
298
+ version: '3.2'
271
299
  required_rubygems_version: !ruby/object:Gem::Requirement
272
300
  requirements:
273
301
  - - ">="