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 +4 -4
- data/README.md +61 -21
- data/lib/service_actor/arguments_validator.rb +5 -2
- data/lib/service_actor/attributable.rb +12 -4
- data/lib/service_actor/checks/inclusion_check.rb +23 -10
- data/lib/service_actor/checks/must_check.rb +27 -10
- data/lib/service_actor/checks/nil_check.rb +9 -7
- data/lib/service_actor/checks/type_check.rb +15 -12
- data/lib/service_actor/result.rb +1 -0
- data/lib/service_actor/version.rb +1 -1
- metadata +33 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 971f7ed20085d9eb94a796326b5a9f06d9080d765fbea5b3f80e7f311eb48a3e
|
4
|
+
data.tar.gz: 38c6168b68139741f7c3209ec8cbceb90028c1ce0029b4a22d5e0535e0c544da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-

|
7
|
+

|
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
|
-
|
44
|
-
[service_actor-
|
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
|
79
|
-
actors to accept and return multiple arguments. Let’s find out how to do
|
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
|
-
|
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: {
|
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
|
-
|
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
|
-
|
8
|
-
return
|
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,
|
26
|
+
name,
|
27
|
+
origin: :input,
|
27
28
|
)
|
28
29
|
|
29
30
|
ServiceActor::ArgumentsValidator.validate_default_value(
|
30
|
-
arguments[:default],
|
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,
|
55
|
+
name,
|
56
|
+
origin: :output,
|
52
57
|
)
|
53
58
|
|
54
59
|
ServiceActor::ArgumentsValidator.validate_default_value(
|
55
|
-
arguments[:default],
|
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(
|
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?(
|
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:
|
62
|
-
actor:
|
72
|
+
input_key: input_key,
|
73
|
+
actor: actor,
|
63
74
|
inclusion_in: inclusion_in,
|
64
|
-
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
|
72
|
-
|
73
|
-
|
84
|
+
if inclusion.is_a?(Hash)
|
85
|
+
inclusion[:message] ||= DEFAULT_MESSAGE
|
86
|
+
inclusion.values_at(:in, :message)
|
74
87
|
else
|
75
|
-
[
|
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(
|
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
|
-
|
60
|
-
|
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:
|
67
|
-
actor:
|
81
|
+
input_key: input_key,
|
82
|
+
actor: actor,
|
68
83
|
check_name: nested_check_name,
|
69
|
-
value:
|
84
|
+
value: value,
|
70
85
|
)
|
71
86
|
end
|
72
87
|
|
73
|
-
|
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(
|
98
|
+
return if check.call(value)
|
82
99
|
|
83
100
|
message
|
84
101
|
rescue StandardError => e
|
85
|
-
"The \"#{
|
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
|
78
|
+
return unless value.nil?
|
79
79
|
|
80
80
|
allow_nil, message =
|
81
|
-
define_allow_nil_and_message_from(
|
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:
|
88
|
-
input_key:
|
89
|
-
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
|
109
|
+
if input_options.key?(:default) && input_options[:default].nil?
|
108
110
|
return true
|
109
111
|
end
|
110
112
|
|
111
|
-
|
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
|
71
|
-
return if
|
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 ===
|
75
|
+
return if types.any? { |type| type === given_type }
|
76
76
|
|
77
77
|
add_argument_error(
|
78
78
|
message,
|
79
|
-
origin:
|
80
|
-
input_key:
|
81
|
-
actor:
|
79
|
+
origin: origin,
|
80
|
+
input_key: input_key,
|
81
|
+
actor: actor,
|
82
82
|
expected_type: types.map(&:name).join(", "),
|
83
|
-
given_type:
|
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
|
-
|
91
|
-
|
92
|
+
definition = type_definition
|
93
|
+
|
94
|
+
if definition.is_a?(Hash)
|
95
|
+
definition[:message] ||= DEFAULT_MESSAGE
|
92
96
|
|
93
|
-
|
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(
|
102
|
+
types = Array(definition).map do |name|
|
100
103
|
name.is_a?(String) ? Object.const_get(name) : name
|
101
104
|
end
|
102
105
|
|
data/lib/service_actor/result.rb
CHANGED
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
|
+
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-
|
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: '
|
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: '
|
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.
|
298
|
+
version: '3.2'
|
271
299
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
272
300
|
requirements:
|
273
301
|
- - ">="
|