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