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