golly-utils 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +8 -0
  3. data/.simplecov +14 -0
  4. data/.travis.yml +5 -0
  5. data/.yardopts +6 -0
  6. data/CHANGELOG.md +10 -0
  7. data/Gemfile +12 -0
  8. data/Gemfile.corvid +27 -0
  9. data/Gemfile.lock +86 -0
  10. data/Guardfile +44 -0
  11. data/README.md +3 -0
  12. data/RELEASE.md +13 -0
  13. data/Rakefile +6 -0
  14. data/bin/guard +16 -0
  15. data/bin/rake +16 -0
  16. data/bin/rspec +16 -0
  17. data/bin/yard +16 -0
  18. data/bin/yardoc +16 -0
  19. data/bin/yri +16 -0
  20. data/golly-utils.gemspec +17 -0
  21. data/lib/golly-utils/attr_declarative.rb +74 -0
  22. data/lib/golly-utils/callbacks.rb +92 -0
  23. data/lib/golly-utils/child_process.rb +124 -0
  24. data/lib/golly-utils/colourer.rb +50 -0
  25. data/lib/golly-utils/delegator.rb +81 -0
  26. data/lib/golly-utils/multi_io.rb +13 -0
  27. data/lib/golly-utils/ruby_ext.rb +2 -0
  28. data/lib/golly-utils/ruby_ext/array_to_hash.rb +29 -0
  29. data/lib/golly-utils/ruby_ext/deep_dup.rb +33 -0
  30. data/lib/golly-utils/ruby_ext/env_helpers.rb +27 -0
  31. data/lib/golly-utils/ruby_ext/hash_combinations.rb +49 -0
  32. data/lib/golly-utils/ruby_ext/pretty_error_messages.rb +9 -0
  33. data/lib/golly-utils/ruby_ext/subclasses.rb +17 -0
  34. data/lib/golly-utils/test/spec/deferrable_specs.rb +85 -0
  35. data/lib/golly-utils/test/spec/within_time.rb +56 -0
  36. data/lib/golly-utils/version.rb +3 -0
  37. data/test/bootstrap/all.rb +4 -0
  38. data/test/bootstrap/spec.rb +5 -0
  39. data/test/bootstrap/unit.rb +4 -0
  40. data/test/spec/child_process_mock_target.rb +28 -0
  41. data/test/spec/child_process_spec.rb +41 -0
  42. data/test/unit/attr_declarative_test.rb +54 -0
  43. data/test/unit/callbacks_test.rb +76 -0
  44. data/test/unit/delegator_test.rb +99 -0
  45. data/test/unit/multi_io_test.rb +18 -0
  46. data/test/unit/ruby_ext/env_helpers_test.rb +48 -0
  47. data/test/unit/ruby_ext/hash_combinations_test.rb +31 -0
  48. data/test/unit/ruby_ext/subclasses_test.rb +24 -0
  49. metadata +107 -0
@@ -0,0 +1,92 @@
1
+ require 'golly-utils/ruby_ext/deep_dup'
2
+
3
+ module GollyUtils
4
+ module Callbacks
5
+
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ base.send :include, InstanceAndClassMethods
9
+ base.extend InstanceAndClassMethods
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ #-------------------------------------------------------------------------------------------------------------------
14
+
15
+ module ClassMethods
16
+
17
+ def define_callbacks(*callbacks)
18
+ callbacks.each do |name|
19
+ name= _norm_callback_key(name)
20
+
21
+ if self.methods.include?(name.to_sym)
22
+ raise "Can't create callback with name '#{name}'. A method with that name already exists."
23
+ end
24
+
25
+ _callbacks[name] ||= {}
26
+ class_eval <<-EOB
27
+ def self.#{name}(&block)
28
+ v= (_callbacks[#{name.inspect}] ||= {})
29
+ (v[:procs] ||= [])<< block
30
+ end
31
+ EOB
32
+ end
33
+ end
34
+ alias :define_callback :define_callbacks
35
+
36
+ private
37
+
38
+ def _callbacks
39
+ @callbacks ||= {}
40
+ end
41
+
42
+ def _get_callback_procs(name)
43
+ name_verified= false
44
+ results= []
45
+
46
+ # Get local
47
+ if local= _callbacks[name]
48
+ name_verified= true
49
+ results.concat local[:procs] if local[:procs]
50
+ end
51
+
52
+ # Get inherited
53
+ if superclass.private_methods.include?(:_get_callback_procs)
54
+ n,r = superclass.send(:_get_callback_procs,name)
55
+ name_verified ||= n
56
+ results.concat r
57
+ end
58
+
59
+ [name_verified,results]
60
+ end
61
+
62
+ end
63
+
64
+ #-------------------------------------------------------------------------------------------------------------------
65
+
66
+ module InstanceAndClassMethods
67
+
68
+ private
69
+ def _norm_callback_key(key)
70
+ key.to_sym
71
+ end
72
+
73
+ end
74
+
75
+ #-------------------------------------------------------------------------------------------------------------------
76
+
77
+ module InstanceMethods
78
+
79
+ def run_callbacks(*callbacks)
80
+ callbacks.each do |name|
81
+ name= _norm_callback_key(name)
82
+ name_verified,results = self.class.send :_get_callback_procs, name
83
+ raise "There is no callback defined with name #{name}." unless name_verified
84
+ results.each{|cb| cb.call }
85
+ end
86
+ true
87
+ end
88
+ alias :run_callback :run_callbacks
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,124 @@
1
+ module GollyUtils
2
+
3
+ # Start, manage, and stop a child process.
4
+ class ChildProcess
5
+ attr_accessor :start_command, :quiet, :spawn_options, :env
6
+ attr_reader :pid
7
+ alias :quiet? :quiet
8
+
9
+ # @option options [String] :start_command The shell command to start the child process.
10
+ # @option options [Hash] :env Environment variables to set in the child process.
11
+ # @option options [Boolean] :quiet (false) Whether to print startup/shutdown info to stdout, and whether or not to
12
+ # the stdout and stderr streams of the child process (unless explictly redirected via :spawn_options)
13
+ # @option options [Hash] :spawn_options Options to pass to Process#spawn.
14
+ def initialize(options={})
15
+ options= {env: {}, quiet: false, spawn_options: {}}.merge(options)
16
+ options[:spawn_options][:in] ||= '/dev/null'
17
+ options.each {|k,v| send "#{k}=", v}
18
+ end
19
+
20
+ # Starts the child process.
21
+ #
22
+ # If it is already running, then this will do nothing.
23
+ #
24
+ # @return @self@
25
+ def startup
26
+ unless alive?
27
+ opt= self.spawn_options
28
+ if quiet?
29
+ opt= opt.dup
30
+ mute= [:out,:err] - opt.keys.flatten
31
+ mute.each {|fd| opt[fd] ||= '/dev/null'}
32
+ end
33
+ unless quiet?
34
+ e= ''
35
+ env.each{|k,v| e+="#{k}=#{v} "} if env
36
+ puts "> #{e}#{start_command}"
37
+ end
38
+ @pid= spawn env, start_command, opt
39
+ Process.detach @pid
40
+ puts "< Spawned process #@pid" unless quiet?
41
+ end
42
+ self
43
+ end
44
+
45
+ # Stops the child process.
46
+ #
47
+ # If it is already running, then this will do nothing.
48
+ #
49
+ # @return [Boolean] Boolean indicating whether shutdown was successful and process is down.
50
+ def shutdown
51
+ if alive?
52
+ t= Thread.new{ attempt_kill }
53
+ if quiet?
54
+ t.join
55
+ else
56
+ puts "Stopping process #@pid..."
57
+ t.join
58
+ end
59
+ end
60
+ !alive?
61
+ end
62
+
63
+ # Checks if the process [*previously started by this class*] is still alive.
64
+ #
65
+ # If it is determined that the process is no longer alive then the internal {#pid PID} is cleared.
66
+ #
67
+ # @return [Boolean]
68
+ def alive?
69
+ return false if @pid.nil?
70
+ alive= begin
71
+ Process.getpgid(@pid) != -1
72
+ rescue Errno::ESRCH
73
+ false
74
+ end
75
+ @pid= nil unless alive
76
+ alive
77
+ end
78
+
79
+ protected
80
+
81
+ def kill_signals
82
+ [
83
+ ['TERM', 6],
84
+ ['QUIT', 6],
85
+ ['INT' , 6],
86
+ ['KILL', 1],
87
+ ]
88
+ end
89
+
90
+ private
91
+
92
+ def attempt_kill
93
+ kill_signals.each do |signal, wait_time|
94
+ Process.kill(signal, @pid)
95
+ start_time= Time.now
96
+ sleep 0.1 while alive? and (Time.now - start_time) < wait_time
97
+ break unless alive?
98
+ end
99
+ if alive?
100
+ STDERR.puts "Failed to kill process, PID #@pid"
101
+ end
102
+ end
103
+
104
+ # --------------------------------------------------------------------------------------------------------------------
105
+ class << self
106
+ OPTION_TRANSLATION= {
107
+ stdout: :out,
108
+ stderr: :err,
109
+ stdin: :in,
110
+ }.freeze
111
+
112
+ def translate_options(prefix='')
113
+ spawn_opts= {}
114
+ OPTION_TRANSLATION.each do |from,to|
115
+ if v= ENV["#{prefix}#{from}"] and !v.empty?
116
+ spawn_opts[to]= v
117
+ end
118
+ end
119
+ spawn_opts
120
+ end
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ module GollyUtils
2
+
3
+ # Helps decorate text with ANSI colour codes.
4
+ class Colourer
5
+ attr_reader :output
6
+
7
+ # @param output The target IO object that text will be written to.
8
+ def initialize(output)
9
+ @output= output
10
+ end
11
+
12
+ def color_enabled?
13
+ @color ||= output_to_tty?
14
+ end
15
+
16
+ def color_enabled=(bool)
17
+ # Ungracefully ripped out of RSpec.
18
+ return unless bool
19
+ @color = true
20
+ if bool && ::RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
21
+ unless ENV['ANSICON']
22
+ warn "You must use ANSICON 1.31 or later (http://adoxa.110mb.com/ansicon/) to use colour on Windows"
23
+ @color = false
24
+ end
25
+ end
26
+ end
27
+
28
+ # Wraps text in a given colour code (with a colour-clear code on the end) if colours are enabled.
29
+ # @return [String]
30
+ def add_color(text, color_code)
31
+ color_enabled? ? "#{color_code}#{text}\e[0m" : text
32
+ end
33
+
34
+ # Calls `puts` on the output stream with optionally coloured text.
35
+ def puts(text, color_code)
36
+ output.puts add_color(text, color_code)
37
+ end
38
+
39
+ private
40
+
41
+ def output_to_tty?
42
+ begin
43
+ output.tty?
44
+ rescue NoMethodError
45
+ false
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,81 @@
1
+ module GollyUtils
2
+
3
+ # An object that delegates method calls to eligible delegate objects.
4
+ class Delegator
5
+ attr_reader :delegate_to
6
+
7
+ # @overload initialize(*delegates, options={})
8
+ # @param [Object] delegates Objects that method calls may be delegated to.
9
+ # @param [Hash] options
10
+ # @option options [true,false] :cache (true) Whether or not to maintain a cache of which delegate objects can
11
+ # respond to each method call.
12
+ # @option options [:first,:all] :delegate_to (:first) When multiple delegates can respond to a method call, this
13
+ # setting determines which object(s) are delegated to.
14
+ # @option options [String, Symbol, Regexp, Array] :method_whitelist Method name matcher(s) that specify which
15
+ # methods are allowed to be delegated.
16
+ # @option options [String, Symbol, Regexp, Array] :method_blacklist Method name matcher(s) that specify methods
17
+ # that are not allowed to be delegated.
18
+ def initialize(*args)
19
+ options= args.last.kind_of?(Hash) ? args.pop.clone : {}
20
+ @original_options= options
21
+
22
+ @delegates= args
23
+ @delegate_to= options[:delegate_to] || :first
24
+ @cache= {} unless options.has_key?(:cache) && !options[:cache]
25
+ parse_method_delegation_option options, :method_whitelist
26
+ parse_method_delegation_option options, :method_blacklist
27
+ end
28
+
29
+ def dup; Delegator.new @delegates.map(&:dup), @original_options end
30
+ def clone; Delegator.new @delegates.map(&:clone), @original_options end
31
+
32
+ def method_missing(method, *args)
33
+ matches= delegates_that_respond_to(method)
34
+ return super(method,*args) if matches.empty?
35
+
36
+ case delegate_to
37
+ when :first
38
+ matches[0].public_send(method,*args)
39
+ when :all
40
+ matches.map{|m| m.public_send(method,*args)}
41
+ else
42
+ raise "Don't know how to respond to :delegate_to value of #{delegate_to.inspect}"
43
+ end
44
+ end
45
+
46
+ def respond_to?(method)
47
+ !delegates_that_respond_to(method).empty?
48
+ end
49
+
50
+ private
51
+
52
+ def parse_method_delegation_option(options, name)
53
+ if values= options[name]
54
+ methods= [values].flatten.compact.map{|m| m.is_a?(String) ? m.to_sym : m}.uniq
55
+ instance_variable_set :"@#{name}", methods
56
+ end
57
+ end
58
+
59
+ def delegates_that_respond_to(method)
60
+ delegation_cache(method){ uncached_delegates_that_respond_to(method) }
61
+ end
62
+
63
+ NO_MATCHES= [].freeze
64
+ def uncached_delegates_that_respond_to(method)
65
+ return NO_MATCHES unless @delegates # i.e. not initialised yet, e.g. missing method before super() called in subclass
66
+ return NO_MATCHES if @method_whitelist && !@method_whitelist.any?{|m| m === method}
67
+ return NO_MATCHES if @method_blacklist && @method_blacklist.any?{|m| m === method}
68
+ r= @delegates.select{|d| d.respond_to?(method)}
69
+ r.empty? ? NO_MATCHES : r
70
+ end
71
+
72
+ # Don't allow nil values
73
+ def delegation_cache(key)
74
+ if @cache
75
+ @cache[key] ||= yield
76
+ else
77
+ yield
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,13 @@
1
+ require 'golly-utils/delegator'
2
+
3
+ module GollyUtils
4
+
5
+ # A fake IO implmentation that writes data to multiple underlying IO objects.
6
+ class MultiIO < Delegator
7
+
8
+ # @param targets Real IO objects.
9
+ def initialize(*targets)
10
+ super *targets, delegate_to: :all
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ Dir[File.expand_path("../ruby_ext",__FILE__) + '/**/*.rb'].each {|f| require f}
2
+
@@ -0,0 +1,29 @@
1
+ class Array
2
+
3
+ # Converts the array to a hash where the values are the array elements and the keys are provided by calling a given
4
+ # block.
5
+ #
6
+ # @example
7
+ # ['m','abc'].to_hash_keyed_by{ |v| v.length } # => {1 => 'm', 3 => 'abc}
8
+ def to_hash_keyed_by(raise_on_duplicate_keys=true, &key_provider)
9
+ h= {}
10
+ each {|e|
11
+ k= key_provider.call(e)
12
+ raise "Duplicate key: #{k.inspect}" if raise_on_duplicate_keys and h.has_key?(k)
13
+ h[k]= e
14
+ }
15
+ h
16
+ end
17
+
18
+ # Converts the array to a hash where the keys are the array elements and the values are provided by either calling a
19
+ # given block, or using a fixed, provided argument.
20
+ #
21
+ # @example
22
+ # [2,5].to_hash_with_values('x') # => {2 => 'x', 5 => 'x'}
23
+ # [2,5].to_hash_with_values{ |k| 'xo' * k } # => {2 => 'xoxo', 5 => 'xoxoxoxoxo'}
24
+ def to_hash_with_values(value=nil)
25
+ h= {}
26
+ each {|e| h[e]= block_given? ? yield(e) : value}
27
+ h
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ class Object
2
+ # Creates a deep copy of the object. Where supported (arrays and hashes by default), object state will be duplicated
3
+ # and used, rather than the original and duplicate objects sharing the same state.
4
+ def deep_dup
5
+ dup
6
+ end
7
+ end
8
+
9
+ class Array
10
+ # Creates a copy of the array with deep copies of each element.
11
+ #
12
+ # @see Object#deep_dup
13
+ def deep_dup
14
+ map(&:deep_dup)
15
+ end
16
+ end
17
+
18
+ class Hash
19
+ # Creates a copy of the hash with deep copies of each key and value.
20
+ #
21
+ # @see Object#deep_dup
22
+ def deep_dup
23
+ duplicate = {}
24
+ each_pair do |k,v|
25
+ duplicate[k.deep_dup]= v.deep_dup
26
+ end
27
+ duplicate
28
+ end
29
+ end
30
+
31
+ [TrueClass, FalseClass, NilClass, Symbol, Numeric].each do |klass|
32
+ klass.class_eval "def deep_dup; self end"
33
+ end
@@ -0,0 +1,27 @@
1
+ module GollyUtils
2
+ module EnvHelpers
3
+
4
+ def boolean(key, default=nil)
5
+ return default unless self.has_key?(key)
6
+ v= self[key]
7
+ return true if v =~ /^\s*(?:[1yt]|yes|true|on|enabled?)\s*$/i
8
+ return false if v =~ /^\s*(?:[0nf]|no|false|off|disabled?)\s*$/i
9
+ STDERR.puts "Unable to parse boolean value #{v.inspect} for key #{key.inspect}."
10
+ default
11
+ end
12
+
13
+ alias yes? boolean
14
+ alias on? boolean
15
+ alias enabled? boolean
16
+
17
+ def no?(key, default=nil)
18
+ !boolean(key, default)
19
+ end
20
+ alias off? no?
21
+ alias disabled? no?
22
+
23
+ end
24
+ end
25
+
26
+ # Add helpers to ENV
27
+ ENV.send :extend, GollyUtils::EnvHelpers