aye_commander 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ module AyeCommander
2
+ # This module takes care of command arguments being limited by the user
3
+ # specifics.
4
+ module Limitable
5
+ # Limitable is a module which functionality is completely defined at class
6
+ # level.
7
+ module ClassMethods
8
+ LIMITERS = %i(receives requires returns).freeze
9
+
10
+ # Contains all the limiters
11
+ def limiters
12
+ @limiters ||= Hash.new([])
13
+ end
14
+
15
+ # Helps the command define methods to not use method missing on every
16
+ # instance.
17
+ #
18
+ # The original idea was to encourage to use uses for a small performance
19
+ # boost when running their commands since the methods would be created at
20
+ # load time.
21
+ # This idea has been since scrapped since it would make the commands look
22
+ # very convoluted and the performance hit is probably neglegible since
23
+ # the methods themselves are defined after the first method missing.
24
+ #
25
+ # The functionality however still remains as limited call this method
26
+ # internally
27
+ def uses(*args)
28
+ uses = limiters[:uses]
29
+ return uses if args.empty?
30
+
31
+ missing = args - uses
32
+ attr_accessor(*missing) if missing.any?
33
+
34
+ limiters[:uses] |= args
35
+ end
36
+
37
+ # Defines .receives .requires and .returns
38
+ # .receives Tells the command which arguments are expected to be received
39
+ # .requires Tells the command which arguments are actually required
40
+ # .returns Tells the command which arguments to return in the result
41
+ LIMITERS.each do |limiter|
42
+ define_method(limiter) do |*args|
43
+ return limiters[__method__] if args.empty?
44
+ uses(*args)
45
+ limiters[__method__] |= args
46
+ end
47
+ end
48
+
49
+ # Helper method that tells the result class which methods to create as
50
+ # readers the first time it is created.
51
+ def readers
52
+ [:status] | uses
53
+ end
54
+
55
+ # Validates the limiter arguments
56
+ def validate_arguments(args, skip_validations: false)
57
+ unless [true, :requires].include?(skip_validations) || requires.empty?
58
+ validate_required_arguments(args)
59
+ end
60
+
61
+ unless [true, :receives].include?(skip_validations) || receives.empty?
62
+ validate_received_arguments(args)
63
+ end
64
+ end
65
+
66
+ # Validates the required arguments
67
+ # Required arguments are ones that your commander absolutely needs to be
68
+ # able to run properly.
69
+ def validate_required_arguments(args)
70
+ missing = requires - args.keys
71
+ raise MissingRequiredArgumentError, missing if missing.any?
72
+ end
73
+
74
+ # Validates the received arguments
75
+ # Received arguments are the ones that your command is able to receive.
76
+ # Any other argument not defined by this would be considered an error.
77
+ def validate_received_arguments(args)
78
+ extras = args.keys - (receives | requires)
79
+ raise UnexpectedReceivedArgumentError, extras if extras.any?
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ module AyeCommander
2
+ # This module helps the command return a special class, the Result which is
3
+ # typically what the command responds with.
4
+ module Resultable
5
+ # Most of the functionality is at class level since it receives several
6
+ # class instance variables.
7
+ module ClassMethods
8
+ # Returns a result based on the skip_cleanup option
9
+ # skip_cleanup
10
+ # false (Default) Returns a result taking returns in account
11
+ # true Returns the result skipping the cleanup.
12
+ # :command Using this option asks to get the command instance rather
13
+ # than a result. This of course means the command is not clean.
14
+ def result(command, skip_cleanup = false)
15
+ case skip_cleanup
16
+ when :command
17
+ command
18
+ when true
19
+ new_result(command.to_hash)
20
+ else
21
+ new_result(command.to_result_hash)
22
+ end
23
+ end
24
+
25
+ # Creates a new instance of a Command::Result with the received values
26
+ # and returns it.
27
+ def new_result(values)
28
+ result_class.new(values)
29
+ end
30
+
31
+ # Returns and/or defines the Result class to be returned by the current
32
+ # command.
33
+ # This class is created under the namespace of the command so the end
34
+ # result looks pretty damn cool in my opinion.
35
+ # Eg: Command::Result
36
+ def result_class
37
+ const_defined?('Result') ? const_get('Result') : define_result_class
38
+ end
39
+
40
+ private
41
+
42
+ # Defines the result class with the necessary modules so it can behave
43
+ # like a result
44
+ def define_result_class
45
+ readers = self.readers
46
+ result = Class.new do
47
+ include Initializable
48
+ include Inspectable
49
+ include Status::Readable
50
+ include Ivar::Readable
51
+ extend Ivar::ClassMethods
52
+ attr_reader(*readers)
53
+ end
54
+ const_set 'Result', result
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ module AyeCommander
2
+ module Shareable
3
+ # This module serves to make sure that when included or inherited everything
4
+ # related to the command is preserved
5
+ # Prepend is not really supported, but you really shouldnt be prepending a
6
+ # command so... meh
7
+ module ClassMethods
8
+ # This ensures that class methods are extended when Command is included
9
+ def included(includer)
10
+ super
11
+ includer.extend AyeCommander::Command::ClassMethods
12
+ %i(@limiters @succeeds @hooks).each do |var|
13
+ if instance_variable_defined? var
14
+ includer.instance_variable_set var, instance_variable_get(var)
15
+ end
16
+ end
17
+ end
18
+
19
+ # Rubys object model already links the ancestry path of singleton classes
20
+ # when using classic inheritance so no need to extend. Just need to add
21
+ # the variables to the inheriter.
22
+ def inherited(inheriter)
23
+ super
24
+ %i(@limiters @succeeds @hooks).each do |var|
25
+ if instance_variable_defined? var
26
+ inheriter.instance_variable_set var, instance_variable_get(var)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ module AyeCommander
2
+ # This module helps Command and Result to be able to respond to various
3
+ # status and status responses.
4
+ module Status
5
+ DEFAULT = :success
6
+
7
+ # Status related class Methods to be included
8
+ module ClassMethods
9
+ # Returns and/or initializes the :@succeeds class instance variable
10
+ def succeeds
11
+ @succeeds ||= [DEFAULT]
12
+ end
13
+
14
+ # Adds extra succeeds status other than success.
15
+ # Use exclude_success: true if for whathever reason you don't want
16
+ # :success to be a successful status.
17
+ def succeeds_with(*args, exclude_success: false)
18
+ @succeeds = succeeds | args
19
+ @succeeds.delete(DEFAULT) if exclude_success
20
+ end
21
+ end
22
+
23
+ # This module defines methods that allow to read and know about the status
24
+ module Readable
25
+ attr_reader :status
26
+
27
+ # Whether or not the command is succesful
28
+ def success?
29
+ self.class.succeeds.include?(status)
30
+ end
31
+
32
+ # Boolean opposite of success
33
+ def failure?
34
+ !success?
35
+ end
36
+ end
37
+
38
+ # This module defines methods that allow to modify the status
39
+ module Writeable
40
+ attr_writer :status
41
+
42
+ # Fails the status
43
+ def fail!(status = :failure)
44
+ @status = status
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module AyeCommander
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,24 @@
1
+ describe AyeCommander::Abortable::ClassMethods do
2
+ include_context :command
3
+
4
+ context '.abortable' do
5
+ it 'does nothing if nothing happens' do
6
+ expect { command.abortable { :nothing } }.to_not raise_error
7
+ end
8
+
9
+ it 'is able to catch throw(:abort!)' do
10
+ expect { command.abortable { throw :abort! } }.to_not raise_error
11
+ end
12
+ end
13
+ end
14
+
15
+ describe AyeCommander::Abortable do
16
+ include_context :command
17
+
18
+ context '#abort' do
19
+ it 'throws with :abort! symbol' do
20
+ expect(instance).to receive(:throw).with(:abort!, :aborted)
21
+ instance.abort!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ describe AyeCommander::Callable::ClassMethods do
2
+ include_context :command
3
+
4
+ context '.call' do
5
+ let(:args) { { some: :other, irrelevant: :args } }
6
+
7
+ it 'calls several methods in a specific order' do
8
+ expect(command).to receive(:new).with(args).and_return(instance)
9
+ expect(command).to receive(:validate_arguments).with(args)
10
+ expect(command).to receive(:abortable)
11
+ expect(command).to receive(:result).with(instance, false)
12
+ command.call(args)
13
+ end
14
+
15
+ it 'runs the aborted hooks if command was aborted' do
16
+ allow(command).to receive(:call_before_hooks) { throw :abort!, :aborted }
17
+ expect(command).to receive(:call_aborted_hooks)
18
+ command.call(args)
19
+ end
20
+
21
+ it 'calls several methods in the abortable block' do
22
+ allow(command).to receive(:new).and_return(instance)
23
+ expect(command).to receive(:call_before_hooks)
24
+ expect(instance).to receive(:call)
25
+ expect(command).to receive(:call_after_hooks)
26
+ expect(command).to_not receive(:call_aborted_hooks)
27
+ command.call(args)
28
+ end
29
+
30
+ it 'calls around hooks only if they exist' do
31
+ command.around { :something }
32
+ allow(command).to receive(:new).and_return(instance)
33
+ expect(command).to receive(:call_around_hooks)
34
+ command.call(args)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe AyeCommander::Callable do
40
+ include_context :command
41
+
42
+ context '#call' do
43
+ it 'exists' do
44
+ expect(instance).to respond_to :call
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ describe AyeCommander::Command do
2
+ include_context :command
3
+
4
+ context 'a command' do
5
+ it 'includes the necessary instance modules' do
6
+ expect(command).to include AyeCommander::Abortable
7
+ expect(command).to include AyeCommander::Callable
8
+ expect(command).to include AyeCommander::Initializable
9
+ expect(command).to include AyeCommander::Inspectable
10
+ expect(command).to include AyeCommander::Ivar::Readable
11
+ expect(command).to include AyeCommander::Ivar::Writeable
12
+ expect(command).to include AyeCommander::Status::Readable
13
+ expect(command).to include AyeCommander::Status::Writeable
14
+ end
15
+
16
+ it 'includes the necessary class modules' do
17
+ expect(commandsc).to include AyeCommander::Abortable::ClassMethods
18
+ expect(commandsc).to include AyeCommander::Callable::ClassMethods
19
+ expect(commandsc).to include AyeCommander::Hookable::ClassMethods
20
+ expect(commandsc).to include AyeCommander::Ivar::ClassMethods
21
+ expect(commandsc).to include AyeCommander::Limitable::ClassMethods
22
+ expect(commandsc).to include AyeCommander::Resultable::ClassMethods
23
+ expect(commandsc).to include AyeCommander::Shareable::ClassMethods
24
+ expect(commandsc).to include AyeCommander::Status::ClassMethods
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,168 @@
1
+ describe AyeCommander::Commander::ClassMethods do
2
+ include_context :commander
3
+
4
+ context '.included' do
5
+ it 'includes Commanders Class Methods when included' do
6
+ expect(commander.singleton_class).to include AyeCommander::Commander::ClassMethods
7
+ end
8
+
9
+ it 'includes the necessary modules even further down the line' do
10
+ expect(includer2).to include AyeCommander::Commander
11
+ expect(includer2.singleton_class).to include AyeCommander::Commander::ClassMethods
12
+ end
13
+
14
+ it 'saves the necessary instance variables for the commander' do
15
+ includer.execute :taco, :burrito
16
+ expect(includer2.executes).to eq %i(taco burrito)
17
+ end
18
+ end
19
+
20
+ context '.inherited' do
21
+ it 'includes the necessary modules even further down the line' do
22
+ expect(inheriter).to include AyeCommander::Commander
23
+ expect(inheriter.singleton_class).to include AyeCommander::Commander::ClassMethods
24
+ end
25
+
26
+ it 'saves the necessary instance variables for the commander' do
27
+ commander.execute :taco, :burrito
28
+ expect(inheriter.executes).to eq %i(taco burrito)
29
+ end
30
+ end
31
+
32
+ context '.call' do
33
+ it 'calls several methods' do
34
+ expect(commander).to receive(:prepare_commander_result)
35
+ expect(commander).to receive(:result).twice
36
+ commander.call
37
+ end
38
+ end
39
+
40
+ context '.prepare_commander_result' do
41
+ it 'cleans up the commander after being called' do
42
+ commander.execute(command)
43
+ result = commander.call(taco: :bell)
44
+ expect(result.instance_variables).to include :@status
45
+ expect(result.instance_variables).to include :@executed
46
+ expect(result.instance_variables).to include :@taco
47
+ expect(result.instance_variables).to_not include :@command
48
+ end
49
+ end
50
+
51
+ context '.command' do
52
+ it 'gives an anonymous class that includes Command' do
53
+ expect(commander.command).to be_instance_of Class
54
+ expect(commander.command).to include AyeCommander::Command
55
+ end
56
+
57
+ it 'always returns the same one per commander' do
58
+ expect(commander.command.object_id).to eq commander.command.object_id
59
+ end
60
+ end
61
+
62
+ context '.execute' do
63
+ it 'adds the received arguments to the executes array' do
64
+ commander.execute :taco, :burrito
65
+ expect(commander.executes).to eq %i(taco burrito)
66
+ end
67
+ end
68
+
69
+ context '.executes' do
70
+ it 'returns an empty array if it has not been initialized' do
71
+ expect(commander.executes).to be_empty
72
+ end
73
+
74
+ it 'returns the content of the class instance variable executes' do
75
+ commander.instance_variable_set :@executes, :random
76
+ expect(commander.executes).to eq :random
77
+ end
78
+ end
79
+
80
+ context '.abort_on_failure' do
81
+ it 'sets the @abort_on_failure to true when called without arguments' do
82
+ commander.abort_on_failure
83
+ expect(commander.instance_variable_get(:@abort_on_failure)).to be true
84
+ end
85
+
86
+ it 'sets the @abort_on_failure to the received argument' do
87
+ commander.abort_on_failure false
88
+ expect(commander.instance_variable_get(:@abort_on_failure)).to be false
89
+ end
90
+ end
91
+
92
+ context '.abort_on_failure?' do
93
+ it 'returns @abort_on_failure' do
94
+ expect(commander.abort_on_failure?).to be_nil
95
+ commander.abort_on_failure true
96
+ expect(commander.abort_on_failure?).to be true
97
+ end
98
+ end
99
+ end
100
+
101
+ describe AyeCommander::Commander do
102
+ include_context :commander
103
+
104
+ context '#initialize' do
105
+ it 'initializes the commander with the required variables' do
106
+ ci = commander.new(taco: :potato)
107
+ expect(ci.command).to be_instance_of commander.command
108
+ expect(ci.executed).to eq []
109
+ end
110
+ end
111
+
112
+ context '#call' do
113
+ it 'executes the comamnds in the order they were received' do
114
+ commander.execute(1, 1, 1)
115
+ expect(instance).to receive(:execute).with(1, 1, 1, abort_on_failure: true)
116
+ instance.call
117
+ end
118
+ end
119
+
120
+ context '#execute' do
121
+ before do
122
+ commander.class_eval { public :execute }
123
+ end
124
+
125
+ it 'calls the command with the result of the previous command' do
126
+ expect(instance.command).to receive(:to_hash).and_return(hello: :world)
127
+ instance.execute(command)
128
+ end
129
+
130
+ it 'updates the command variable to the last executed command instance' do
131
+ allow(command).to receive(:call).and_return(commandi)
132
+ instance.execute(command)
133
+ expect(instance.command).to eq commandi
134
+ end
135
+
136
+ it 'pushes the command to the executed array' do
137
+ allow(command).to receive(:call).and_return(commandi)
138
+ instance.execute(command)
139
+ expect(instance.executed).to eq [commandi]
140
+ end
141
+
142
+ it 'calls fail! and abort! if command fails and with abort_on_failure option' do
143
+ allow(command).to receive(:call).and_return(commandi)
144
+ commandi.fail!
145
+
146
+ expect(instance).to receive(:fail!)
147
+ expect(instance).to receive(:abort!)
148
+ instance.execute(command, abort_on_failure: true)
149
+ end
150
+
151
+ it 'doesnt calls fail! and abort! even if command fails and without abort_on_failure option' do
152
+ allow(command).to receive(:call).and_return(commandi)
153
+ commandi.fail!
154
+
155
+ expect(instance).to_not receive(:fail!)
156
+ expect(instance).to_not receive(:abort!)
157
+ instance.execute(command)
158
+ end
159
+
160
+ it 'doesnt calls fail! and abort! if command succeeds even with abort_on_failure option' do
161
+ allow(command).to receive(:call).and_return(commandi)
162
+
163
+ expect(instance).to_not receive(:fail!)
164
+ expect(instance).to_not receive(:abort!)
165
+ instance.execute(command, abort_on_failure: true)
166
+ end
167
+ end
168
+ end