service_actor 3.3.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|