service_actor 1.1.0 → 2.0.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 +181 -96
- data/lib/service_actor.rb +3 -85
- data/lib/service_actor/argument_error.rb +6 -0
- data/lib/{actor → service_actor}/attributable.rb +14 -17
- data/lib/service_actor/base.rb +36 -0
- data/lib/service_actor/conditionable.rb +38 -0
- data/lib/service_actor/core.rb +80 -0
- data/lib/service_actor/defaultable.rb +36 -0
- data/lib/service_actor/error.rb +6 -0
- data/lib/service_actor/failure.rb +16 -0
- data/lib/service_actor/nil_checkable.rb +64 -0
- data/lib/{actor → service_actor}/playable.rb +8 -8
- data/lib/{actor/context.rb → service_actor/result.rb} +18 -13
- data/lib/{actor → service_actor}/success.rb +3 -2
- data/lib/service_actor/type_checkable.rb +52 -0
- data/lib/service_actor/version.rb +5 -0
- metadata +30 -13
- data/lib/actor/conditionable.rb +0 -34
- data/lib/actor/defaultable.rb +0 -30
- data/lib/actor/failure.rb +0 -16
- data/lib/actor/filtered_context.rb +0 -49
- data/lib/actor/requireable.rb +0 -36
- data/lib/actor/type_checkable.rb +0 -43
- data/lib/actor/version.rb +0 -5
data/lib/service_actor.rb
CHANGED
@@ -1,90 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'service_actor/base'
|
4
4
|
|
5
|
-
|
6
|
-
require 'actor/success'
|
7
|
-
require 'actor/context'
|
8
|
-
require 'actor/filtered_context'
|
9
|
-
|
10
|
-
require 'actor/playable'
|
11
|
-
require 'actor/attributable'
|
12
|
-
require 'actor/defaultable'
|
13
|
-
require 'actor/type_checkable'
|
14
|
-
require 'actor/requireable'
|
15
|
-
require 'actor/conditionable'
|
16
|
-
|
17
|
-
# Actors should start with a verb, inherit from Actor and implement a `call`
|
18
|
-
# method.
|
5
|
+
# Class to inherit from in your application.
|
19
6
|
class Actor
|
20
|
-
include
|
21
|
-
include Playable
|
22
|
-
prepend Defaultable
|
23
|
-
prepend TypeCheckable
|
24
|
-
prepend Requireable
|
25
|
-
prepend Conditionable
|
26
|
-
|
27
|
-
class << self
|
28
|
-
# Call an actor with a given context. Returns the context.
|
29
|
-
#
|
30
|
-
# CreateUser.call(name: 'Joe')
|
31
|
-
def call(context = {}, **arguments)
|
32
|
-
context = Actor::Context.to_context(context).merge!(arguments)
|
33
|
-
new(context).run
|
34
|
-
context
|
35
|
-
rescue Actor::Success
|
36
|
-
context
|
37
|
-
end
|
38
|
-
|
39
|
-
alias call! call
|
40
|
-
|
41
|
-
# Call an actor with a given context. Returns the context and does not raise
|
42
|
-
# on failure.
|
43
|
-
#
|
44
|
-
# CreateUser.result(name: 'Joe')
|
45
|
-
def result(context = {}, **arguments)
|
46
|
-
call(context, **arguments)
|
47
|
-
rescue Actor::Failure => e
|
48
|
-
e.context
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# :nodoc:
|
53
|
-
def initialize(context)
|
54
|
-
@context = context
|
55
|
-
end
|
56
|
-
|
57
|
-
# To implement in your actors.
|
58
|
-
def call; end
|
59
|
-
|
60
|
-
# To implement in your actors.
|
61
|
-
def rollback; end
|
62
|
-
|
63
|
-
# :nodoc:
|
64
|
-
def before; end
|
65
|
-
|
66
|
-
# :nodoc:
|
67
|
-
def after; end
|
68
|
-
|
69
|
-
# :nodoc:
|
70
|
-
def run
|
71
|
-
before
|
72
|
-
call
|
73
|
-
after
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
# Returns the current context from inside an actor.
|
79
|
-
attr_reader :context
|
80
|
-
|
81
|
-
# Can be called from inside an actor to stop execution and mark as failed.
|
82
|
-
def fail!(**arguments)
|
83
|
-
@context.fail!(**arguments)
|
84
|
-
end
|
85
|
-
|
86
|
-
# Can be called from inside an actor to stop execution early.
|
87
|
-
def succeed!(**arguments)
|
88
|
-
@context.succeed!(**arguments)
|
89
|
-
end
|
7
|
+
include ServiceActor::Base
|
90
8
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module ServiceActor
|
4
4
|
# DSL to document the accepted attributes.
|
5
5
|
#
|
6
6
|
# class CreateUser < Actor
|
@@ -10,7 +10,6 @@ class Actor
|
|
10
10
|
module Attributable
|
11
11
|
def self.included(base)
|
12
12
|
base.extend(ClassMethods)
|
13
|
-
base.prepend(PrependedMethods)
|
14
13
|
end
|
15
14
|
|
16
15
|
module ClassMethods
|
@@ -25,10 +24,10 @@ class Actor
|
|
25
24
|
inputs[name] = arguments
|
26
25
|
|
27
26
|
define_method(name) do
|
28
|
-
|
27
|
+
result[name]
|
29
28
|
end
|
30
29
|
|
31
|
-
|
30
|
+
protected name
|
32
31
|
end
|
33
32
|
|
34
33
|
def inputs
|
@@ -37,23 +36,21 @@ class Actor
|
|
37
36
|
|
38
37
|
def output(name, **arguments)
|
39
38
|
outputs[name] = arguments
|
40
|
-
end
|
41
39
|
|
42
|
-
|
43
|
-
|
40
|
+
define_method(name) do
|
41
|
+
result[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
define_method("#{name}=") do |value|
|
45
|
+
result[name] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
protected name, "#{name}="
|
44
49
|
end
|
45
|
-
end
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
def context
|
50
|
-
@filtered_context ||= Actor::FilteredContext.new(
|
51
|
-
super,
|
52
|
-
readers: self.class.inputs.keys,
|
53
|
-
setters: self.class.outputs.keys,
|
54
|
-
)
|
51
|
+
def outputs
|
52
|
+
@outputs ||= {}
|
55
53
|
end
|
56
|
-
# rubocop:enable Naming/MemoizedInstanceVariableName
|
57
54
|
end
|
58
55
|
end
|
59
56
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'service_actor/core'
|
4
|
+
|
5
|
+
# Exceptions
|
6
|
+
require 'service_actor/error'
|
7
|
+
require 'service_actor/failure'
|
8
|
+
require 'service_actor/success'
|
9
|
+
require 'service_actor/argument_error'
|
10
|
+
|
11
|
+
# Core
|
12
|
+
require 'service_actor/result'
|
13
|
+
require 'service_actor/attributable'
|
14
|
+
require 'service_actor/playable'
|
15
|
+
require 'service_actor/core'
|
16
|
+
|
17
|
+
# Concerns
|
18
|
+
require 'service_actor/type_checkable'
|
19
|
+
require 'service_actor/nil_checkable'
|
20
|
+
require 'service_actor/conditionable'
|
21
|
+
require 'service_actor/defaultable'
|
22
|
+
|
23
|
+
module ServiceActor
|
24
|
+
module Base
|
25
|
+
def self.included(base)
|
26
|
+
# Core
|
27
|
+
base.include(Core)
|
28
|
+
|
29
|
+
# Concerns
|
30
|
+
base.include(TypeCheckable)
|
31
|
+
base.include(NilCheckable)
|
32
|
+
base.include(Conditionable)
|
33
|
+
base.include(Defaultable)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Add checks to your inputs, by calling lambdas with the name of you choice.
|
5
|
+
# Will raise an error if any check does return a truthy value.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class Pay < Actor
|
10
|
+
# input :provider,
|
11
|
+
# must: {
|
12
|
+
# exist: ->(provider) { PROVIDERS.include?(provider) }
|
13
|
+
# }
|
14
|
+
# end
|
15
|
+
module Conditionable
|
16
|
+
def self.included(base)
|
17
|
+
base.prepend(PrependedMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
module PrependedMethods
|
21
|
+
def _call
|
22
|
+
self.class.inputs.each do |key, options|
|
23
|
+
next unless options[:must]
|
24
|
+
|
25
|
+
options[:must].each do |name, check|
|
26
|
+
value = result[key]
|
27
|
+
next if check.call(value)
|
28
|
+
|
29
|
+
raise ArgumentError,
|
30
|
+
"Input #{key} must #{name} but was #{value.inspect}."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Actors should start with a verb, inherit from Actor and implement a `call`
|
5
|
+
# method.
|
6
|
+
module Core
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.include(Attributable)
|
10
|
+
base.include(Playable)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# Call an actor with a given result. Returns the result.
|
15
|
+
#
|
16
|
+
# CreateUser.call(name: 'Joe')
|
17
|
+
def call(options = nil, **arguments)
|
18
|
+
result = Result.to_result(options).merge!(arguments)
|
19
|
+
new(result)._call
|
20
|
+
result
|
21
|
+
# DEPRECATED
|
22
|
+
rescue Success
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
# :nodoc:
|
27
|
+
def call!(**arguments)
|
28
|
+
warn "DEPRECATED: Prefer `#{name}.call` to `#{name}.call!`."
|
29
|
+
call(**arguments)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Call an actor with arguments. Returns the result and does not raise on
|
33
|
+
# failure.
|
34
|
+
#
|
35
|
+
# CreateUser.result(name: 'Joe')
|
36
|
+
def result(data = nil, **arguments)
|
37
|
+
call(data, **arguments)
|
38
|
+
rescue Failure => e
|
39
|
+
e.result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# :nodoc:
|
44
|
+
def initialize(result)
|
45
|
+
@result = result
|
46
|
+
end
|
47
|
+
|
48
|
+
# To implement in your actors.
|
49
|
+
def call; end
|
50
|
+
|
51
|
+
# To implement in your actors.
|
52
|
+
def rollback; end
|
53
|
+
|
54
|
+
# :nodoc:
|
55
|
+
def _call
|
56
|
+
call
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Returns the current context from inside an actor.
|
62
|
+
attr_reader :result
|
63
|
+
|
64
|
+
def context
|
65
|
+
warn "DEPRECATED: Prefer `result.` to `context.` in #{self.class.name}."
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
# Can be called from inside an actor to stop execution and mark as failed.
|
71
|
+
def fail!(**arguments)
|
72
|
+
result.fail!(**arguments)
|
73
|
+
end
|
74
|
+
|
75
|
+
# DEPRECATED
|
76
|
+
def succeed!(**arguments)
|
77
|
+
result.succeed!(**arguments)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Adds the `default:` option to inputs. Accepts regular values and lambdas.
|
5
|
+
# If no default is set and the value has not been given, raises an error.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class MultiplyThing < Actor
|
10
|
+
# input :counter, default: 1
|
11
|
+
# input :multiplier, default: -> { rand(1..10) }
|
12
|
+
# end
|
13
|
+
module Defaultable
|
14
|
+
def self.included(base)
|
15
|
+
base.prepend(PrependedMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module PrependedMethods
|
19
|
+
def _call
|
20
|
+
self.class.inputs.each do |name, input|
|
21
|
+
next if result.key?(name)
|
22
|
+
|
23
|
+
unless input.key?(:default)
|
24
|
+
raise ArgumentError, "Input #{name} on #{self.class} is missing."
|
25
|
+
end
|
26
|
+
|
27
|
+
default = input[:default]
|
28
|
+
default = default.call if default.respond_to?(:call)
|
29
|
+
result[name] = default
|
30
|
+
end
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Error raised when using `fail!` inside an actor.
|
5
|
+
class Failure < Error
|
6
|
+
def initialize(result)
|
7
|
+
@result = result
|
8
|
+
|
9
|
+
error = result.respond_to?(:error) ? result.error : nil
|
10
|
+
|
11
|
+
super(error)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :result
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Ensure your inputs and outputs are not nil by adding `allow_nil: false`.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# class CreateUser < Actor
|
9
|
+
# input :name, allow_nil: false
|
10
|
+
# output :user, allow_nil: false
|
11
|
+
# end
|
12
|
+
module NilCheckable
|
13
|
+
def self.included(base)
|
14
|
+
base.prepend(PrependedMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module PrependedMethods
|
18
|
+
def _call
|
19
|
+
check_context_for_nil(self.class.inputs, origin: 'input')
|
20
|
+
|
21
|
+
super
|
22
|
+
|
23
|
+
check_context_for_nil(self.class.outputs, origin: 'output')
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def check_context_for_nil(definitions, origin:)
|
29
|
+
definitions.each do |name, options|
|
30
|
+
warn_of_deprecated_required_option(options, name, origin)
|
31
|
+
|
32
|
+
next if !result[name].nil? || allow_nil?(options)
|
33
|
+
|
34
|
+
raise ArgumentError,
|
35
|
+
"The #{origin} \"#{name}\" on #{self.class} does not allow " \
|
36
|
+
'nil values.'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def warn_of_deprecated_required_option(options, name, origin)
|
41
|
+
return unless options.key?(:required)
|
42
|
+
|
43
|
+
warn 'DEPRECATED: The "required" option is deprecated. Replace ' \
|
44
|
+
"`#{origin} :#{name}, required: #{options[:required]}` by " \
|
45
|
+
"`#{origin} :#{name}, allow_nil: #{!options[:required]}` in " \
|
46
|
+
"#{self.class}."
|
47
|
+
end
|
48
|
+
|
49
|
+
def allow_nil?(options)
|
50
|
+
if options.key?(:allow_nil)
|
51
|
+
options[:allow_nil]
|
52
|
+
elsif options.key?(:required)
|
53
|
+
!options[:required]
|
54
|
+
elsif options.key?(:default) && options[:default].nil?
|
55
|
+
true
|
56
|
+
elsif options[:type]
|
57
|
+
false
|
58
|
+
else
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|