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,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
|