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
@@ -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
|