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.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Actor
4
- # DSL to call a series of actors with the same context. On failure, calls
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(@context)
41
+ next if options[:if] && !options[:if].call(result)
42
42
 
43
43
  play_actor(options[:actor])
44
44
  end
45
- rescue Actor::Failure
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(@context)
65
- actor.run
64
+ actor = actor.new(result)
65
+ actor._call
66
66
  else
67
- actor.call(@context)
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
- class Actor
4
- # Represents the result of an action.
5
- class Context < OpenStruct
6
- def self.to_context(data)
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
- "<ActorContext #{to_h}>"
15
+ "<#{self.class.name} #{to_h}>"
14
16
  end
15
17
 
16
- def fail!(context = {})
17
- merge!(context)
18
+ def fail!(result = {})
19
+ merge!(result)
18
20
  merge!(failure?: true)
19
21
 
20
- raise Actor::Failure, self
22
+ raise Failure, self
21
23
  end
22
24
 
23
- def succeed!(context = {})
24
- merge!(context)
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 Actor::Success, self
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!(context)
39
- context.each_pair do |key, value|
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
- class Actor
3
+ module ServiceActor
4
4
  # Raised when using `succeed!` to halt the progression of an organizer.
5
- class Success < StandardError; end
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceActor
4
+ VERSION = '2.0.0'
5
+ 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: 1.1.0
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-18 00:00:00.000000000 Z
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
@@ -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
@@ -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
@@ -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
@@ -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