aye_commander 1.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.
@@ -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