service_actor 3.3.0 → 3.4.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 +4 -4
- data/README.md +145 -105
- data/lib/service_actor/argument_error.rb +2 -4
- data/lib/service_actor/attributable.rb +39 -41
- data/lib/service_actor/base.rb +15 -33
- data/lib/service_actor/collectionable.rb +58 -22
- data/lib/service_actor/conditionable.rb +63 -28
- data/lib/service_actor/core.rb +52 -45
- data/lib/service_actor/defaultable.rb +65 -26
- data/lib/service_actor/error.rb +2 -4
- data/lib/service_actor/failable.rb +27 -29
- data/lib/service_actor/failure.rb +8 -10
- data/lib/service_actor/nil_checkable.rb +73 -31
- data/lib/service_actor/playable.rb +62 -64
- data/lib/service_actor/raisable.rb +20 -0
- data/lib/service_actor/result.rb +50 -52
- data/lib/service_actor/support/loader.rb +16 -0
- data/lib/service_actor/type_checkable.rb +75 -35
- data/lib/service_actor/version.rb +1 -1
- metadata +32 -2
@@ -1,39 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
3
|
+
# Add checks to your inputs, by calling lambdas with the name of you choice
|
4
|
+
# under the "must" key.
|
5
|
+
#
|
6
|
+
# Will raise an error if any check returns a truthy value.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# class Pay < Actor
|
11
|
+
# input :provider,
|
12
|
+
# must: {
|
13
|
+
# exist: -> provider { PROVIDERS.include?(provider) },
|
14
|
+
# }
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# class Pay < Actor
|
18
|
+
# input :provider,
|
19
|
+
# must: {
|
20
|
+
# exist: {
|
21
|
+
# is: -> provider { PROVIDERS.include?(provider) },
|
22
|
+
# message: (lambda do |input_key:, check_name:, actor:, value:|
|
23
|
+
# "The specified provider \"#{value}\" was not found."
|
24
|
+
# end)
|
25
|
+
# }
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
module ServiceActor::Conditionable
|
29
|
+
def self.included(base)
|
30
|
+
base.prepend(PrependedMethods)
|
31
|
+
end
|
32
|
+
|
33
|
+
module PrependedMethods
|
34
|
+
DEFAULT_MESSAGE = lambda do |input_key:, check_name:, actor:, value:|
|
35
|
+
"The \"#{input_key}\" input on \"#{actor}\" must \"#{check_name}\" " \
|
36
|
+
"but was #{value.inspect}"
|
20
37
|
end
|
21
38
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
39
|
+
private_constant :DEFAULT_MESSAGE
|
40
|
+
|
41
|
+
def _call # rubocop:disable Metrics/MethodLength
|
42
|
+
self.class.inputs.each do |key, options|
|
43
|
+
next unless options[:must]
|
44
|
+
|
45
|
+
options[:must].each do |check_name, check|
|
46
|
+
value = result[key]
|
47
|
+
|
48
|
+
check, message = define_check_from(check)
|
26
49
|
|
27
|
-
|
28
|
-
value = result[key]
|
29
|
-
next if check.call(value)
|
50
|
+
next if check.call(value)
|
30
51
|
|
31
|
-
|
32
|
-
|
33
|
-
|
52
|
+
raise_error_with(
|
53
|
+
message,
|
54
|
+
input_key: key,
|
55
|
+
check_name: check_name,
|
56
|
+
actor: self.class,
|
57
|
+
value: value,
|
58
|
+
)
|
34
59
|
end
|
60
|
+
end
|
61
|
+
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
35
66
|
|
36
|
-
|
67
|
+
def define_check_from(check)
|
68
|
+
if check.is_a?(Hash)
|
69
|
+
check.values_at(:is, :message)
|
70
|
+
else
|
71
|
+
[check, DEFAULT_MESSAGE]
|
37
72
|
end
|
38
73
|
end
|
39
74
|
end
|
data/lib/service_actor/core.rb
CHANGED
@@ -1,59 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module ServiceActor
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
module ServiceActor::Core
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
# Call an actor with arguments. Returns the result and does not raise on
|
20
|
-
# failure.
|
21
|
-
#
|
22
|
-
# CreateUser.result(name: "Joe")
|
23
|
-
def result(result = nil, **arguments)
|
24
|
-
call(result, **arguments)
|
25
|
-
rescue Failure => e
|
26
|
-
e.result
|
27
|
-
end
|
8
|
+
module ClassMethods
|
9
|
+
# Call an actor with a given result. Returns the result.
|
10
|
+
#
|
11
|
+
# CreateUser.call(name: "Joe")
|
12
|
+
def call(result = nil, **arguments)
|
13
|
+
result = ServiceActor::Result.to_result(result).merge!(arguments)
|
14
|
+
new(result)._call
|
15
|
+
result
|
28
16
|
end
|
29
17
|
|
30
|
-
#
|
31
|
-
|
32
|
-
|
18
|
+
# Call an actor with arguments. Returns the result and does not raise on
|
19
|
+
# failure.
|
20
|
+
#
|
21
|
+
# CreateUser.result(name: "Joe")
|
22
|
+
def result(result = nil, **arguments)
|
23
|
+
call(result, **arguments)
|
24
|
+
rescue failure_class => e
|
25
|
+
e.result
|
33
26
|
end
|
27
|
+
end
|
34
28
|
|
35
|
-
|
36
|
-
|
29
|
+
# :nodoc:
|
30
|
+
def initialize(result)
|
31
|
+
@result = result
|
32
|
+
end
|
37
33
|
|
38
|
-
|
39
|
-
|
34
|
+
# To implement in your actors.
|
35
|
+
def call; end
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
37
|
+
# To implement in your actors.
|
38
|
+
def rollback; end
|
39
|
+
|
40
|
+
# This method is used internally to override behavior on call. Overriding
|
41
|
+
# `call` instead would mean that end-users have to call `super` in their
|
42
|
+
# actors.
|
43
|
+
# :nodoc:
|
44
|
+
def _call
|
45
|
+
call
|
46
|
+
end
|
48
47
|
|
49
|
-
|
48
|
+
protected
|
50
49
|
|
51
|
-
|
52
|
-
|
50
|
+
# Returns the current context from inside an actor.
|
51
|
+
attr_reader :result
|
53
52
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
# Can be called from inside an actor to stop execution and mark as failed.
|
54
|
+
def fail!(**arguments)
|
55
|
+
result.fail!(self.class.failure_class, **arguments)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Raises an error depending on the mode
|
61
|
+
def raise_error_with(message, **arguments)
|
62
|
+
message = message.call(**arguments) if message.is_a?(Proc)
|
63
|
+
|
64
|
+
raise self.class.argument_error_class, message
|
58
65
|
end
|
59
66
|
end
|
@@ -1,37 +1,76 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
# Adds the `default:` option to inputs. Accepts regular values and lambdas.
|
4
|
+
# If no default is set and the value has not been given, raises an error.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# class MultiplyThing < Actor
|
9
|
+
# input :counter, default: 1
|
10
|
+
# input :multiplier, default: -> { rand(1..10) }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class MultiplyThing < Actor
|
14
|
+
# input :counter,
|
15
|
+
# default: {
|
16
|
+
# is: 1,
|
17
|
+
# message: "Counter is required"
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
# input :multiplier,
|
21
|
+
# default: {
|
22
|
+
# is: -> { rand(1..10) },
|
23
|
+
# message: (lambda do |input_key:, actor:|
|
24
|
+
# "Input \"#{input_key}\" is required"
|
25
|
+
# end)
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
module ServiceActor::Defaultable
|
29
|
+
def self.included(base)
|
30
|
+
base.prepend(PrependedMethods)
|
31
|
+
end
|
17
32
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
33
|
+
module PrependedMethods
|
34
|
+
def _call # rubocop:disable Metrics/MethodLength
|
35
|
+
self.class.inputs.each do |key, input|
|
36
|
+
next if result.key?(key)
|
37
|
+
|
38
|
+
unless input.key?(:default)
|
39
|
+
raise_error_with(
|
40
|
+
"The \"#{key}\" input on \"#{self.class}\" is missing",
|
41
|
+
)
|
42
|
+
end
|
22
43
|
|
23
|
-
|
24
|
-
default = input[:default]
|
25
|
-
default = default.call if default.is_a?(Proc)
|
26
|
-
result[name] = default
|
27
|
-
next
|
28
|
-
end
|
44
|
+
default = input[:default]
|
29
45
|
|
30
|
-
|
46
|
+
if default.is_a?(Hash)
|
47
|
+
default_for_advanced_mode_with(result, key, default)
|
48
|
+
else
|
49
|
+
default_for_normal_mode_with(result, key, default)
|
31
50
|
end
|
51
|
+
end
|
52
|
+
|
53
|
+
super
|
54
|
+
end
|
32
55
|
|
33
|
-
|
56
|
+
private
|
57
|
+
|
58
|
+
def default_for_normal_mode_with(result, key, default)
|
59
|
+
default = default.call if default.is_a?(Proc)
|
60
|
+
result[key] = default
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_for_advanced_mode_with(result, key, content)
|
64
|
+
default, message = content.values_at(:is, :message)
|
65
|
+
|
66
|
+
unless default
|
67
|
+
raise_error_with(message, input_key: key, actor: self.class)
|
34
68
|
end
|
69
|
+
|
70
|
+
default = default.call if default.is_a?(Proc)
|
71
|
+
result[key] = default
|
72
|
+
|
73
|
+
message.call(key, self.class)
|
35
74
|
end
|
36
75
|
end
|
37
76
|
end
|
data/lib/service_actor/error.rb
CHANGED
@@ -1,40 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
3
|
+
# Adds the `fail_on` DSL to actors. This allows you to call `.result` and get
|
4
|
+
# back a failed actor instead of raising an exception.
|
5
|
+
#
|
6
|
+
# class ApplicationActor < Actor
|
7
|
+
# fail_on ServiceActor::ArgumentError
|
8
|
+
# end
|
9
|
+
module ServiceActor::Failable
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
base.prepend(PrependedMethods)
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
module ClassMethods
|
16
|
+
def inherited(child)
|
17
|
+
super
|
19
18
|
|
20
|
-
|
21
|
-
|
19
|
+
child.fail_ons.push(*fail_ons)
|
20
|
+
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def fail_on(*exceptions)
|
23
|
+
fail_ons.push(*exceptions)
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
end
|
26
|
+
def fail_ons
|
27
|
+
@fail_ons ||= []
|
30
28
|
end
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
31
|
+
module PrependedMethods
|
32
|
+
def _call
|
33
|
+
super
|
34
|
+
rescue *self.class.fail_ons => e
|
35
|
+
fail!(error: e.message)
|
38
36
|
end
|
39
37
|
end
|
40
38
|
end
|
@@ -1,16 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@result = result
|
3
|
+
# Error raised when using `fail!` inside an actor.
|
4
|
+
class ServiceActor::Failure < ServiceActor::Error
|
5
|
+
def initialize(result)
|
6
|
+
@result = result
|
8
7
|
|
9
|
-
|
8
|
+
error = result.respond_to?(:error) ? result.error : nil
|
10
9
|
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
attr_reader :result
|
10
|
+
super(error)
|
15
11
|
end
|
12
|
+
|
13
|
+
attr_reader :result
|
16
14
|
end
|
@@ -1,46 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
# Ensure your inputs and outputs are not nil by adding `allow_nil: false`.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# class CreateUser < Actor
|
8
|
+
# input :name, allow_nil: false
|
9
|
+
# output :user, allow_nil: false
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# class CreateUser < Actor
|
13
|
+
# input :name,
|
14
|
+
# allow_nil: {
|
15
|
+
# is: false,
|
16
|
+
# message: (lambda do |origin:, input_key:, actor:|
|
17
|
+
# "The value \"#{input_key}\" cannot be empty"
|
18
|
+
# end)
|
19
|
+
# }
|
20
|
+
#
|
21
|
+
# input :phone, allow_nil: { is: false, message: "Phone must be present" }
|
22
|
+
#
|
23
|
+
# output :user,
|
24
|
+
# allow_nil: {
|
25
|
+
# is: false,
|
26
|
+
# message: (lambda do |origin:, input_key:, actor:|
|
27
|
+
# "The value \"#{input_key}\" cannot be empty"
|
28
|
+
# end)
|
29
|
+
# }
|
30
|
+
# end
|
31
|
+
module ServiceActor::NilCheckable
|
32
|
+
def self.included(base)
|
33
|
+
base.prepend(PrependedMethods)
|
34
|
+
end
|
35
|
+
|
36
|
+
module PrependedMethods
|
37
|
+
DEFAULT_MESSAGE = lambda do |origin:, input_key:, actor:|
|
38
|
+
"The \"#{input_key}\" #{origin} on \"#{actor}\" does not allow " \
|
39
|
+
"nil values"
|
15
40
|
end
|
16
41
|
|
17
|
-
|
18
|
-
def _call
|
19
|
-
check_context_for_nil(self.class.inputs, origin: "input")
|
42
|
+
private_constant :DEFAULT_MESSAGE
|
20
43
|
|
21
|
-
|
44
|
+
def _call
|
45
|
+
check_context_for_nil(self.class.inputs, origin: "input")
|
22
46
|
|
23
|
-
|
24
|
-
end
|
47
|
+
super
|
25
48
|
|
26
|
-
|
49
|
+
check_context_for_nil(self.class.outputs, origin: "output")
|
50
|
+
end
|
27
51
|
|
28
|
-
|
29
|
-
definitions.each do |name, options|
|
30
|
-
next if !result[name].nil? || allow_nil?(options)
|
52
|
+
private
|
31
53
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
54
|
+
def check_context_for_nil(definitions, origin:)
|
55
|
+
definitions.each do |key, options|
|
56
|
+
value = result[key]
|
57
|
+
|
58
|
+
next unless value.nil?
|
59
|
+
|
60
|
+
allow_nil, message = define_allow_nil_from(options[:allow_nil])
|
37
61
|
|
38
|
-
|
39
|
-
return options[:allow_nil] if options.key?(:allow_nil)
|
40
|
-
return true if options.key?(:default) && options[:default].nil?
|
62
|
+
next if allow_nil?(allow_nil, options)
|
41
63
|
|
42
|
-
|
64
|
+
raise_error_with(
|
65
|
+
message,
|
66
|
+
origin: origin,
|
67
|
+
input_key: key,
|
68
|
+
actor: self.class,
|
69
|
+
)
|
43
70
|
end
|
44
71
|
end
|
72
|
+
|
73
|
+
def define_allow_nil_from(allow_nil)
|
74
|
+
if allow_nil.is_a?(Hash)
|
75
|
+
allow_nil.values_at(:is, :message)
|
76
|
+
else
|
77
|
+
[allow_nil, DEFAULT_MESSAGE]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def allow_nil?(allow_nil, options)
|
82
|
+
return allow_nil unless allow_nil.nil?
|
83
|
+
return true if options.key?(:default) && options[:default].nil?
|
84
|
+
|
85
|
+
!options[:type]
|
86
|
+
end
|
45
87
|
end
|
46
88
|
end
|