rototiller 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +13 -5
  2. data/CONTRIBUTING.md +57 -0
  3. data/MAINTAINERS +23 -0
  4. data/README.md +177 -134
  5. data/docs/Rakefile.example +101 -0
  6. data/docs/env_var_example_reference.md +110 -0
  7. data/docs/rototiller_class_graph.png +0 -0
  8. data/docs/rototiller_task_reference.md +212 -0
  9. data/lib/rototiller/rake/dsl/dsl_extention.rb +4 -0
  10. data/lib/rototiller/task/collections/argument_collection.rb +15 -0
  11. data/lib/rototiller/task/collections/command_collection.rb +15 -0
  12. data/lib/rototiller/task/collections/env_collection.rb +27 -0
  13. data/lib/rototiller/task/collections/option_collection.rb +20 -0
  14. data/lib/rototiller/task/collections/param_collection.rb +66 -0
  15. data/lib/rototiller/task/collections/switch_collection.rb +20 -0
  16. data/lib/rototiller/task/hash_handling.rb +30 -0
  17. data/lib/rototiller/task/params.rb +24 -0
  18. data/lib/rototiller/task/params/argument.rb +9 -0
  19. data/lib/rototiller/task/params/command.rb +209 -0
  20. data/lib/rototiller/task/params/env_var.rb +118 -0
  21. data/lib/rototiller/task/params/option.rb +63 -0
  22. data/lib/rototiller/task/params/switch.rb +81 -0
  23. data/lib/rototiller/task/rototiller_task.rb +68 -90
  24. data/lib/rototiller/utilities/color_text.rb +28 -26
  25. data/lib/rototiller/version.rb +1 -1
  26. metadata +31 -26
  27. data/Gemfile +0 -35
  28. data/Gemfile.lock +0 -280
  29. data/Rakefile +0 -72
  30. data/Rakefile.bak +0 -72
  31. data/lib/rototiller/task/flags/cli_flags.rb +0 -7
  32. data/lib/rototiller/utilities/block_syntax_object.rb +0 -16
  33. data/lib/rototiller/utilities/command.rb +0 -43
  34. data/lib/rototiller/utilities/command_flag.rb +0 -123
  35. data/lib/rototiller/utilities/env_collection.rb +0 -10
  36. data/lib/rototiller/utilities/env_var.rb +0 -130
  37. data/lib/rototiller/utilities/flag_collection.rb +0 -28
  38. data/lib/rototiller/utilities/param_collection.rb +0 -73
  39. data/spec/spec_helper.rb +0 -41
@@ -0,0 +1,15 @@
1
+ require 'rototiller/task/collections/param_collection'
2
+ require 'rototiller/task/params/command'
3
+
4
+ module Rototiller
5
+ module Task
6
+
7
+ class CommandCollection < ParamCollection
8
+ # @return [Type] allowed class for this collection (Command)
9
+ def allowed_class
10
+ Command
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ require 'rototiller/task/collections/param_collection'
2
+ require 'rototiller/task/params/env_var'
3
+
4
+ module Rototiller
5
+ module Task
6
+
7
+ class EnvCollection < ParamCollection
8
+
9
+ # @return [Type] allowed class for this collection (EnvVar)
10
+ def allowed_class
11
+ EnvVar
12
+ end
13
+
14
+ # remove the nils and return the last known value
15
+ # @return [String] last set environment variable or default
16
+ def last
17
+ if self.any?
18
+ last_known_env_var = self.map{|x| x.value}.compact.last
19
+ # ruby converts nil to "", so guard against single non-set env vars here
20
+ last_known_env_var.to_s if last_known_env_var
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ require 'rototiller/task/collections/param_collection'
2
+ require 'rototiller/task/params/option'
3
+
4
+ module Rototiller
5
+ module Task
6
+
7
+ # The OptionCollection class to collect more than one option for a Command
8
+ # delegates to Array via inheritance from ParamCollection
9
+ # @since v1.0.0
10
+ class OptionCollection < ParamCollection
11
+
12
+ # set allowed classes to be inserted into this Collection/Array
13
+ # @return [Option] the collection's allowed class
14
+ def allowed_class
15
+ Option
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ require 'forwardable'
2
+
3
+ module Rototiller
4
+ module Task
5
+
6
+ # The base ParamCollection class to collect more than one parameter for a task, or other parameters
7
+ # delegates to Array for most of Array's methods
8
+ # @since v0.1.0
9
+ class ParamCollection
10
+
11
+ extend Forwardable
12
+
13
+ def_delegators :@collection, :clear, :delete_if, :include?, :include, :inspect, :each, :[], :map, :any?, :compact
14
+
15
+ # setup the collection as a composed Array
16
+ # @return the collection
17
+ def initialize
18
+ @collection = []
19
+ end
20
+
21
+ # push to the collection
22
+ # @param [Param] args instances of the child classes allowed_class
23
+ # @return the new collection
24
+ def push(*args)
25
+ check_classes(allowed_class, *args)
26
+ @collection.push(*args)
27
+ end
28
+
29
+ # format the messages inside this ParamCollection
30
+ # @return [String] messages from the contents of this ParamCollection
31
+ def messages
32
+ @collection.map { |param| param.message }.join('')
33
+ end
34
+
35
+ # Do any of the contents of this ParamCollection require the task to stop
36
+ # @return [true, nil] should the values of this ParamCollection stop the task
37
+ def stop?
38
+ @collection.any?{ |param| param.stop }
39
+ end
40
+
41
+ # convert a ParamCollection to a string
42
+ # the value sent by author, or overridden by any EnvVar
43
+ # @return [String] the Param's value
44
+ def to_str
45
+ @collection.join(' ') unless @collection.empty?
46
+ end
47
+ alias :to_s :to_str
48
+
49
+ private
50
+
51
+ #@private
52
+ def check_classes(allowed_klass, *args)
53
+
54
+ args.each do |arg|
55
+
56
+ unless arg.is_a?(allowed_klass)
57
+ argument_error = "Argument was of class #{arg.class}, Can only be of class #{allowed_klass}"
58
+ raise(ArgumentError, argument_error)
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ require 'rototiller/task/collections/param_collection'
2
+ require 'rototiller/task/params/switch'
3
+
4
+ module Rototiller
5
+ module Task
6
+
7
+ # The SwitchCollection class to collect more than one switch for a Command
8
+ # delegates to Array via inheritance from ParamCollection
9
+ # @since v1.0.0
10
+ class SwitchCollection < ParamCollection
11
+
12
+ # set allowed classes to be inserted into this Collection/Array
13
+ # @return [Switch] the collection's allowed class
14
+ def allowed_class
15
+ Switch
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ module Rototiller
2
+ module Task
3
+ module HashHandling
4
+
5
+ # equates methods to keys inside a hash or an array of hashes
6
+ # @param [Hash] hash attempt to use keys as setter or getter methods on self
7
+ # @raise [ArgumentError] if a key is not a valid method on self
8
+ def send_hash_keys_as_methods_to_self(hash)
9
+
10
+ hash = [hash].flatten
11
+ hash.each do |h|
12
+ raise ArgumentError unless h.is_a?(Hash)
13
+ h.each do |k, v|
14
+
15
+ method_list = self.methods
16
+
17
+ if method_list.include?(k) && method_list.include?("#{k}=".to_sym)
18
+ # methods that have attr_accesors
19
+ self.send("#{k}=", v)
20
+ elsif method_list.include?(k)
21
+ self.send(k,v)
22
+ else
23
+ raise ArgumentError.new("'#{k}' is not a valid key: #{self.class}")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ require 'rototiller/task/hash_handling'
2
+
3
+ module Rototiller
4
+ module Task
5
+
6
+ # The base class for creating rototiller task params (commands, envs, etc)
7
+ # @since v0.1.0
8
+ # @attr [String] name The name of the param
9
+ # @attr [String] message The param message (for debugging/informing/logging)
10
+ class RototillerParam
11
+ include HashHandling
12
+
13
+ attr_accessor :name
14
+ attr_accessor :message
15
+
16
+ # we must always have a message that can be aggregated via the parent params
17
+ # @return [String] <empty string>
18
+ def message
19
+ return ''
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ require 'rototiller/task/params'
2
+
3
+ module Rototiller
4
+ module Task
5
+
6
+ Argument = Switch
7
+
8
+ end
9
+ end
@@ -0,0 +1,209 @@
1
+ require 'open3'
2
+ require 'rototiller/task/collections/env_collection'
3
+ require 'rototiller/task/collections/switch_collection'
4
+ require 'rototiller/task/collections/option_collection'
5
+ require 'rototiller/task/collections/argument_collection'
6
+
7
+ module Rototiller
8
+ module Task
9
+
10
+ # The Command class to implement rototiller command handling
11
+ # via a RototillerTask's #add_command
12
+ # @since v0.1.0
13
+ # @attr [String] name The name of the command to run
14
+ # @attr_reader [Struct] result A structured command result
15
+ # contains members: output, exit_code and pid (from Open3.popen2e)
16
+ class Command < RototillerParam
17
+
18
+ # @return [String] the command to be used, could be considered a default
19
+ attr_accessor :name
20
+
21
+ # @return [Struct] the command results, if run
22
+ attr_reader :result
23
+
24
+ # Creates a new instance of Command, holds information about desired state of a command
25
+ # @param [Hash,Array<Hash>] args hashes of information about the command
26
+ # for block { |b| ... }
27
+ # @yield Command object with attributes matching method calls supported by Command
28
+ # @return Command object
29
+ def initialize(args={}, &block)
30
+ # the env_vars that override the command name
31
+ @env_vars = EnvCollection.new
32
+ @switches = SwitchCollection.new
33
+ @options = OptionCollection.new
34
+ @arguments = ArgumentCollection.new
35
+
36
+ block_given? ? (yield self) : send_hash_keys_as_methods_to_self(args)
37
+ # @name is the default unless @env_vars returns something truthy
38
+ (@name = @env_vars.last) if @env_vars.last
39
+ end
40
+
41
+ # adds environment variables to be tracked, messaged.
42
+ # In the Command context this env_var overrides the command "name"
43
+ # @param [Hash] args hashes of information about the environment variable
44
+ # @option args [String] :name The environment variable
45
+ # @option args [String] :default The default value for the environment variable
46
+ # this is optional and defaults to the parent's `:name`
47
+ # @option args [String] :message A message describing the use of this variable
48
+ #
49
+ # for block {|a| ... }
50
+ # @yield [a] Optional block syntax allows you to specify information about the environment variable, available methods match hash keys described above
51
+ def add_env(*args, &block)
52
+ raise ArgumentError.new("#{__method__} takes a block or a hash") if !args.empty? && block_given?
53
+ # this is kinda annoying we have to do this for all params? (not DRY)
54
+ # have to do it this way so EnvVar doesn't become a collection
55
+ # but if this gets moved to a mixin, it might be more tolerable
56
+ if block_given?
57
+ # send in the name of this Param, so it can be used when no default is given to add_env
58
+ @env_vars.push(EnvVar.new({:parent_name => @name},&block))
59
+ else
60
+ #TODO: test this with array and non-array single hash
61
+ args.each do |arg| # we can accept an array of hashes, each of which defines a param
62
+ error_string = "#{__method__} takes an Array of Hashes. Received Array of: '#{arg.class}'"
63
+ raise ArgumentError.new(error_string) unless arg.is_a?(Hash)
64
+ # send in the name of this Param, so it can be used when no default is given to add_env
65
+ arg[:parent_name] = @name
66
+ @env_vars.push(EnvVar.new(arg))
67
+ end
68
+ end
69
+ @name = @env_vars.last if @env_vars.last
70
+ end
71
+
72
+ # adds switch(es) (binary option flags) to this Command instance with optional env_var overrides
73
+ # @param [Hash] args hashes of information about the switch
74
+ # @option args [String] :name The switch string, including any '-', '--', etc
75
+ # @option args [String] :message A message describing the use of this variable
76
+ #
77
+ # for block {|a| ... }
78
+ # @yield [a] Optional block syntax allows you to specify information about the environment variable, available methods match hash keys described above
79
+ def add_switch(*args, &block)
80
+ raise ArgumentError.new("#{__method__} takes a block or a hash") if !args.empty? && block_given?
81
+ # this is kinda annoying we have to do this for all params? (not DRY)
82
+ # have to do it this way so EnvVar doesn't become a collection
83
+ # but if this gets moved to a mixin, it might be more tolerable
84
+ if block_given?
85
+ @switches.push(Switch.new(&block))
86
+ else
87
+ #TODO: test this with array and non-array single hash
88
+ args.each do |arg| # we can accept an array of hashes, each of which defines a param
89
+ error_string = "#{__method__} takes an Array of Hashes. Received Array of: '#{arg.class}'"
90
+ raise ArgumentError.new(error_string) unless arg.is_a?(Hash)
91
+ @switches.push(Switch.new(arg))
92
+ end
93
+ end
94
+ end
95
+
96
+ # adds option to append to command (a switch with an argument)
97
+ # add_option creates an option object which has its own `#add_env`, `#add_argument` methods
98
+ # @param [Hash] args hashes of information about the option
99
+ # @option args [String] :name The value to be used as the option
100
+ # @option args [String] :message A message describing the use of option
101
+ #
102
+ # for block {|a| ... }
103
+ # @yield [a] Optional block syntax allows you to specify information about the option, available methods match hash keys
104
+ def add_option(*args, &block)
105
+ raise ArgumentError.new("#{__method__} takes a block or a hash") if !args.empty? && block_given?
106
+ # this is kinda annoying we have to do this for all params? (not DRY)
107
+ # have to do it this way so EnvVar doesn't become a collection
108
+ # but if this gets moved to a mixin, it might be more tolerable
109
+ if block_given?
110
+ @options.push(Option.new(&block))
111
+ else
112
+ #TODO: test this with array and non-array single hash
113
+ args.each do |arg| # we can accept an array of hashes, each of which defines a param
114
+ error_string = "#{__method__} takes an Array of Hashes. Received Array of: '#{arg.class}'"
115
+ raise ArgumentError.new(error_string) unless arg.is_a?(Hash)
116
+ @options.push(Option.new(arg))
117
+ end
118
+ end
119
+ end
120
+
121
+ # adds argument to append to command.
122
+ # In the Command context this Argument is added to the end of the command string
123
+ # @param [Hash] args hashes of information about the argument
124
+ # @option args [String] :name The value to be used as the argument
125
+ # @option args [String] :message A message describing the use of argument
126
+ #
127
+ # for block {|a| ... }
128
+ # @yield [a] Optional block syntax allows you to specify information about the option, available methods match hash keys
129
+ def add_argument(*args, &block)
130
+ raise ArgumentError.new("#{__method__} takes a block or a hash") if !args.empty? && block_given?
131
+ if block_given?
132
+ @arguments.push(Argument.new(&block))
133
+ else
134
+ args.each do |arg| # we can accept an array of hashes, each of which defines a param
135
+ error_string = "#{__method__} takes an Array of Hashes. Received Array of: '#{arg.class}'"
136
+ raise ArgumentError.new(error_string) unless arg.is_a?(Hash)
137
+ @arguments.push(Argument.new(arg))
138
+ end
139
+ end
140
+ end
141
+
142
+ # convert a Command object to a string (runable command string)
143
+ # @return [String] the current value of the command string as built from its params
144
+ # TODO make private method? so that it will throw an error if yielded to?
145
+ def to_str
146
+ delete_nil_empty_false([
147
+ (name if name),
148
+ @switches.to_s,
149
+ @options.to_s,
150
+ @arguments.to_s
151
+ ]).join(' ').to_s
152
+ end
153
+ alias :to_s :to_str
154
+
155
+ Result = Struct.new(:output, :exit_code, :pid)
156
+ # run Command locally, capture relevent result data
157
+ # @return [Struct<Result>] a Result Struct with stdout, stderr, exit_code members
158
+ # TODO make private method? so that it will throw an error if yielded to?
159
+ def run
160
+ # make this look a bit like beaker's result class
161
+ # we may have to convert this to a class if it gets complex
162
+ @result = Result.new
163
+ @result.output = ''
164
+ # add ';' to command string as it is a metacharacter that forces open3
165
+ # to send the command to the shell. This returns the correct
166
+ # exit_code and stderr, etc when the command is not found
167
+ Open3.popen2e(self.to_str + ";"){|stdin, stdout_err, wait_thr|
168
+ stdout_err.each { |line| puts line
169
+ @result.output << line }
170
+ @result.pid = wait_thr.pid # pid of the started process.
171
+ @result.exit_code = wait_thr.value.exitstatus # Process::Status object returned.
172
+ }
173
+
174
+ if block_given? # if block, send result to the block
175
+ yield @result
176
+ end
177
+ @result
178
+ end
179
+
180
+ # Does this param require the task to stop
181
+ # Determined by the interactions between @name, @env_vars, @options, @switches, @arguments
182
+ # @return [true|nil] if this param requires a stop
183
+ # TODO make private method? so that it will throw an error if yielded to?
184
+ def stop
185
+ return true if [@switches, @options, @arguments].any?{ |collection| collection.stop? }
186
+ return true unless @name
187
+ end
188
+
189
+ # @return [String] formatted messages from all of Command's pieces
190
+ # itself, env_vars, switches, options, arguments
191
+ # TODO make private method? so that it will throw an error if yielded to?
192
+ def message
193
+ return [@message,
194
+ @env_vars.messages,
195
+ @switches.messages,
196
+ @options.messages,
197
+ @arguments.messages,
198
+ ].join('')
199
+ end
200
+
201
+ private
202
+ # @private
203
+ def delete_nil_empty_false(arg)
204
+ arg.delete_if{ |i| ([nil, '', false].include?(i)) }
205
+ end
206
+ end
207
+
208
+ end
209
+ end
@@ -0,0 +1,118 @@
1
+ require 'rototiller/task/params'
2
+ require 'rototiller/utilities/color_text'
3
+
4
+ module Rototiller
5
+ module Task
6
+
7
+ # The main EnvVar type to implement envrironment variable handling
8
+ # contains its messaging, status, and whether it is required.
9
+ # The rototiller Param using it knows what to do with its value.
10
+ # @since v0.1.0
11
+ # @attr [String] default The default value of this env_var to use. if we have a default and
12
+ # the system ENV does not have a value this implies the env_var is not required. If not default is specified but the parent parameter has a `#name` then that name is used as the default.
13
+ # Used internally by CommandFlag, ignored for standalone EnvVar.
14
+ # @attr_reader [Boolean] stop Whether the state of the EnvVar requires the task to stop
15
+ # @attr_reader [Boolean] value The value of the ENV based on specified default and environment state
16
+ class EnvVar < RototillerParam
17
+ include Rototiller::ColorText
18
+ STATUS = {:nodefault_noexist=>0, :nodefault_exist=>1, :default_noexist=>2, :default_exist=>3}
19
+
20
+ attr_accessor :name
21
+ attr_accessor :default
22
+ attr_accessor :message
23
+ attr_reader :value
24
+ attr_reader :stop
25
+
26
+ # Creates a new instance of EnvVar, holds information about the ENV in the environment
27
+ # @param [Hash, Array<Hash>] args hash of information about the environment variable
28
+ # @option args [String] :name The environment variable
29
+ # @option args [String] :default The default value for the environment variable
30
+ # @option args [String] :message A message describing the use of this variable
31
+ # for block { |b| ... }
32
+ # @yield EnvVar object with attributes matching method calls supported by EnvVar
33
+ # @return EnvVar object
34
+ def initialize(args={}, &block)
35
+ @parent_name = args[:parent_name]
36
+ args.delete(:parent_name)
37
+ block_given? ? (yield self) : send_hash_keys_as_methods_to_self(args)
38
+
39
+ raise(ArgumentError, 'A name must be supplied to add_env') unless @name
40
+ @env_value_set_by_us = false
41
+ reset
42
+ end
43
+
44
+ # The formatted messages about this EnvVar's status to be displayed to the user
45
+ # @return [String] the EnvVar's message, formatted for color and meaningful to the state of the EnvVar
46
+ def message
47
+ this_message = String.new
48
+
49
+ if env_status == STATUS[:nodefault_noexist]
50
+ this_message << red_text('ERROR: environment-variable not set and no default provided: ')
51
+ this_message << "'#{@name}': '#{@message}'\n"
52
+ elsif env_status == STATUS[:nodefault_exist]
53
+ this_message << yellow_text('INFO: using system environment-variable value, no default provided: ')
54
+ this_message << "'#{@name}': '#{@value}': '#{@message}'\n"
55
+ elsif env_status == STATUS[:default_noexist]
56
+ this_message << green_text('INFO: no system environment-variable value, using default provided: ')
57
+ this_message << "'#{@name}': '#{@value}': '#{@message}'\n"
58
+ elsif env_status == STATUS[:default_exist]
59
+ this_message << yellow_text('INFO: environment-variable overridden from system, not using default: ')
60
+ this_message << "'#{@name}': default: '#{@default}' using: '#{@value}': '#{@message}'\n"
61
+ end
62
+
63
+ end
64
+
65
+ # The string representation of this EnvVar; the value on the system, or nil
66
+ # @return [String] the EnvVar's value
67
+ def to_str
68
+ @value
69
+ end
70
+ alias :to_s :to_str
71
+
72
+ # Sets the name of the EnvVar
73
+ # @raise [ArgumentError] if name contains an illegal character for bash environment variable
74
+ def name=(name)
75
+ name.each_char do |char|
76
+ message = "You have defined an environment variable with an illegal character: #{char}"
77
+ raise ArgumentError.new(message) unless char =~ /[a-zA-Z]|\d|_/
78
+ end
79
+ @name = name
80
+ end
81
+
82
+ private
83
+
84
+ # @private
85
+ def reset
86
+ # if no default given, use parent param's name
87
+ @default ||= @parent_name
88
+ (env_value_provided_by_user? || @default) ? @stop = false : @stop = true
89
+
90
+ if @name
91
+ @value = ENV[@name] || @default
92
+ unless env_value_provided_by_user?
93
+ ENV[@name] = @value
94
+ @env_value_set_by_us = true
95
+ end
96
+ else
97
+ @value = @default
98
+ end
99
+ end
100
+
101
+ # @private
102
+ def env_value_provided_by_user?
103
+ # its possible that name could not be set
104
+ (ENV.key?(@name) if @name) ? true : false
105
+ end
106
+
107
+ # @private
108
+ def env_status
109
+ return STATUS[:nodefault_noexist] if !@default && @env_value_set_by_us
110
+ return STATUS[:nodefault_exist] if !@default && !@env_value_set_by_us
111
+ return STATUS[:default_noexist] if @default && @env_value_set_by_us
112
+ return STATUS[:default_exist] if @default && !@env_value_set_by_us
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end