service_actor 1.1.0 → 2.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 +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
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
# rollback on any actor that succeeded.
|
3
|
+
module ServiceActor
|
4
|
+
# Play class method to call a series of actors with the same result. On
|
5
|
+
# failure, calls rollback on any actor that succeeded.
|
6
6
|
#
|
7
7
|
# class CreateUser < Actor
|
8
8
|
# play SaveUser,
|
@@ -38,11 +38,11 @@ class Actor
|
|
38
38
|
module PrependedMethods
|
39
39
|
def call
|
40
40
|
self.class.play_actors.each do |options|
|
41
|
-
next if options[:if] && !options[:if].call(
|
41
|
+
next if options[:if] && !options[:if].call(result)
|
42
42
|
|
43
43
|
play_actor(options[:actor])
|
44
44
|
end
|
45
|
-
rescue
|
45
|
+
rescue Failure
|
46
46
|
rollback
|
47
47
|
raise
|
48
48
|
end
|
@@ -61,10 +61,10 @@ class Actor
|
|
61
61
|
|
62
62
|
def play_actor(actor)
|
63
63
|
if actor.is_a?(Class) && actor.ancestors.include?(Actor)
|
64
|
-
actor = actor.new(
|
65
|
-
actor.
|
64
|
+
actor = actor.new(result)
|
65
|
+
actor._call
|
66
66
|
else
|
67
|
-
actor.call(
|
67
|
+
actor.call(result)
|
68
68
|
end
|
69
69
|
|
70
70
|
(@played ||= []).unshift(actor)
|
@@ -1,30 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module ServiceActor
|
6
|
+
# Represents the result of an actor.
|
7
|
+
class Result < OpenStruct
|
8
|
+
def self.to_result(data)
|
7
9
|
return data if data.is_a?(self)
|
8
10
|
|
9
11
|
new(data.to_h)
|
10
12
|
end
|
11
13
|
|
12
14
|
def inspect
|
13
|
-
"
|
15
|
+
"<#{self.class.name} #{to_h}>"
|
14
16
|
end
|
15
17
|
|
16
|
-
def fail!(
|
17
|
-
merge!(
|
18
|
+
def fail!(result = {})
|
19
|
+
merge!(result)
|
18
20
|
merge!(failure?: true)
|
19
21
|
|
20
|
-
raise
|
22
|
+
raise Failure, self
|
21
23
|
end
|
22
24
|
|
23
|
-
def succeed!(
|
24
|
-
|
25
|
+
def succeed!(result = {})
|
26
|
+
warn 'DEPRECATED: Early success with `succeed!` is deprecated in favor ' \
|
27
|
+
'of adding conditions to `play` calls.'
|
28
|
+
|
29
|
+
merge!(result)
|
25
30
|
merge!(failure?: false)
|
26
31
|
|
27
|
-
raise
|
32
|
+
raise Success, self
|
28
33
|
end
|
29
34
|
|
30
35
|
def success?
|
@@ -35,8 +40,8 @@ class Actor
|
|
35
40
|
super || false
|
36
41
|
end
|
37
42
|
|
38
|
-
def merge!(
|
39
|
-
|
43
|
+
def merge!(result)
|
44
|
+
result.each_pair do |key, value|
|
40
45
|
self[key] = value
|
41
46
|
end
|
42
47
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module ServiceActor
|
4
4
|
# Raised when using `succeed!` to halt the progression of an organizer.
|
5
|
-
|
5
|
+
# DEPRECATED in favor of adding conditions to your play.
|
6
|
+
class Success < Error; end
|
6
7
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceActor
|
4
|
+
# Adds `type:` checking to inputs and outputs. Accepts classes or class names
|
5
|
+
# that should match an ancestor. Also accepts arrays.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class ReduceOrderAmount < Actor
|
10
|
+
# input :order, type: Order
|
11
|
+
# input :coupon, type: 'Coupon'
|
12
|
+
# input :amount, type: [Integer, Float]
|
13
|
+
# input :bonus_applied, type: [TrueClass FalseClass]
|
14
|
+
# end
|
15
|
+
module TypeCheckable
|
16
|
+
def self.included(base)
|
17
|
+
base.prepend(PrependedMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
module PrependedMethods
|
21
|
+
def _call
|
22
|
+
check_type_definitions(self.class.inputs, kind: 'Input')
|
23
|
+
|
24
|
+
super
|
25
|
+
|
26
|
+
check_type_definitions(self.class.outputs, kind: 'Output')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_type_definitions(definitions, kind:)
|
32
|
+
definitions.each do |key, options|
|
33
|
+
type_definition = options[:type] || next
|
34
|
+
value = result[key] || next
|
35
|
+
|
36
|
+
types = types_for_definition(type_definition)
|
37
|
+
next if types.any? { |type| value.is_a?(type) }
|
38
|
+
|
39
|
+
raise ArgumentError,
|
40
|
+
"#{kind} #{key} on #{self.class} must be of type " \
|
41
|
+
"#{types.join(', ')} but was #{value.class}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def types_for_definition(type_definition)
|
46
|
+
Array(type_definition).map do |name|
|
47
|
+
name.is_a?(String) ? Object.const_get(name) : name
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: service_actor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sunny Ripert
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-03-
|
11
|
+
date: 2020-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description: Service objects for your application logic
|
70
84
|
email:
|
71
85
|
- sunny@sunfox.org
|
@@ -77,18 +91,21 @@ extra_rdoc_files:
|
|
77
91
|
files:
|
78
92
|
- LICENSE.txt
|
79
93
|
- README.md
|
80
|
-
- lib/actor/attributable.rb
|
81
|
-
- lib/actor/conditionable.rb
|
82
|
-
- lib/actor/context.rb
|
83
|
-
- lib/actor/defaultable.rb
|
84
|
-
- lib/actor/failure.rb
|
85
|
-
- lib/actor/filtered_context.rb
|
86
|
-
- lib/actor/playable.rb
|
87
|
-
- lib/actor/requireable.rb
|
88
|
-
- lib/actor/success.rb
|
89
|
-
- lib/actor/type_checkable.rb
|
90
|
-
- lib/actor/version.rb
|
91
94
|
- lib/service_actor.rb
|
95
|
+
- lib/service_actor/argument_error.rb
|
96
|
+
- lib/service_actor/attributable.rb
|
97
|
+
- lib/service_actor/base.rb
|
98
|
+
- lib/service_actor/conditionable.rb
|
99
|
+
- lib/service_actor/core.rb
|
100
|
+
- lib/service_actor/defaultable.rb
|
101
|
+
- lib/service_actor/error.rb
|
102
|
+
- lib/service_actor/failure.rb
|
103
|
+
- lib/service_actor/nil_checkable.rb
|
104
|
+
- lib/service_actor/playable.rb
|
105
|
+
- lib/service_actor/result.rb
|
106
|
+
- lib/service_actor/success.rb
|
107
|
+
- lib/service_actor/type_checkable.rb
|
108
|
+
- lib/service_actor/version.rb
|
92
109
|
homepage: https://github.com/sunny/actor
|
93
110
|
licenses:
|
94
111
|
- MIT
|
data/lib/actor/conditionable.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
4
|
-
# Add boolean checks to inputs, by calling lambdas starting with `must*`.
|
5
|
-
#
|
6
|
-
# Example:
|
7
|
-
#
|
8
|
-
# class Pay < Actor
|
9
|
-
# input :provider,
|
10
|
-
# must: {
|
11
|
-
# exist: ->(provider) { PROVIDERS.include?(provider) }
|
12
|
-
# }
|
13
|
-
#
|
14
|
-
# output :user, required: true
|
15
|
-
# end
|
16
|
-
module Conditionable
|
17
|
-
def before
|
18
|
-
super
|
19
|
-
|
20
|
-
self.class.inputs.each do |key, options|
|
21
|
-
next unless options[:must]
|
22
|
-
|
23
|
-
options[:must].each do |name, check|
|
24
|
-
value = @context[key]
|
25
|
-
next if check.call(value)
|
26
|
-
|
27
|
-
name = name.to_s.sub(/^must_/, '')
|
28
|
-
raise ArgumentError,
|
29
|
-
"Input #{key} must #{name} but was #{value.inspect}."
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/actor/defaultable.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
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 before
|
15
|
-
self.class.inputs.each do |name, input|
|
16
|
-
next if @context.key?(name)
|
17
|
-
|
18
|
-
unless input.key?(:default)
|
19
|
-
raise ArgumentError, "Input #{name} on #{self.class} is missing."
|
20
|
-
end
|
21
|
-
|
22
|
-
default = input[:default]
|
23
|
-
default = default.call if default.respond_to?(:call)
|
24
|
-
@context.merge!(name => default)
|
25
|
-
end
|
26
|
-
|
27
|
-
super
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/actor/failure.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
4
|
-
# Error raised when using `fail!` inside an actor.
|
5
|
-
class Failure < StandardError
|
6
|
-
def initialize(context)
|
7
|
-
@context = context
|
8
|
-
|
9
|
-
error = context.respond_to?(:error) ? context.error : nil
|
10
|
-
|
11
|
-
super(error)
|
12
|
-
end
|
13
|
-
|
14
|
-
attr_reader :context
|
15
|
-
end
|
16
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
4
|
-
# Represents the result of an action, tied to inputs and outputs.
|
5
|
-
class FilteredContext
|
6
|
-
def initialize(context, readers:, setters:)
|
7
|
-
@context = context
|
8
|
-
@readers = readers
|
9
|
-
@setters = setters
|
10
|
-
end
|
11
|
-
|
12
|
-
def inspect
|
13
|
-
"<#{self.class.name} #{context.inspect} " \
|
14
|
-
"readers: #{readers.inspect} " \
|
15
|
-
"setters: #{setters.inspect}>"
|
16
|
-
end
|
17
|
-
|
18
|
-
def fail!(**arguments)
|
19
|
-
context.fail!(**arguments)
|
20
|
-
end
|
21
|
-
|
22
|
-
def succeed!(**arguments)
|
23
|
-
context.fail!(**arguments)
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
attr_reader :context, :readers, :setters
|
29
|
-
|
30
|
-
# rubocop:disable Style/MethodMissingSuper
|
31
|
-
def method_missing(name, *arguments, &block)
|
32
|
-
unless available_methods.include?(name)
|
33
|
-
raise ArgumentError, "Cannot call #{name} on #{inspect}"
|
34
|
-
end
|
35
|
-
|
36
|
-
context.public_send(name, *arguments, &block)
|
37
|
-
end
|
38
|
-
# rubocop:enable Style/MethodMissingSuper
|
39
|
-
|
40
|
-
def respond_to_missing?(name, _include_private = false)
|
41
|
-
available_methods.include?(name)
|
42
|
-
end
|
43
|
-
|
44
|
-
def available_methods
|
45
|
-
@available_methods ||=
|
46
|
-
readers + setters.map { |key| "#{key}=".to_sym }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
data/lib/actor/requireable.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
4
|
-
# Ensure your inputs and outputs are not nil by adding `required: true`.
|
5
|
-
#
|
6
|
-
# Example:
|
7
|
-
#
|
8
|
-
# class CreateUser < Actor
|
9
|
-
# input :name, required: true
|
10
|
-
# output :user, required: true
|
11
|
-
# end
|
12
|
-
module Requireable
|
13
|
-
def before
|
14
|
-
super
|
15
|
-
|
16
|
-
check_required_definitions(self.class.inputs, kind: 'Input')
|
17
|
-
end
|
18
|
-
|
19
|
-
def after
|
20
|
-
super
|
21
|
-
|
22
|
-
check_required_definitions(self.class.outputs, kind: 'Output')
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def check_required_definitions(definitions, kind:)
|
28
|
-
definitions.each do |key, options|
|
29
|
-
next unless options[:required] && @context[key].nil?
|
30
|
-
|
31
|
-
raise ArgumentError,
|
32
|
-
"#{kind} #{key} on #{self.class} is required but was nil."
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/lib/actor/type_checkable.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Actor
|
4
|
-
# Adds `type:` checking to inputs and outputs. Accepts strings that should
|
5
|
-
# match an ancestor. Also accepts arrays.
|
6
|
-
#
|
7
|
-
# Example:
|
8
|
-
#
|
9
|
-
# class ReduceOrderAmount < Actor
|
10
|
-
# input :order, type: 'Order'
|
11
|
-
# input :amount, type: %w[Integer Float]
|
12
|
-
# input :bonus_applied, type: %w[TrueClass FalseClass]
|
13
|
-
# end
|
14
|
-
module TypeCheckable
|
15
|
-
def before
|
16
|
-
super
|
17
|
-
|
18
|
-
check_type_definitions(self.class.inputs, kind: 'Input')
|
19
|
-
end
|
20
|
-
|
21
|
-
def after
|
22
|
-
super
|
23
|
-
|
24
|
-
check_type_definitions(self.class.outputs, kind: 'Output')
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def check_type_definitions(definitions, kind:)
|
30
|
-
definitions.each do |key, options|
|
31
|
-
type_definition = options[:type] || next
|
32
|
-
value = @context[key] || next
|
33
|
-
|
34
|
-
types = Array(type_definition).map { |name| Object.const_get(name) }
|
35
|
-
next if types.any? { |type| value.is_a?(type) }
|
36
|
-
|
37
|
-
error = "#{kind} #{key} on #{self.class} must be of type " \
|
38
|
-
"#{types.join(', ')} but was #{value.class}"
|
39
|
-
raise ArgumentError, error
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|