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.
data/lib/service_actor.rb CHANGED
@@ -1,88 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'actor/failure'
4
- require 'actor/success'
5
- require 'actor/context'
6
- require 'actor/filtered_context'
3
+ require 'service_actor/base'
7
4
 
8
- require 'actor/playable'
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 Attributable
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceActor
4
+ # Raised when an input or output does not match the given conditions.
5
+ class ArgumentError < Error; end
6
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Actor
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
- context.public_send(name)
27
+ result[name]
29
28
  end
30
29
 
31
- private name
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
- def outputs
43
- @outputs ||= { error: { type: 'String' } }
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
- module PrependedMethods
48
- # rubocop:disable Naming/MemoizedInstanceVariableName
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceActor
4
+ # Standard exception from which other inherit.
5
+ class Error < StandardError; end
6
+ 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