aye_commander 1.0.0

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