golly-utils 0.0.1

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