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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +720 -0
- data/lib/aye_commander.rb +19 -0
- data/lib/aye_commander/abortable.rb +25 -0
- data/lib/aye_commander/callable.rb +41 -0
- data/lib/aye_commander/command.rb +30 -0
- data/lib/aye_commander/commander.rb +120 -0
- data/lib/aye_commander/errors.rb +24 -0
- data/lib/aye_commander/hookable.rb +82 -0
- data/lib/aye_commander/initializable.rb +18 -0
- data/lib/aye_commander/inspectable.rb +36 -0
- data/lib/aye_commander/ivar.rb +94 -0
- data/lib/aye_commander/limitable.rb +83 -0
- data/lib/aye_commander/resultable.rb +58 -0
- data/lib/aye_commander/shareable.rb +32 -0
- data/lib/aye_commander/status.rb +48 -0
- data/lib/aye_commander/version.rb +3 -0
- data/spec/aye_commander/abortable_spec.rb +24 -0
- data/spec/aye_commander/callable_spec.rb +47 -0
- data/spec/aye_commander/command_spec.rb +27 -0
- data/spec/aye_commander/commander_spec.rb +168 -0
- data/spec/aye_commander/errors_spec.rb +25 -0
- data/spec/aye_commander/hookable_spec.rb +105 -0
- data/spec/aye_commander/initializable_spec.rb +33 -0
- data/spec/aye_commander/inspectable_spec.rb +45 -0
- data/spec/aye_commander/ivar_spec.rb +130 -0
- data/spec/aye_commander/limitable_spec.rb +131 -0
- data/spec/aye_commander/resultable_spec.rb +64 -0
- data/spec/aye_commander/shareable_spec.rb +38 -0
- data/spec/aye_commander/status_spec.rb +97 -0
- data/spec/aye_commander_spec.rb +5 -0
- data/spec/spec_helper.rb +49 -0
- metadata +90 -0
@@ -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,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
|