service_actor 1.0.0 → 3.1.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 +234 -91
- data/lib/service_actor.rb +3 -83
- data/lib/service_actor/argument_error.rb +6 -0
- data/lib/{actor → service_actor}/attributable.rb +14 -17
- data/lib/service_actor/base.rb +41 -0
- data/lib/service_actor/collectionable.rb +32 -0
- data/lib/service_actor/conditionable.rb +40 -0
- data/lib/service_actor/core.rb +56 -0
- data/lib/service_actor/defaultable.rb +37 -0
- data/lib/service_actor/error.rb +6 -0
- data/lib/service_actor/failable.rb +40 -0
- data/lib/service_actor/failure.rb +16 -0
- data/lib/service_actor/nil_checkable.rb +51 -0
- data/lib/{actor → service_actor}/playable.rb +10 -9
- data/lib/service_actor/result.rb +55 -0
- data/lib/service_actor/type_checkable.rb +51 -0
- data/lib/service_actor/version.rb +5 -0
- metadata +76 -16
- data/lib/actor/conditionable.rb +0 -34
- data/lib/actor/context.rb +0 -92
- data/lib/actor/defaultable.rb +0 -25
- 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/success.rb +0 -6
- data/lib/actor/type_checkable.rb +0 -43
- data/lib/actor/version.rb +0 -5
data/lib/service_actor.rb
CHANGED
@@ -1,88 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'actor/success'
|
5
|
-
require 'actor/context'
|
6
|
-
require 'actor/filtered_context'
|
3
|
+
require 'service_actor/base'
|
7
4
|
|
8
|
-
|
9
|
-
require 'actor/attributable'
|
10
|
-
require 'actor/defaultable'
|
11
|
-
require 'actor/type_checkable'
|
12
|
-
require 'actor/requireable'
|
13
|
-
require 'actor/conditionable'
|
14
|
-
|
15
|
-
# Actors should start with a verb, inherit from Actor and implement a `call`
|
16
|
-
# method.
|
5
|
+
# Class to inherit from in your application.
|
17
6
|
class Actor
|
18
|
-
include
|
19
|
-
include Playable
|
20
|
-
prepend Defaultable
|
21
|
-
prepend TypeCheckable
|
22
|
-
prepend Requireable
|
23
|
-
prepend Conditionable
|
24
|
-
|
25
|
-
class << self
|
26
|
-
# Call an actor with a given context. Returns the context.
|
27
|
-
#
|
28
|
-
# CreateUser.call(name: 'Joe')
|
29
|
-
def call(context = {}, **arguments)
|
30
|
-
context = Actor::Context.to_context(context).merge!(arguments)
|
31
|
-
new(context).run
|
32
|
-
context
|
33
|
-
rescue Actor::Success
|
34
|
-
context
|
35
|
-
end
|
36
|
-
|
37
|
-
alias call! call
|
38
|
-
|
39
|
-
# Call an actor with a given context. Returns the context and does not raise
|
40
|
-
# on failure.
|
41
|
-
#
|
42
|
-
# CreateUser.result(name: 'Joe')
|
43
|
-
def result(context = {}, **arguments)
|
44
|
-
call(context, **arguments)
|
45
|
-
rescue Actor::Failure => e
|
46
|
-
e.context
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# :nodoc:
|
51
|
-
def initialize(context)
|
52
|
-
@context = context
|
53
|
-
end
|
54
|
-
|
55
|
-
# To implement in your actors.
|
56
|
-
def call; end
|
57
|
-
|
58
|
-
# To implement in your actors.
|
59
|
-
def rollback; end
|
60
|
-
|
61
|
-
# :nodoc:
|
62
|
-
def before; end
|
63
|
-
|
64
|
-
# :nodoc:
|
65
|
-
def after; end
|
66
|
-
|
67
|
-
# :nodoc:
|
68
|
-
def run
|
69
|
-
before
|
70
|
-
call
|
71
|
-
after
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
# Returns the current context from inside an actor.
|
77
|
-
attr_reader :context
|
78
|
-
|
79
|
-
# Can be called from inside an actor to stop execution and mark as failed.
|
80
|
-
def fail!(**arguments)
|
81
|
-
@context.fail!(**arguments)
|
82
|
-
end
|
83
|
-
|
84
|
-
# Can be called from inside an actor to stop execution early.
|
85
|
-
def succeed!(**arguments)
|
86
|
-
@context.succeed!(**arguments)
|
87
|
-
end
|
7
|
+
include ServiceActor::Base
|
88
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,41 @@
|
|
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/argument_error'
|
9
|
+
|
10
|
+
# Core
|
11
|
+
require 'service_actor/core'
|
12
|
+
require 'service_actor/attributable'
|
13
|
+
require 'service_actor/playable'
|
14
|
+
require 'service_actor/result'
|
15
|
+
|
16
|
+
# Concerns
|
17
|
+
require 'service_actor/type_checkable'
|
18
|
+
require 'service_actor/nil_checkable'
|
19
|
+
require 'service_actor/conditionable'
|
20
|
+
require 'service_actor/defaultable'
|
21
|
+
require 'service_actor/collectionable'
|
22
|
+
require 'service_actor/failable'
|
23
|
+
|
24
|
+
module ServiceActor
|
25
|
+
module Base
|
26
|
+
def self.included(base)
|
27
|
+
# Core
|
28
|
+
base.include(Core)
|
29
|
+
base.include(Attributable)
|
30
|
+
base.include(Playable)
|
31
|
+
|
32
|
+
# Concerns
|
33
|
+
base.include(TypeCheckable)
|
34
|
+
base.include(NilCheckable)
|
35
|
+
base.include(Conditionable)
|
36
|
+
base.include(Defaultable)
|
37
|
+
base.include(Collectionable)
|
38
|
+
base.include(Failable)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Add checks to your inputs, by specifying what values are authorized
|
5
|
+
# under the "in" key.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class Pay < Actor
|
10
|
+
# input :provider, in: ['MANGOPAY', 'PayPal', 'Stripe']
|
11
|
+
# end
|
12
|
+
module Collectionable
|
13
|
+
def self.included(base)
|
14
|
+
base.prepend(PrependedMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module PrependedMethods
|
18
|
+
def _call
|
19
|
+
self.class.inputs.each do |key, options|
|
20
|
+
next unless options[:in]
|
21
|
+
|
22
|
+
next if options[:in].include?(result[key])
|
23
|
+
|
24
|
+
raise ArgumentError,
|
25
|
+
"Input #{key} must be included in #{options[:in].inspect}"
|
26
|
+
end
|
27
|
+
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
+
# under the "must" key.
|
6
|
+
#
|
7
|
+
# Will raise an error if any check returns a truthy value.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# class Pay < Actor
|
12
|
+
# input :provider,
|
13
|
+
# must: {
|
14
|
+
# exist: ->(provider) { PROVIDERS.include?(provider) }
|
15
|
+
# }
|
16
|
+
# end
|
17
|
+
module Conditionable
|
18
|
+
def self.included(base)
|
19
|
+
base.prepend(PrependedMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
module PrependedMethods
|
23
|
+
def _call
|
24
|
+
self.class.inputs.each do |key, options|
|
25
|
+
next unless options[:must]
|
26
|
+
|
27
|
+
options[:must].each do |name, check|
|
28
|
+
value = result[key]
|
29
|
+
next if check.call(value)
|
30
|
+
|
31
|
+
raise ArgumentError,
|
32
|
+
"Input #{key} must #{name} but was #{value.inspect}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
module Core
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Call an actor with a given result. Returns the result.
|
11
|
+
#
|
12
|
+
# CreateUser.call(name: 'Joe')
|
13
|
+
def call(options = nil, **arguments)
|
14
|
+
result = Result.to_result(options).merge!(arguments)
|
15
|
+
new(result)._call
|
16
|
+
result
|
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(data = nil, **arguments)
|
24
|
+
call(data, **arguments)
|
25
|
+
rescue Failure => e
|
26
|
+
e.result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# :nodoc:
|
31
|
+
def initialize(result)
|
32
|
+
@result = result
|
33
|
+
end
|
34
|
+
|
35
|
+
# To implement in your actors.
|
36
|
+
def call; end
|
37
|
+
|
38
|
+
# To implement in your actors.
|
39
|
+
def rollback; end
|
40
|
+
|
41
|
+
# :nodoc:
|
42
|
+
def _call
|
43
|
+
call
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Returns the current context from inside an actor.
|
49
|
+
attr_reader :result
|
50
|
+
|
51
|
+
# Can be called from inside an actor to stop execution and mark as failed.
|
52
|
+
def fail!(**arguments)
|
53
|
+
result.fail!(**arguments)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
if input.key?(:default)
|
24
|
+
default = input[:default]
|
25
|
+
default = default.call if default.respond_to?(:call)
|
26
|
+
result[name] = default
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
raise(ArgumentError, "Input #{name} on #{self.class} is missing")
|
31
|
+
end
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Adds the `fail_on` DSL to actors. This allows you to call `.result` and get
|
5
|
+
# back a failed actor instead of raising an exception.
|
6
|
+
#
|
7
|
+
# class ApplicationActor < Actor
|
8
|
+
# fail_on ServiceActor::ArgumentError
|
9
|
+
# end
|
10
|
+
module Failable
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.prepend(PrependedMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def inherited(child)
|
18
|
+
super
|
19
|
+
|
20
|
+
child.fail_ons.push(*fail_ons)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fail_on(*exceptions)
|
24
|
+
fail_ons.push(*exceptions)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fail_ons
|
28
|
+
@fail_ons ||= []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module PrependedMethods
|
33
|
+
def _call
|
34
|
+
super
|
35
|
+
rescue *self.class.fail_ons => e
|
36
|
+
fail!(error: e.message)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
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,51 @@
|
|
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
|
+
next if !result[name].nil? || allow_nil?(options)
|
31
|
+
|
32
|
+
raise ArgumentError,
|
33
|
+
"The #{origin} \"#{name}\" on #{self.class} does not allow " \
|
34
|
+
'nil values'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def allow_nil?(options)
|
39
|
+
if options.key?(:allow_nil)
|
40
|
+
options[:allow_nil]
|
41
|
+
elsif options.key?(:default) && options[:default].nil?
|
42
|
+
true
|
43
|
+
elsif options[:type]
|
44
|
+
false
|
45
|
+
else
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|