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