aye_commander 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|