actioninteractor 0.1.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2204c4f56ead70dd52e32bf5a656a2421c5b85e7de1ceec6d43caa7446ae9866
4
- data.tar.gz: a915d6983916e0a41b12fc65806fae06e0e76c6a4dbce3a1e907de519ecc7b7f
3
+ metadata.gz: f97f47174484834e5e39341900c7a91760664041393e05bbb006f2b144e05e1c
4
+ data.tar.gz: bd8e7df376548d43e245bc86e939ffd853b22843fd2abf74819f108e01edc938
5
5
  SHA512:
6
- metadata.gz: 6d41987911103f961d7286fd4b57cf55ba646e4021bf98361dd9962691ff9cecd780896f65cf3a02318a3bb5d01915037d6ef524093251fcf193d6a5df5b53ae
7
- data.tar.gz: 44eae8fdacf7ff12e16e93178acec405db9178f1f8394ee2f6a140d152a433a97e5ef7c0ac8e20155cddc1de7ae4e5521876faf12230f2257a260746e1982b6a
6
+ metadata.gz: 20c5c68d896c830eb4e1a3e4a7104ae79e8d5bcd5a3471b5d091b2f252fe8fe2f5504cf3605403ed8b054308da0a27b1cac5395abf6d1ac5591b95c133af6a84
7
+ data.tar.gz: eeb597af885f874170ae7c1f7009b446f90e74e5b725d2bf14d4251cc1327d0e5bd9839ceedd8ca24011d5d9512664e8d20cc862e4a2b946b1ccf03a207b2075
@@ -14,24 +14,25 @@ module ActionInteractor
14
14
  #
15
15
  # class RegistrationInteractor < ActionInteractor::Base
16
16
  # def execute
17
- # return fail! unless payload[:name]
17
+ # return failure! unless payload[:name]
18
18
  # user = User.create!(name: payload[:name])
19
19
  # notiticaion = user.notifications.create!(name: 'Welcome')
20
20
  # RegistrationNotificationJob.perform_later!
21
21
  # results.add(:user, user)
22
- # success!
22
+ # successful!
23
23
  # end
24
24
  # end
25
25
  class Base
26
- attr_reader :payload, :errors, :results
26
+ attr_reader :payload, :errors, :results, :state, :interactor_name
27
27
 
28
28
  # Initialize with payload
29
29
  # Errors and Results data and initial state will be set.
30
30
  def initialize(payload)
31
31
  @payload = payload
32
- @errors = Errors.new
33
- @results = Results.new
34
- reset!
32
+ @errors = payload[:errors] || Errors.new
33
+ @results = payload[:results] || Results.new
34
+ @state = payload[:state] || ExecutionState.new
35
+ @interactor_name = payload[:interactor_name] || underscore(self.class.name)
35
36
  end
36
37
 
37
38
  # Execute the operation with given payload.
@@ -39,24 +40,24 @@ module ActionInteractor
39
40
  def execute
40
41
  # if the interactor already finished execution, do nothing.
41
42
  return if finished?
42
- # if contract is not satisfied= (ex: payload is empty), mark as failed.
43
- return fail! if payload.nil?
43
+ # if contract is not satisfied= (ex: payload is empty), mark as failure.
44
+ return failure! if payload.nil?
44
45
  # (Implement some codes for the operation.)
45
46
 
46
- # if finished execution, mark as success.
47
- success!
47
+ # if finished execution, mark as successful.
48
+ successful!
48
49
  end
49
50
 
50
51
  # Execute the operation with given payload.
51
52
  # If there are some errors, ActionInteractor::ExeuctionError will be raised.
52
53
  def execute!
53
54
  execute
54
- success? || raise(ExecutionError.new("Failed to execute the interactor"))
55
+ successful? || raise(ExecutionError.new("Failed to execute the interactor"))
55
56
  end
56
57
 
57
58
  # Returns `true` if marked as finished otherwise `false`.
58
59
  def finished?
59
- @_finished
60
+ state.successful? || state.failure?
60
61
  end
61
62
 
62
63
  # Returns `true` if not marked as finished otherwise `false`.
@@ -65,44 +66,46 @@ module ActionInteractor
65
66
  end
66
67
 
67
68
  # Returns `true` if marked as success and there are no errors otherwise `false`.
68
- def success?
69
- @_success && @errors.empty?
69
+ def successful?
70
+ state.successful? && @errors.empty?
70
71
  end
71
72
 
73
+ alias_method :success?, :successful?
74
+
72
75
  # Returns `true` if not marked as success or there are some errors otherwise `false`.
73
76
  def failure?
74
- !success?
77
+ !successful?
75
78
  end
76
79
 
77
80
  # Returns `true` if the operation was not successful and not finished otherwise `false`.
78
81
  def aborted?
79
- failure? && unfinished?
82
+ state.aborted?
80
83
  end
81
84
 
82
85
  # Reset statuses.
83
86
  def reset!
84
- @_success = false
85
- @_finished = false
87
+ @state = State.new
86
88
  end
87
89
 
88
- # Mark the operation as failed and unfinished.
90
+ # Mark the operation as aborted.
89
91
  def abort!
90
- @_success = false
91
- @_finished = false
92
+ state.aborted!
92
93
  end
93
94
 
94
- # Mark the operation as success and finished.
95
- def success!
96
- @_success = true
97
- @_finished = true
95
+ # Mark the operation as successful.
96
+ def successful!
97
+ state.successful!
98
98
  end
99
99
 
100
- # Mask the operation as failed and finished.
101
- def fail!
102
- @_success = false
103
- @_finished = true
100
+ alias_method :success!, :successful!
101
+
102
+ # Mask the operation as failure.
103
+ def failure!
104
+ state.failure!
104
105
  end
105
106
 
107
+ alias_method :fail!, :failure!
108
+
106
109
  class << self
107
110
  # Execute the operation with given payload.
108
111
  def execute(payload)
@@ -115,6 +118,16 @@ module ActionInteractor
115
118
  new(payload).tap(&:execute!)
116
119
  end
117
120
  end
121
+
122
+ private
123
+
124
+ def underscore(name)
125
+ name.gsub(/::/, "__")
126
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
127
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
128
+ .tr("-", "_")
129
+ .downcase
130
+ end
118
131
  end
119
132
 
120
133
  class ExecutionError < StandardError; end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionInteractor
4
+ class Composite < Base
5
+ # == Action \Interactor \Composite
6
+ #
7
+ # An interactor class which containing multiple interactors
8
+ # using the composite pattern.
9
+ #
10
+ # It can be used for execute multiple operations and it will be
11
+ # marked as successful if all operations executed successful.
12
+ # (otherwise it will be marked as failure.)
13
+ attr_reader :interactors
14
+
15
+ # Initialize with payload and an array for containing interactors.
16
+ def initialize(payload)
17
+ super
18
+ @interactors = []
19
+ end
20
+
21
+ # Execute containing interactors' `execute` method with given payload.
22
+ def execute
23
+ return if finished?
24
+ return failure! if payload.nil?
25
+
26
+ interactors.each_with_index do |interactor, index|
27
+ execute_sub_interactor(interactor, index)
28
+ return failure! if interactor.failure?
29
+ end
30
+
31
+ successful!
32
+ end
33
+
34
+ # Add an interactor to the interactors array.
35
+ def add(interactor)
36
+ interactors << interactor
37
+ end
38
+
39
+ # Delete the interactor from the interactors array.
40
+ def delete(interactor)
41
+ interactors.delete(interactor)
42
+ end
43
+
44
+ private
45
+
46
+ def execute_sub_interactor(interactor, index)
47
+ interactor.execute
48
+ if interactor.successful?
49
+ interactor.results.each_pair do |attr_name, value|
50
+ results.add(sub_attr_key(interactor, index, attr_name), value)
51
+ end
52
+ else interactor.failure?
53
+ interactor.errors.each_pair do |attr_name, value|
54
+ errors.add(sub_attr_key(interactor, index, attr_name), value)
55
+ end
56
+ end
57
+ end
58
+
59
+ def sub_attr_key(interactor, index, attr_name)
60
+ "#{interactor.interactor_name}_#{index}__#{attr_name}".to_sym
61
+ end
62
+ end
63
+ end
@@ -11,7 +11,7 @@ module ActionInteractor
11
11
 
12
12
  attr_reader :errors
13
13
 
14
- def_delegators :@errors, :clear, :keys, :values, :[], :empty?, :any?
14
+ def_delegators :@errors, :clear, :keys, :values, :[], :empty?, :any?, :each, :each_pair
15
15
 
16
16
  def initialize(*)
17
17
  @errors = {}
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionInteractor
4
+ # == Action \Interactor \Execution \State
5
+ #
6
+ # State machine used in `ActionInteractor::Base`
7
+ class ExecutionState < State
8
+ # Define default states
9
+ STATES = [
10
+ :initial, # Initial state
11
+ :processing, # The operation is processing
12
+ :successful, # The operation is finished successfully
13
+ :failure, # The operation is failed
14
+ :aborted, # The operation is aborted
15
+ ]
16
+
17
+ # Define default transitions
18
+ # key: target state, value: original states
19
+ TRANSITIONS = {
20
+ initial: [:processing],
21
+ processing: [:initial],
22
+ successful: [:initial, :processing],
23
+ failure: [:initial, :processing],
24
+ aborted: [:initial, :processing],
25
+ }
26
+ end
27
+ end
@@ -11,7 +11,7 @@ module ActionInteractor
11
11
 
12
12
  attr_reader :results
13
13
 
14
- def_delegators :@results, :clear, :keys, :values, :[]
14
+ def_delegators :@results, :clear, :keys, :values, :[], :empty?, :any?, :each, :each_pair
15
15
 
16
16
  def initialize(*)
17
17
  @results = {}
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionInteractor
4
+ # == Action \Interactor \State
5
+ #
6
+ # Simple state machine for general purpose.
7
+ # You can declare STATES / TRANSITIONS in subclasses for
8
+ # customizing the behavior.
9
+ # See also `ActionInteractor::ExecutionState`
10
+ class State
11
+ # Define default states
12
+ STATES = [:initial, :finished]
13
+
14
+ # Define default transitions
15
+ # key: target state, value: original states
16
+ TRANSITIONS = {
17
+ finished: [:initial]
18
+ }
19
+
20
+ attr_reader :state
21
+
22
+ def initialize(initial_state=nil)
23
+ @state = initial_state || default_state
24
+ end
25
+
26
+ # Default initial state
27
+ # (You can override in subclasses.)
28
+ def default_state
29
+ :initial
30
+ end
31
+
32
+ # Returns true if transition to target_state from current state
33
+ def valid_transition?(target_state)
34
+ transitions[target_state].include?(state)
35
+ end
36
+
37
+ # Available states for the instance
38
+ def states
39
+ self.class.states
40
+ end
41
+
42
+ # Avaiolable transitions for the instance's states
43
+ def transitions
44
+ self.class.transitions
45
+ end
46
+
47
+ # Available states for the class
48
+ def self.states
49
+ self::STATES
50
+ end
51
+
52
+ # Available transitions for the class
53
+ def self.transitions
54
+ self::TRANSITIONS
55
+ end
56
+
57
+ def method_missing(method_name, *args)
58
+ name = method_name.to_s
59
+ # Returns true if state_name is the same as current state
60
+ if status_method_with_suffix?(name, "?")
61
+ return state == name.chop.to_sym
62
+ end
63
+
64
+ # Set current state to the state_name, otherwise raises error
65
+ if status_method_with_suffix?(name, "!")
66
+ state_name = name.chop.to_sym
67
+ unless valid_transition?(state_name)
68
+ raise TransitionError.new("Could not change state :#{state_name} from :#{state}")
69
+ end
70
+ @state = state_name
71
+ return
72
+ end
73
+
74
+ super
75
+ end
76
+
77
+ private
78
+
79
+ def status_method_with_suffix?(method_name, suffix)
80
+ return false unless method_name.end_with?(suffix)
81
+ states.include?(method_name.chop.to_sym)
82
+ end
83
+
84
+ # Error for invalid transitions
85
+ class TransitionError < StandardError; end
86
+ end
87
+ end
@@ -6,4 +6,7 @@ module ActionInteractor
6
6
  autoload :Base, "action_interactor/base"
7
7
  autoload :Errors, "action_interactor/errors"
8
8
  autoload :Results, "action_interactor/results"
9
+ autoload :State, "action_interactor/state"
10
+ autoload :ExecutionState, "action_interactor/execution_state"
11
+ autoload :Composite, "action_interactor/composite"
9
12
  end
@@ -33,7 +33,13 @@ class BaseTest < Test::Unit::TestCase
33
33
  assert_instance_of(ActionInteractor::Results, interactor.results)
34
34
  end
35
35
 
36
- test "#success? is true" do
36
+ test "#successful? is true" do
37
+ payload = {}
38
+ interactor = ActionInteractor::Base.execute(payload)
39
+ assert interactor.successful?
40
+ end
41
+
42
+ test "#success? (alias) is true" do
37
43
  payload = {}
38
44
  interactor = ActionInteractor::Base.execute(payload)
39
45
  assert interactor.success?
@@ -47,7 +53,7 @@ class BaseTest < Test::Unit::TestCase
47
53
 
48
54
  test "#aborted? is true after #abort!" do
49
55
  payload = {}
50
- interactor = ActionInteractor::Base.execute(payload)
56
+ interactor = ActionInteractor::Base.new(payload)
51
57
  interactor.abort!
52
58
  assert interactor.aborted?
53
59
  end
@@ -0,0 +1,92 @@
1
+ require "test/unit"
2
+ require_relative "../lib/actioninteractor"
3
+
4
+ class ChiefInteractor < ActionInteractor::Composite
5
+ end
6
+
7
+ class CookInteractor < ActionInteractor::Base
8
+ def execute
9
+ results.add(:steak, "Juicy beaf steak")
10
+ successful!
11
+ end
12
+ end
13
+
14
+ class ArrangeInteractor < ActionInteractor::Base
15
+ attr_reader :broken_dish
16
+
17
+ def initialize(payload)
18
+ super
19
+ @broken_dish = payload[:broken_dish].nil? ? false : payload[:broken_dish]
20
+ end
21
+
22
+ def execute
23
+ if broken_dish
24
+ errors.add(:dish, "Broken dish")
25
+ failure!
26
+ else
27
+ successful!
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ class CompositeTest < Test::Unit::TestCase
34
+ test "initialized successfully" do
35
+ payload = {}
36
+ interactor = ActionInteractor::Composite.new(payload)
37
+ assert_equal(interactor.success?, false)
38
+ assert_equal(interactor.finished?, false)
39
+ assert_equal(interactor.errors.empty?, true)
40
+ end
41
+
42
+ test "add interactors" do
43
+ payload = {}
44
+ chief_interactor = ChiefInteractor.new(payload)
45
+ cook_interactor = CookInteractor.new(interactor_name: "cook")
46
+ arrange_interactor = ArrangeInteractor.new(interactor_name: "arrange")
47
+ chief_interactor.add(cook_interactor)
48
+ chief_interactor.add(arrange_interactor)
49
+ interactor_names = chief_interactor.interactors.map(&:interactor_name)
50
+ assert_equal(interactor_names, %w[cook arrange])
51
+ end
52
+
53
+ test "remove interactor" do
54
+ payload = {}
55
+ chief_interactor = ChiefInteractor.new(payload)
56
+ cook_interactor = CookInteractor.new(interactor_name: "cook")
57
+ arrange_interactor = ArrangeInteractor.new(interactor_name: "arrange")
58
+ chief_interactor.add(cook_interactor)
59
+ chief_interactor.add(arrange_interactor)
60
+ chief_interactor.delete(cook_interactor)
61
+ interactor_names = chief_interactor.interactors.map(&:interactor_name)
62
+ assert_equal(interactor_names, %w[arrange])
63
+ end
64
+
65
+ test "successful in executing all interactors" do
66
+ payload = {}
67
+ chief_interactor = ChiefInteractor.new(payload)
68
+ cook_interactor = CookInteractor.new(interactor_name: "cook")
69
+ arrange_interactor = ArrangeInteractor.new(interactor_name: "arrange")
70
+ chief_interactor.add(cook_interactor)
71
+ chief_interactor.add(arrange_interactor)
72
+ chief_interactor.execute
73
+ assert_equal(chief_interactor.successful?, true)
74
+ assert_equal(chief_interactor.results.keys, [:cook_0__steak])
75
+ assert_equal(chief_interactor.results[:cook_0__steak], "Juicy beaf steak")
76
+ end
77
+
78
+ test "failure in executing the last interactor" do
79
+ payload = {}
80
+ chief_interactor = ChiefInteractor.new(payload)
81
+ cook_interactor = CookInteractor.new(interactor_name: "cook")
82
+ arrange_interactor = ArrangeInteractor.new(interactor_name: "arrange", broken_dish: true)
83
+ chief_interactor.add(cook_interactor)
84
+ chief_interactor.add(arrange_interactor)
85
+ chief_interactor.execute
86
+ assert_equal(chief_interactor.successful?, false)
87
+ assert_equal(chief_interactor.results.keys, [:cook_0__steak])
88
+ assert_equal(chief_interactor.results[:cook_0__steak], "Juicy beaf steak")
89
+ assert_equal(chief_interactor.errors.keys, [:arrange_1__dish])
90
+ assert_equal(chief_interactor.errors[:arrange_1__dish], "Broken dish")
91
+ end
92
+ end
@@ -0,0 +1,60 @@
1
+ require "test/unit"
2
+ require_relative "../lib/actioninteractor"
3
+
4
+ class ExecutionStateTest < Test::Unit::TestCase
5
+ test "initialized correctly" do
6
+ assert_nothing_raised { ActionInteractor::ExecutionState.new }
7
+ end
8
+
9
+ test "initial state is :initial" do
10
+ state = ActionInteractor::ExecutionState.new
11
+ assert_equal(state.state, :initial)
12
+ end
13
+
14
+ test "default states are :initial, :processing, :successful and :failure" do
15
+ state = ActionInteractor::ExecutionState.new
16
+ assert_equal(state.states, [:initial, :processing, :successful, :failure, :aborted])
17
+ end
18
+
19
+ test "default transitions are defined correctly" do
20
+ state = ActionInteractor::ExecutionState.new
21
+ assert_equal(
22
+ state.transitions,
23
+ {
24
+ initial: [:processing],
25
+ processing: [:initial],
26
+ successful: [:initial, :processing],
27
+ failure: [:initial, :processing],
28
+ aborted: [:initial, :processing],
29
+ }
30
+ )
31
+ end
32
+
33
+ test "able to change state from :initial to :processing" do
34
+ state = ActionInteractor::ExecutionState.new
35
+ assert_equal(state.initial?, true)
36
+ state.processing!
37
+ assert_equal(state.processing?, true)
38
+ end
39
+
40
+ test "able to change state from :processing to :successful" do
41
+ state = ActionInteractor::ExecutionState.new(:processing)
42
+ state.successful!
43
+ assert_equal(state.successful?, true)
44
+ end
45
+
46
+ test "able to change state from :initial to :failure" do
47
+ state = ActionInteractor::ExecutionState.new(:initial)
48
+ state.failure!
49
+ assert_equal(state.failure?, true)
50
+ end
51
+
52
+ test "raise TransitionError when try to change state from :successful to :failure" do
53
+ state = ActionInteractor::ExecutionState.new(:successful)
54
+ assert_equal(state.valid_transition?(:failure), false)
55
+ error = assert_raises ActionInteractor::ExecutionState::TransitionError do
56
+ state.failure!
57
+ end
58
+ assert_equal("Could not change state :failure from :successful", error.message)
59
+ end
60
+ end
@@ -13,10 +13,10 @@ class RegistrationInteractor < ActionInteractor::Base
13
13
  def execute
14
14
  unless payload[:name]
15
15
  errors.add(:name, "can't be blank.")
16
- return fail!
16
+ return failure!
17
17
  end
18
18
  results.add(:user, User.new(name: payload[:name]))
19
- success!
19
+ successful!
20
20
  end
21
21
  end
22
22
 
@@ -24,10 +24,10 @@ class NotificationInteractor < ActionInteractor::Base
24
24
  def execute
25
25
  errors.add(:name, "can't be blank.") unless payload[:name]
26
26
  errors.add(:email, "can't be blank.") unless payload[:email]
27
- return fail! if errors.any?
27
+ return failure! if errors.any?
28
28
  results.add(:name, payload[:name])
29
29
  results.add(:email, payload[:email])
30
- success!
30
+ successful!
31
31
  end
32
32
  end
33
33
 
@@ -38,6 +38,18 @@ class InheritanceTest < Test::Unit::TestCase
38
38
  assert_nothing_raised { RegistrationInteractor.execute(payload) }
39
39
  end
40
40
 
41
+ test "returns 'registration_interactor' as default interactor name" do
42
+ payload = { name: 'John'}
43
+ interactor = RegistrationInteractor.new(payload)
44
+ assert_equal('registration_interactor', interactor.interactor_name)
45
+ end
46
+
47
+ test "returns 'registration' as specified interactor name" do
48
+ payload = { name: 'John', interactor_name: 'registration' }
49
+ interactor = RegistrationInteractor.new(payload)
50
+ assert_equal('registration', interactor.interactor_name)
51
+ end
52
+
41
53
  test ".execute returns an RegistrationInteractor instance" do
42
54
  payload = { name: 'John'}
43
55
  interactor = RegistrationInteractor.execute(payload)
@@ -57,10 +69,10 @@ class InheritanceTest < Test::Unit::TestCase
57
69
  assert_equal(user.name, 'John')
58
70
  end
59
71
 
60
- test "#success? is true" do
72
+ test "#successful? is true" do
61
73
  payload = { name: 'John'}
62
74
  interactor = RegistrationInteractor.execute(payload)
63
- assert interactor.success?
75
+ assert interactor.successful?
64
76
  end
65
77
 
66
78
  test "#finished? is true" do
@@ -122,7 +134,7 @@ class InheritanceTest < Test::Unit::TestCase
122
134
  test "if only the name is given, the registration is successful but the notification is not." do
123
135
  payload = { name: 'Taro' }
124
136
  registration = RegistrationInteractor.execute(payload)
125
- assert registration.success?
137
+ assert registration.successful?
126
138
  assert registration.errors.empty?
127
139
  notification = NotificationInteractor.execute(payload)
128
140
  assert notification.failure?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actioninteractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryo Hashimoto
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-02 00:00:00.000000000 Z
11
+ date: 2020-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -62,11 +62,16 @@ files:
62
62
  - Rakefile
63
63
  - actioninteractor.gemspec
64
64
  - lib/action_interactor/base.rb
65
+ - lib/action_interactor/composite.rb
65
66
  - lib/action_interactor/errors.rb
67
+ - lib/action_interactor/execution_state.rb
66
68
  - lib/action_interactor/results.rb
69
+ - lib/action_interactor/state.rb
67
70
  - lib/actioninteractor.rb
68
71
  - test/base_test.rb
72
+ - test/composite_test.rb
69
73
  - test/errors_test.rb
74
+ - test/execution_state_test.rb
70
75
  - test/inheritance_test.rb
71
76
  - test/results_test.rb
72
77
  homepage: https://github.com/ryohashimoto/lightrails
@@ -95,6 +100,8 @@ summary: Action Interactor provides a simple interface for performing operations
95
100
  of Lightrails).
96
101
  test_files:
97
102
  - test/base_test.rb
103
+ - test/composite_test.rb
98
104
  - test/errors_test.rb
105
+ - test/execution_state_test.rb
99
106
  - test/inheritance_test.rb
100
107
  - test/results_test.rb