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,19 @@
|
|
1
|
+
require 'aye_commander/abortable'
|
2
|
+
require 'aye_commander/callable'
|
3
|
+
require 'aye_commander/hookable'
|
4
|
+
require 'aye_commander/initializable'
|
5
|
+
require 'aye_commander/inspectable'
|
6
|
+
require 'aye_commander/ivar'
|
7
|
+
require 'aye_commander/limitable'
|
8
|
+
require 'aye_commander/shareable'
|
9
|
+
require 'aye_commander/status'
|
10
|
+
require 'aye_commander/errors'
|
11
|
+
|
12
|
+
require 'aye_commander/resultable'
|
13
|
+
require 'aye_commander/command'
|
14
|
+
require 'aye_commander/commander'
|
15
|
+
|
16
|
+
# AyeCommander is a (hopefully) easy to use gem that helps develop classes that
|
17
|
+
# follow the command pattern.
|
18
|
+
module AyeCommander
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This module helps a command to stop the code flow completely and return the
|
3
|
+
# result immediately.
|
4
|
+
# It is specially useful when your command is running on more deeply nested
|
5
|
+
# code (Eg: private methods called by call)
|
6
|
+
#
|
7
|
+
# It also uses, what is probably one of the most underused features of ruby:
|
8
|
+
# catch and throw.
|
9
|
+
module Abortable
|
10
|
+
# Abortable just comes with a class method that is basically a wrapper for
|
11
|
+
# catch and throw.
|
12
|
+
module ClassMethods
|
13
|
+
# .abortable receives a block and yields it inside a catch so that abort!
|
14
|
+
# can be safely called.
|
15
|
+
def abortable
|
16
|
+
catch(:abort!) { yield }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# #abort! throws an :abort! to stop the current command flow on its tracks
|
21
|
+
def abort!
|
22
|
+
throw :abort!, :aborted
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This module takes care of both .call and #call, the most important methods
|
3
|
+
# in a command
|
4
|
+
module Callable
|
5
|
+
# Class Methods defined by callable
|
6
|
+
module ClassMethods
|
7
|
+
# .call is what the user calls when he wants to run his commands. It is
|
8
|
+
# able to receive several named arguments, and a couple of options for
|
9
|
+
# specific behavior on how the command must be run.
|
10
|
+
#
|
11
|
+
# Options
|
12
|
+
# skip_validations: (Handled by validate_arguments)
|
13
|
+
# true Skips both :receives and :requires argument validations
|
14
|
+
# :requires Skips :requires argument validation
|
15
|
+
# :receives Skips :receives argument validation
|
16
|
+
#
|
17
|
+
# skip_cleanup:
|
18
|
+
# true Skips the result cleanup so it has all the instance variables
|
19
|
+
# that were declared
|
20
|
+
# :command Returns the command instead of an instance of the result
|
21
|
+
# class
|
22
|
+
def call(skip_cleanup: false, **args)
|
23
|
+
command = new(args)
|
24
|
+
validate_arguments(args)
|
25
|
+
aborted = abortable do
|
26
|
+
call_before_hooks(command)
|
27
|
+
around_hooks.any? ? call_around_hooks(command) : command.call
|
28
|
+
call_after_hooks(command)
|
29
|
+
end
|
30
|
+
abortable { call_aborted_hooks(command) } if aborted == :aborted
|
31
|
+
result(command, skip_cleanup)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# #call is what a user redefines in their own command, and what he
|
36
|
+
# customizes to give a command the behavior he desires.
|
37
|
+
# An empty call is defined in a command so they can be run even without one.
|
38
|
+
def call
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This is the meat of AyeComander, what the user has to include in his own
|
3
|
+
# commands for everything to work.
|
4
|
+
module Command
|
5
|
+
# Class Methods that define the functionality of a command.
|
6
|
+
# The most complex functionality is in fact contained at class level since
|
7
|
+
# I wanted to preserve the commands as clean as possible to avoid name
|
8
|
+
# clashes within the instance.
|
9
|
+
module ClassMethods
|
10
|
+
include Abortable::ClassMethods
|
11
|
+
include Callable::ClassMethods
|
12
|
+
include Hookable::ClassMethods
|
13
|
+
include Ivar::ClassMethods
|
14
|
+
include Limitable::ClassMethods
|
15
|
+
include Resultable::ClassMethods
|
16
|
+
include Shareable::ClassMethods
|
17
|
+
include Status::ClassMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
include Abortable
|
21
|
+
include Callable
|
22
|
+
include Initializable
|
23
|
+
include Inspectable
|
24
|
+
include Ivar::Readable
|
25
|
+
include Ivar::Writeable
|
26
|
+
include Status::Readable
|
27
|
+
include Status::Writeable
|
28
|
+
extend ClassMethods
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# Commander is a special command that lets you run several command in a
|
3
|
+
# succession. At the end it returns its own result containing a hash with
|
4
|
+
# the commands run.
|
5
|
+
module Commander
|
6
|
+
# Eventhough the Commander is basically a Command, it does come with some
|
7
|
+
# minor tweaking to keep it simple to understand and consistant
|
8
|
+
module ClassMethods
|
9
|
+
# This ensure that Commander specific class methods and commander specific
|
10
|
+
# class instance variables are included when the Commander is included
|
11
|
+
def included(includer)
|
12
|
+
super
|
13
|
+
includer.extend ClassMethods
|
14
|
+
includer.instance_variable_set :@executes, @executes
|
15
|
+
includer.instance_variable_set :@abort_on_failure, @abort_on_failure
|
16
|
+
end
|
17
|
+
|
18
|
+
# This ensures that Commander specific instance variables become available
|
19
|
+
# for any class inheriting from another that includes the command
|
20
|
+
def inherited(inheriter)
|
21
|
+
super
|
22
|
+
inheriter.instance_variable_set :@executes, @executes
|
23
|
+
inheriter.instance_variable_set :@abort_on_failure, @abort_on_failure
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override of Command.call
|
27
|
+
# It's almost identical to a normal command call, the only difference
|
28
|
+
# being that it has to prepare the commander result before sending it.
|
29
|
+
#
|
30
|
+
# This was previously done through hooks, but the idea was scrapped to
|
31
|
+
# avoid inconsistencies with the command instance variable during after
|
32
|
+
# and aborted hooks
|
33
|
+
def call(skip_cleanup: false, **args)
|
34
|
+
commander = super(skip_cleanup: :command, **args)
|
35
|
+
prepare_commander_result(commander)
|
36
|
+
result(commander, skip_cleanup)
|
37
|
+
end
|
38
|
+
|
39
|
+
# This method is always run before retuning the result of the commander
|
40
|
+
# It basically removes command instance variable since it's only relevant
|
41
|
+
# during the execution of the commander itself.
|
42
|
+
# It also assigns the ivars of the last executed command to itself.
|
43
|
+
def prepare_commander_result(commander)
|
44
|
+
commander.instance_exec do
|
45
|
+
command.to_hash.each do |name, value|
|
46
|
+
instance_variable_set to_ivar(name), value
|
47
|
+
end
|
48
|
+
remove!(:command)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an anonymous command class to be used to initialize the received
|
53
|
+
# commander args.
|
54
|
+
def command
|
55
|
+
@command ||= Class.new.send(:include, Command)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds the received arguments to the executes array
|
59
|
+
def execute(*args)
|
60
|
+
executes.concat(args)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the executes array
|
64
|
+
def executes
|
65
|
+
@executes ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Can be used to set a default behavior of a Commander that overwrites
|
69
|
+
# call.
|
70
|
+
def abort_on_failure(value = true)
|
71
|
+
@abort_on_failure = value
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the abort_on_failure
|
75
|
+
def abort_on_failure?
|
76
|
+
@abort_on_failure
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
include Command
|
81
|
+
extend ClassMethods
|
82
|
+
|
83
|
+
# A commander works with the following instance variables:
|
84
|
+
# command: The last executed command. Will be an anonymous empty command at
|
85
|
+
# the beginning
|
86
|
+
# executed: An array containing the executed commands
|
87
|
+
def initialize(**args)
|
88
|
+
super(command: self.class.command.new(args), executed: [])
|
89
|
+
end
|
90
|
+
|
91
|
+
# This is the default call for a commander
|
92
|
+
# It basically just executes the commands saved in the executes array.
|
93
|
+
# This however can be overwritten by the user and define their own logic
|
94
|
+
# to execute different commands
|
95
|
+
def call
|
96
|
+
execute(*self.class.executes, abort_on_failure: true)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Execute will run the commands received, save the last executed command in
|
102
|
+
# @command instance variable and push it to the executed array.
|
103
|
+
#
|
104
|
+
# It also comes with an option to to abort the Commander in case one of the
|
105
|
+
# command run fails.
|
106
|
+
def execute(*commands, abort_on_failure: self.class.abort_on_failure?)
|
107
|
+
commands.each do |command_class|
|
108
|
+
args = command.to_hash
|
109
|
+
options = { skip_cleanup: :command, skip_validations: :receives }
|
110
|
+
@command = command_class.call(**args, **options)
|
111
|
+
executed.push(command)
|
112
|
+
|
113
|
+
if command.failure? && abort_on_failure
|
114
|
+
fail!
|
115
|
+
abort!
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# Core AyeCommander Error
|
3
|
+
class Error < RuntimeError
|
4
|
+
def initialize(info = nil)
|
5
|
+
@info = info
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Raised when command specifies 'requires' and one or more required arguments
|
10
|
+
# are missing when called.
|
11
|
+
class MissingRequiredArgumentError < Error
|
12
|
+
def message
|
13
|
+
"Missing required arguments: #{@info}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when the command specifies 'receives' and receives one or more
|
18
|
+
# unexpected arguments
|
19
|
+
class UnexpectedReceivedArgumentError < Error
|
20
|
+
def message
|
21
|
+
"Received unexpected arguments: #{@info}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# Hooks are available for all commands.
|
3
|
+
# They allow you to run specific parts of code before, around, after the
|
4
|
+
# command is called or when if the command was aborted
|
5
|
+
module Hookable
|
6
|
+
# All hook functionality is defined at a class level, but runs at instance
|
7
|
+
# level
|
8
|
+
module ClassMethods
|
9
|
+
TYPES = %i(before around after aborted).freeze
|
10
|
+
|
11
|
+
TYPES.each do |kind|
|
12
|
+
# Defines .before .around .after and .aborted
|
13
|
+
# Saves the received argument into their own array
|
14
|
+
#
|
15
|
+
# Options
|
16
|
+
# prepend: Makes it so that the received args are pushed to the front of
|
17
|
+
# the hook array instead of the end.
|
18
|
+
define_method kind do |*args, prepend: false, &block|
|
19
|
+
args.push block if block
|
20
|
+
if prepend
|
21
|
+
hooks[kind] = args + hooks[kind]
|
22
|
+
else
|
23
|
+
hooks[kind] += args
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Defines .before_hooks .around_hooks .after_hooks and .aborted_hooks
|
28
|
+
# Public interface in case the user wants to see their defined hooks
|
29
|
+
define_method "#{kind}_hooks" do
|
30
|
+
hooks[kind]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defines .call_before_hooks .call_around_hooks .call_after_hooks and
|
34
|
+
# .call_aborted_hooks
|
35
|
+
# Calls the hooks one by one
|
36
|
+
define_method "call_#{kind}_hooks" do |command|
|
37
|
+
prepare_hooks(kind, command).each(&:call)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Hash that saves the hooks
|
44
|
+
def hooks
|
45
|
+
@hooks ||= Hash.new([])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Prepares the hooks so they can just be called.
|
49
|
+
# Before after and around hooks are similar in the sense that they just
|
50
|
+
# need to make all the received hooks callable and then they call
|
51
|
+
# themselves.
|
52
|
+
#
|
53
|
+
# Arounds on the other hand... they basically wrap themselves in procs so
|
54
|
+
# that you can call the proc inside the proc that gives the proc.
|
55
|
+
# Quite a headache.
|
56
|
+
# Why would you need multiple around blocks in the first place?
|
57
|
+
def prepare_hooks(kind, command)
|
58
|
+
hooks = callable_hooks(kind, command)
|
59
|
+
return hooks unless kind == :around
|
60
|
+
|
61
|
+
around_proc = hooks.reverse.reduce(command) do |callable, hook|
|
62
|
+
-> { hook.call(callable) }
|
63
|
+
end
|
64
|
+
[around_proc]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Makes all the saved hooks callable in the command context
|
68
|
+
def callable_hooks(kind, command)
|
69
|
+
hooks[kind].map do |hook|
|
70
|
+
case hook
|
71
|
+
when Symbol
|
72
|
+
command.method(hook)
|
73
|
+
when Proc
|
74
|
+
->(*args) { command.instance_exec(*args, &hook) }
|
75
|
+
when Method
|
76
|
+
hook
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This module handles initialization of both a Command and a Result
|
3
|
+
module Initializable
|
4
|
+
# Initializes the command or Result with the correct setup
|
5
|
+
#
|
6
|
+
# When a command, the status is set based on the first succeeds saved in the
|
7
|
+
# class. In most cases this will be :success
|
8
|
+
#
|
9
|
+
# When a result, the status is sent in the initialization so it is in theory
|
10
|
+
# possible to have a result without a status, though not through this gem.
|
11
|
+
def initialize(**args)
|
12
|
+
@status = self.class.succeeds.first if self.class.respond_to?(:succeeds)
|
13
|
+
args.each do |name, value|
|
14
|
+
instance_variable_set to_ivar(name), value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This module handles methods that help a command instance represent its
|
3
|
+
# contents in different ways.
|
4
|
+
module Inspectable
|
5
|
+
# This inspect mimics the one ActiveModel uses so hopefully it will also
|
6
|
+
# look pretty during a pry session when the variables become too many.
|
7
|
+
def inspect
|
8
|
+
inspection = to_hash.map do |name, value|
|
9
|
+
"#{name}: #{value}"
|
10
|
+
end.compact.join(', ')
|
11
|
+
"#<#{self.class} #{inspection}>"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a hash of the specified instance_variables
|
15
|
+
# Defaults to returning all the currently existing instance variables
|
16
|
+
def to_hash(limit = instance_variables)
|
17
|
+
limit.each_with_object({}) do |iv, hash|
|
18
|
+
ivn = to_ivar(iv)
|
19
|
+
hash[ivn] = instance_variable_get(ivn)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a hash of only the instance variables that were specified by the
|
24
|
+
# .returns method.
|
25
|
+
#
|
26
|
+
# If no variables were specified then it becomes functionally identical to
|
27
|
+
# #to_hash
|
28
|
+
def to_result_hash
|
29
|
+
if self.class.respond_to?(:returns) && self.class.returns.any?
|
30
|
+
to_hash([:status] | self.class.returns)
|
31
|
+
else
|
32
|
+
to_hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module AyeCommander
|
2
|
+
# This module contains mostly methods related to method missing and instance
|
3
|
+
# variables
|
4
|
+
module Ivar
|
5
|
+
# Instance variable related class methods
|
6
|
+
module ClassMethods
|
7
|
+
# Adds the received reader to the class.
|
8
|
+
# It prefers using 'uses' it available (command), but will use attr_reader
|
9
|
+
# if it isn't (result).
|
10
|
+
def define_missing_reader(reader)
|
11
|
+
respond_to?(:uses) ? uses(reader) : attr_reader(reader)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Transforms the received name to instance variable form
|
15
|
+
# Eg: command -> @command
|
16
|
+
def to_ivar(name)
|
17
|
+
name[0] == '@' ? name.to_sym : "@#{name}".to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
# Transforms the received name to normal variable form
|
21
|
+
# Eg: @command -> command
|
22
|
+
def to_nvar(name)
|
23
|
+
name[0] == '@' ? name[1..-1].to_sym : name.to_sym
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Helps a command and result respond to read methods of instance variables
|
28
|
+
# This functionality is divided into two different modules since commander
|
29
|
+
# includes both, but result only includes Readable
|
30
|
+
module Readable
|
31
|
+
# A command will only respond to a read instance variable if it receives
|
32
|
+
# a valid instance variable name that is already defined within the
|
33
|
+
# command or result.
|
34
|
+
def method_missing(name, *args)
|
35
|
+
var_name = to_ivar(name)
|
36
|
+
if instance_variable_defined? var_name
|
37
|
+
self.class.define_missing_reader(name)
|
38
|
+
instance_variable_get var_name
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
rescue NameError
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# This helps remove an instance variable name from the current command.
|
47
|
+
# Consider using the .returns method instead.
|
48
|
+
def remove!(name)
|
49
|
+
remove_instance_variable to_ivar(name)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Transforms the received name to instance variable form
|
53
|
+
def to_ivar(name)
|
54
|
+
self.class.to_ivar(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Transforms the received name to normal variable form
|
58
|
+
def to_nvar(name)
|
59
|
+
self.class.to_nvar(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def respond_to_missing?(name, *args)
|
65
|
+
instance_variable_defined?(to_ivar(name)) || super
|
66
|
+
rescue NameError
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Helps a command respond to methods that would be writers
|
72
|
+
module Writeable
|
73
|
+
# Any method that ends with an equal sign will be able to be handled by
|
74
|
+
# this method missing.
|
75
|
+
def method_missing(name, *args)
|
76
|
+
if name[-1] == '='
|
77
|
+
var_name = to_ivar(name[0...-1])
|
78
|
+
instance_variable_set var_name, args.first
|
79
|
+
self.class.uses name[0...-1]
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
rescue NameError
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def respond_to_missing?(name, *args)
|
90
|
+
name[-1] == '=' || super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|