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.
data/lib/service_actor.rb CHANGED
@@ -1,90 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
3
+ require 'service_actor/base'
4
4
 
5
- require 'actor/failure'
6
- require 'actor/success'
7
- require 'actor/context'
8
- require 'actor/filtered_context'
9
-
10
- require 'actor/playable'
11
- require 'actor/attributable'
12
- require 'actor/defaultable'
13
- require 'actor/type_checkable'
14
- require 'actor/requireable'
15
- require 'actor/conditionable'
16
-
17
- # Actors should start with a verb, inherit from Actor and implement a `call`
18
- # method.
5
+ # Class to inherit from in your application.
19
6
  class Actor
20
- include Attributable
21
- include Playable
22
- prepend Defaultable
23
- prepend TypeCheckable
24
- prepend Requireable
25
- prepend Conditionable
26
-
27
- class << self
28
- # Call an actor with a given context. Returns the context.
29
- #
30
- # CreateUser.call(name: 'Joe')
31
- def call(context = {}, **arguments)
32
- context = Actor::Context.to_context(context).merge!(arguments)
33
- new(context).run
34
- context
35
- rescue Actor::Success
36
- context
37
- end
38
-
39
- alias call! call
40
-
41
- # Call an actor with a given context. Returns the context and does not raise
42
- # on failure.
43
- #
44
- # CreateUser.result(name: 'Joe')
45
- def result(context = {}, **arguments)
46
- call(context, **arguments)
47
- rescue Actor::Failure => e
48
- e.context
49
- end
50
- end
51
-
52
- # :nodoc:
53
- def initialize(context)
54
- @context = context
55
- end
56
-
57
- # To implement in your actors.
58
- def call; end
59
-
60
- # To implement in your actors.
61
- def rollback; end
62
-
63
- # :nodoc:
64
- def before; end
65
-
66
- # :nodoc:
67
- def after; end
68
-
69
- # :nodoc:
70
- def run
71
- before
72
- call
73
- after
74
- end
75
-
76
- private
77
-
78
- # Returns the current context from inside an actor.
79
- attr_reader :context
80
-
81
- # Can be called from inside an actor to stop execution and mark as failed.
82
- def fail!(**arguments)
83
- @context.fail!(**arguments)
84
- end
85
-
86
- # Can be called from inside an actor to stop execution early.
87
- def succeed!(**arguments)
88
- @context.succeed!(**arguments)
89
- end
7
+ include ServiceActor::Base
90
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,36 @@
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/success'
9
+ require 'service_actor/argument_error'
10
+
11
+ # Core
12
+ require 'service_actor/result'
13
+ require 'service_actor/attributable'
14
+ require 'service_actor/playable'
15
+ require 'service_actor/core'
16
+
17
+ # Concerns
18
+ require 'service_actor/type_checkable'
19
+ require 'service_actor/nil_checkable'
20
+ require 'service_actor/conditionable'
21
+ require 'service_actor/defaultable'
22
+
23
+ module ServiceActor
24
+ module Base
25
+ def self.included(base)
26
+ # Core
27
+ base.include(Core)
28
+
29
+ # Concerns
30
+ base.include(TypeCheckable)
31
+ base.include(NilCheckable)
32
+ base.include(Conditionable)
33
+ base.include(Defaultable)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
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
+ # Will raise an error if any check does return a truthy value.
6
+ #
7
+ # Example:
8
+ #
9
+ # class Pay < Actor
10
+ # input :provider,
11
+ # must: {
12
+ # exist: ->(provider) { PROVIDERS.include?(provider) }
13
+ # }
14
+ # end
15
+ module Conditionable
16
+ def self.included(base)
17
+ base.prepend(PrependedMethods)
18
+ end
19
+
20
+ module PrependedMethods
21
+ def _call
22
+ self.class.inputs.each do |key, options|
23
+ next unless options[:must]
24
+
25
+ options[:must].each do |name, check|
26
+ value = result[key]
27
+ next if check.call(value)
28
+
29
+ raise ArgumentError,
30
+ "Input #{key} must #{name} but was #{value.inspect}."
31
+ end
32
+ end
33
+
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceActor
4
+ # Actors should start with a verb, inherit from Actor and implement a `call`
5
+ # method.
6
+ module Core
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ base.include(Attributable)
10
+ base.include(Playable)
11
+ end
12
+
13
+ module ClassMethods
14
+ # Call an actor with a given result. Returns the result.
15
+ #
16
+ # CreateUser.call(name: 'Joe')
17
+ def call(options = nil, **arguments)
18
+ result = Result.to_result(options).merge!(arguments)
19
+ new(result)._call
20
+ result
21
+ # DEPRECATED
22
+ rescue Success
23
+ result
24
+ end
25
+
26
+ # :nodoc:
27
+ def call!(**arguments)
28
+ warn "DEPRECATED: Prefer `#{name}.call` to `#{name}.call!`."
29
+ call(**arguments)
30
+ end
31
+
32
+ # Call an actor with arguments. Returns the result and does not raise on
33
+ # failure.
34
+ #
35
+ # CreateUser.result(name: 'Joe')
36
+ def result(data = nil, **arguments)
37
+ call(data, **arguments)
38
+ rescue Failure => e
39
+ e.result
40
+ end
41
+ end
42
+
43
+ # :nodoc:
44
+ def initialize(result)
45
+ @result = result
46
+ end
47
+
48
+ # To implement in your actors.
49
+ def call; end
50
+
51
+ # To implement in your actors.
52
+ def rollback; end
53
+
54
+ # :nodoc:
55
+ def _call
56
+ call
57
+ end
58
+
59
+ private
60
+
61
+ # Returns the current context from inside an actor.
62
+ attr_reader :result
63
+
64
+ def context
65
+ warn "DEPRECATED: Prefer `result.` to `context.` in #{self.class.name}."
66
+
67
+ result
68
+ end
69
+
70
+ # Can be called from inside an actor to stop execution and mark as failed.
71
+ def fail!(**arguments)
72
+ result.fail!(**arguments)
73
+ end
74
+
75
+ # DEPRECATED
76
+ def succeed!(**arguments)
77
+ result.succeed!(**arguments)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,36 @@
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
+ unless input.key?(:default)
24
+ raise ArgumentError, "Input #{name} on #{self.class} is missing."
25
+ end
26
+
27
+ default = input[:default]
28
+ default = default.call if default.respond_to?(:call)
29
+ result[name] = default
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+ end
36
+ 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,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,64 @@
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
+ warn_of_deprecated_required_option(options, name, origin)
31
+
32
+ next if !result[name].nil? || allow_nil?(options)
33
+
34
+ raise ArgumentError,
35
+ "The #{origin} \"#{name}\" on #{self.class} does not allow " \
36
+ 'nil values.'
37
+ end
38
+ end
39
+
40
+ def warn_of_deprecated_required_option(options, name, origin)
41
+ return unless options.key?(:required)
42
+
43
+ warn 'DEPRECATED: The "required" option is deprecated. Replace ' \
44
+ "`#{origin} :#{name}, required: #{options[:required]}` by " \
45
+ "`#{origin} :#{name}, allow_nil: #{!options[:required]}` in " \
46
+ "#{self.class}."
47
+ end
48
+
49
+ def allow_nil?(options)
50
+ if options.key?(:allow_nil)
51
+ options[:allow_nil]
52
+ elsif options.key?(:required)
53
+ !options[:required]
54
+ elsif options.key?(:default) && options[:default].nil?
55
+ true
56
+ elsif options[:type]
57
+ false
58
+ else
59
+ true
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end