golly-utils 0.0.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.corvid/Gemfile +28 -0
  2. data/.corvid/features.yml +4 -0
  3. data/.corvid/plugins.yml +4 -0
  4. data/.corvid/stats_cfg.rb +13 -0
  5. data/.corvid/todo_cfg.rb +15 -0
  6. data/.corvid/versions.yml +2 -0
  7. data/.simplecov +6 -3
  8. data/CHANGELOG.md +45 -2
  9. data/Gemfile +6 -3
  10. data/Gemfile.lock +34 -37
  11. data/Guardfile +10 -4
  12. data/RELEASE.md +2 -0
  13. data/Rakefile +1 -1
  14. data/golly-utils.gemspec +19 -10
  15. data/lib/golly-utils/attr_declarative.rb +120 -49
  16. data/lib/golly-utils/callbacks.rb +211 -19
  17. data/lib/golly-utils/child_process.rb +28 -8
  18. data/lib/golly-utils/delegator.rb +14 -3
  19. data/lib/golly-utils/ruby_ext/classes_and_types.rb +120 -0
  20. data/lib/golly-utils/ruby_ext/enumerable.rb +16 -0
  21. data/lib/golly-utils/ruby_ext/env_helpers.rb +17 -0
  22. data/lib/golly-utils/ruby_ext/kernel.rb +18 -0
  23. data/lib/golly-utils/ruby_ext/options.rb +28 -0
  24. data/lib/golly-utils/ruby_ext/pretty_error_messages.rb +1 -1
  25. data/lib/golly-utils/singleton.rb +130 -0
  26. data/lib/golly-utils/testing/dynamic_fixtures.rb +268 -0
  27. data/lib/golly-utils/testing/file_helpers.rb +117 -0
  28. data/lib/golly-utils/testing/helpers_base.rb +20 -0
  29. data/lib/golly-utils/testing/rspec/arrays.rb +85 -0
  30. data/lib/golly-utils/testing/rspec/base.rb +9 -0
  31. data/lib/golly-utils/testing/rspec/deferrable_specs.rb +111 -0
  32. data/lib/golly-utils/testing/rspec/files.rb +262 -0
  33. data/lib/golly-utils/{test/spec → testing/rspec}/within_time.rb +17 -7
  34. data/lib/golly-utils/version.rb +2 -1
  35. data/test/bootstrap/all.rb +8 -1
  36. data/test/bootstrap/spec.rb +1 -1
  37. data/test/bootstrap/unit.rb +1 -1
  38. data/test/spec/child_process_spec.rb +1 -1
  39. data/test/spec/testing/dynamic_fixtures_spec.rb +131 -0
  40. data/test/spec/testing/rspec/arrays_spec.rb +33 -0
  41. data/test/spec/testing/rspec/files_spec.rb +300 -0
  42. data/test/unit/attr_declarative_test.rb +79 -13
  43. data/test/unit/callbacks_test.rb +103 -5
  44. data/test/unit/delegator_test.rb +25 -1
  45. data/test/unit/ruby_ext/classes_and_types_test.rb +103 -0
  46. data/test/unit/ruby_ext/enumerable_test.rb +12 -0
  47. data/test/unit/ruby_ext/options_test.rb +29 -0
  48. data/test/unit/singleton_test.rb +59 -0
  49. metadata +100 -10
  50. data/Gemfile.corvid +0 -27
  51. data/lib/golly-utils/ruby_ext.rb +0 -2
  52. data/lib/golly-utils/ruby_ext/subclasses.rb +0 -17
  53. data/lib/golly-utils/test/spec/deferrable_specs.rb +0 -85
  54. data/test/unit/ruby_ext/subclasses_test.rb +0 -24
@@ -1,22 +1,158 @@
1
1
  require 'golly-utils/ruby_ext/deep_dup'
2
+ require 'golly-utils/delegator'
2
3
 
3
4
  module GollyUtils
5
+ # A very simple callback mechanism for use within a single class heirarchy.
6
+ #
7
+ # It is primiarily meant to be used as a replacement for method overriding in external subclasses; the problem with
8
+ # that approach being a) it's unclear with methods are required/overrides, and b) if the wrong method name is used
9
+ # there is no early feedback - the erronously named method will simply never be invoked and the super-method will not
10
+ # receive the intended modification.
11
+ #
12
+ # It allows:
13
+ #
14
+ # 1. A class to define named callback point.
15
+ # 2. Subclasses to supply callbacks to specific points by name.
16
+ # 3. Ability to run all callbacks for a given callback point.
17
+ #
18
+ # Unlike Rails' callbacks implementation, this deliberately doesn't provide before/after/around functionality, nor a
19
+ # chain-like structure where the return value of one callback can affect the determinism of other callbacks being
20
+ # invoked.
21
+ #
22
+ # ## Usage
23
+ #
24
+ # * In your superclass:
25
+ # 1. Include {Callbacks}.
26
+ # 2. Use {ClassMethods#define_callbacks} in the class definition.
27
+ # 3. Call {InstanceMethods#run_callbacks} in your code.
28
+ # * In subclasses:
29
+ # 1. Supply a callback by declaring the callback name in the class definition, followed by a block of code.
30
+ #
31
+ # @example
32
+ # class Engine
33
+ # include GollyUtils::Callbacks
34
+ #
35
+ # define_callback :start
36
+ #
37
+ # def start
38
+ # puts "About to start..."
39
+ # run_callbacks :start
40
+ # puts "Running."
41
+ # end
42
+ # end
43
+ #
44
+ # class CustomEngine < Engine
45
+ # start do
46
+ # puts "---> STARTING!!!"
47
+ # end
48
+ # end
49
+ #
50
+ # CustomEngine.new.start # => About to start...
51
+ # # => ---> STARTING!!!
52
+ # # => Running.
53
+ #
54
+ # @example Also works in modules
55
+ # module SupportsStuff
56
+ # include GollyUtils::Callbacks
57
+ # define_callback :stuff
58
+ # end
59
+ #
60
+ # class DoerOfStuff
61
+ # include SupportsStuff
62
+ # stuff{ puts 'Doing stuff!!' }
63
+ # end
64
+ #
65
+ # def stuff_machine(anything_that_supports_stuff)
66
+ # puts "I'll take anything that SupportsStuff."
67
+ # anything_that_supports_stuff.run_callbacks :stuff
68
+ # puts "See!"
69
+ # end
70
+ #
71
+ # stuff_machine DoerOfStuff.new # => I'll take anything that SupportsStuff.
72
+ # # => Doing stuff!!
73
+ # # => See!
4
74
  module Callbacks
5
75
 
76
+ # @!visibility private
6
77
  def self.included(base)
7
- base.send :include, InstanceMethods
8
- base.send :include, InstanceAndClassMethods
9
- base.extend InstanceAndClassMethods
10
- base.extend ClassMethods
78
+ if base.is_a?(Class)
79
+ base.send :include, InstanceMethods
80
+ base.extend ClassMethods
81
+ else
82
+ base.extend ModuleMethods
83
+ base.class_eval <<-EOB
84
+ class << self
85
+ alias :included_without_gu_callbacks :included
86
+ def included(base)
87
+ included_without_gu_callbacks(base)
88
+ __add_callbacks_when_included(base)
89
+ end
90
+ end
91
+ EOB
92
+ end
93
+ end
94
+
95
+ # @!visibility private
96
+ def self.__norm_callback_key(key)
97
+ key.to_sym
98
+ end
99
+
100
+ #-------------------------------------------------------------------------------------------------------------------
101
+
102
+ # Provides methods that can be run within definitions of modules that include {Callbacks}.
103
+ module ModuleMethods
104
+
105
+ # Create one or more callback points that will be added to classes that include the enclosing module.
106
+ #
107
+ # @param (see GollyUtils::Callbacks::ClassMethods#define_callbacks)
108
+ # @return [true]
109
+ def define_callbacks(*callbacks)
110
+ __module_callback_names.concat callbacks
111
+ __module_callback_names.uniq!
112
+ true
113
+ end
114
+ alias :define_callback :define_callbacks
115
+
116
+ # Returns a list of all callbacks available to this module. (i.e. defined, inherited, and included.)
117
+ #
118
+ # @return [Array<Symbol>] Callback names.
119
+ def callbacks
120
+ c= __module_callback_names
121
+ included_modules.each {|m|
122
+ c.concat m.callbacks if m.respond_to? :callbacks
123
+ }
124
+ c.uniq.sort_by(&:to_s)
125
+ end
126
+
127
+ private
128
+ def __add_callbacks_when_included(base)
129
+ base.send :include, Callbacks
130
+ names= __module_callback_names
131
+ unless names.empty?
132
+ base.class_eval "define_callbacks *#{names.inspect}"
133
+ end
134
+ end
135
+
136
+ def __module_callback_names
137
+ @__module_callbacks ||= []
138
+ end
11
139
  end
12
140
 
13
141
  #-------------------------------------------------------------------------------------------------------------------
14
142
 
143
+ # Provides methods that can be run within definitions of classes that include {Callbacks}.
15
144
  module ClassMethods
16
145
 
146
+ # Create one or more callback points for this class and its children.
147
+ #
148
+ # @param [Array<String|Symbol>] callbacks The callback name(s).
149
+ # @return [true]
150
+ # @raise If the callback has already been defined, or a method with that name already exists.
151
+ # @see Callbacks
152
+ # @see InstanceMethods#run_callbacks
17
153
  def define_callbacks(*callbacks)
18
154
  callbacks.each do |name|
19
- name= _norm_callback_key(name)
155
+ name= ::GollyUtils::Callbacks.__norm_callback_key(name)
20
156
 
21
157
  if self.methods.include?(name.to_sym)
22
158
  raise "Can't create callback with name '#{name}'. A method with that name already exists."
@@ -30,9 +166,19 @@ module GollyUtils
30
166
  end
31
167
  EOB
32
168
  end
169
+ true
33
170
  end
34
171
  alias :define_callback :define_callbacks
35
172
 
173
+ # Returns a list of all callbacks available to this class. (i.e. defined, inherited, and included.)
174
+ #
175
+ # @return [Array<Symbol>] Callback names.
176
+ def callbacks
177
+ c= superclass.respond_to?(:callbacks) ? superclass.callbacks : []
178
+ c.concat _callbacks.keys
179
+ c.uniq.sort_by(&:to_s)
180
+ end
181
+
36
182
  private
37
183
 
38
184
  def _callbacks
@@ -63,29 +209,75 @@ module GollyUtils
63
209
 
64
210
  #-------------------------------------------------------------------------------------------------------------------
65
211
 
66
- module InstanceAndClassMethods
212
+ # Provides methods that are available to instances of classes that include {Callbacks}.
213
+ module InstanceMethods
67
214
 
68
- private
69
- def _norm_callback_key(key)
70
- key.to_sym
71
- end
215
+ # Run all callbacks provided for a single callback point.
216
+ #
217
+ # @param [String, Symbol] callback The callback name.
218
+ # @param [Hash] options
219
+ # @option options [Array] args ([]) Arguments to pass to the callbacks.
220
+ # @option options [nil|Object] context (nil) If provided, code within callbacks will have access to methods
221
+ # available from the provided object.
222
+ # @return [true]
223
+ # @raise If the provided callback name hasn't been declared for this class.
224
+ # @raise If unrecognised or invalid options are provided.
225
+ # @see Callbacks
226
+ # @see ClassMethods#define_callbacks
227
+ def run_callback(callback, options={})
72
228
 
73
- end
229
+ # Validate callback name
230
+ name= ::GollyUtils::Callbacks.__norm_callback_key(callback)
231
+ name_verified,callback_procs = self.class.send :_get_callback_procs, name
232
+ raise "There is no callback defined with name #{name}." unless name_verified
74
233
 
75
- #-------------------------------------------------------------------------------------------------------------------
234
+ # Validate options
235
+ invalid_options= options.keys - RUN_CALLBACKS_OPTIONS
236
+ unless invalid_options.empty?
237
+ raise "Unable to recognise options: #{invalid_options.map(&:inspect).sort}"
238
+ end
239
+ args= options[:args] || []
240
+ raise "The :args option must provide an array. Invalid: #{args}" unless args.is_a?(Array)
76
241
 
77
- module InstanceMethods
242
+ # Run callback
243
+ callback_procs.each{|cb|
244
+ if ctx= options[:context]
245
+ dlg= GollyUtils::Delegator.new self, ctx, delegate_to: :first, allow_protected: true
246
+ dlg.instance_eval &cb
247
+ else
248
+ cb.call *args
249
+ end
250
+ }
251
+
252
+ true
253
+ end
78
254
 
255
+ # Run all callbacks provided for one or more callback points.
256
+ #
257
+ # @overload run_callbacks(*callbacks, options = {})
258
+ # @param [Array<String, Symbol>] callbacks The callback name(s).
259
+ # @param [Hash] options
260
+ # @option options [Array] args ([]) Arguments to pass to the callbacks.
261
+ # @option options [nil|Object] context (nil) If provided, code within callbacks will have access to methods
262
+ # available from the provided object.
263
+ # @return [true]
264
+ # @raise If one of the provided callback names hasn't been declared for this class.
265
+ # @raise If unrecognised or invalid options are provided.
266
+ # @see Callbacks
267
+ # @see ClassMethods#define_callbacks
79
268
  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 }
269
+ options= callbacks.last.is_a?(Hash) ? callbacks.pop : {}
270
+
271
+ # Run callbacks
272
+ callbacks.each do |callback|
273
+ run_callback callback, options
85
274
  end
275
+
86
276
  true
87
277
  end
88
- alias :run_callback :run_callbacks
278
+
279
+ # @!visibility private
280
+ RUN_CALLBACKS_OPTIONS= [:args, :context].freeze
89
281
 
90
282
  end
91
283
  end
@@ -2,15 +2,33 @@ module GollyUtils
2
2
 
3
3
  # Start, manage, and stop a child process.
4
4
  class ChildProcess
5
- attr_accessor :start_command, :quiet, :spawn_options, :env
6
- attr_reader :pid
5
+
6
+ # The shell command to start the child process.
7
+ # @return [String]
8
+ attr_accessor :start_command
9
+
10
+ # Whether to print startup/shutdown info to stdout, and whether or not to the stdout and stderr streams of the child
11
+ # process (unless explictly redirected via :spawn_options)
12
+ # @return [Boolean]
13
+ attr_accessor :quiet
7
14
  alias :quiet? :quiet
8
15
 
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.
16
+ # Options to pass to `Process#spawn`.
17
+ # @return [Hash]
18
+ attr_accessor :spawn_options
19
+
20
+ # Environment variables to set in the child process.
21
+ # @return [Hash]
22
+ attr_accessor :env
23
+
24
+ # The PID of the child process if running.
25
+ # @return [Fixnum, nil]
26
+ attr_reader :pid
27
+
28
+ # @option options [String] :start_command See {#start_command}.
29
+ # @option options [Hash] :env See {#env}.
30
+ # @option options [Boolean] :quiet (false) See {#quiet}.
31
+ # @option options [Hash] :spawn_options See {#spawn_options}.
14
32
  def initialize(options={})
15
33
  options= {env: {}, quiet: false, spawn_options: {}}.merge(options)
16
34
  options[:spawn_options][:in] ||= '/dev/null'
@@ -21,7 +39,7 @@ module GollyUtils
21
39
  #
22
40
  # If it is already running, then this will do nothing.
23
41
  #
24
- # @return @self@
42
+ # @return [self]
25
43
  def startup
26
44
  unless alive?
27
45
  opt= self.spawn_options
@@ -103,12 +121,14 @@ module GollyUtils
103
121
 
104
122
  # --------------------------------------------------------------------------------------------------------------------
105
123
  class << self
124
+ # @!visibility private
106
125
  OPTION_TRANSLATION= {
107
126
  stdout: :out,
108
127
  stderr: :err,
109
128
  stdin: :in,
110
129
  }.freeze
111
130
 
131
+ # @!visibility private
112
132
  def translate_options(prefix='')
113
133
  spawn_opts= {}
114
134
  OPTION_TRANSLATION.each do |from,to|
@@ -7,6 +7,8 @@ module GollyUtils
7
7
  # @overload initialize(*delegates, options={})
8
8
  # @param [Object] delegates Objects that method calls may be delegated to.
9
9
  # @param [Hash] options
10
+ # @option options [true,false] :allow_protected (false) Whether or not to allow calls to protected methods in
11
+ # delegates.
10
12
  # @option options [true,false] :cache (true) Whether or not to maintain a cache of which delegate objects can
11
13
  # respond to each method call.
12
14
  # @option options [:first,:all] :delegate_to (:first) When multiple delegates can respond to a method call, this
@@ -22,6 +24,7 @@ module GollyUtils
22
24
  @delegates= args
23
25
  @delegate_to= options[:delegate_to] || :first
24
26
  @cache= {} unless options.has_key?(:cache) && !options[:cache]
27
+ @allow_protected= options[:allow_protected]
25
28
  parse_method_delegation_option options, :method_whitelist
26
29
  parse_method_delegation_option options, :method_blacklist
27
30
  end
@@ -35,20 +38,28 @@ module GollyUtils
35
38
 
36
39
  case delegate_to
37
40
  when :first
38
- matches[0].public_send(method,*args)
41
+ delegate_call matches[0], method, args
39
42
  when :all
40
- matches.map{|m| m.public_send(method,*args)}
43
+ matches.map{|m| delegate_call m, method, args }
41
44
  else
42
45
  raise "Don't know how to respond to :delegate_to value of #{delegate_to.inspect}"
43
46
  end
44
47
  end
45
48
 
46
49
  def respond_to?(method)
47
- !delegates_that_respond_to(method).empty?
50
+ super(method) or !delegates_that_respond_to(method).empty?
48
51
  end
49
52
 
50
53
  private
51
54
 
55
+ def delegate_call(target, method, args)
56
+ if @allow_protected and target.protected_methods.include?(method)
57
+ target.send method, *args
58
+ else
59
+ target.public_send method, *args
60
+ end
61
+ end
62
+
52
63
  def parse_method_delegation_option(options, name)
53
64
  if values= options[name]
54
65
  methods= [values].flatten.compact.map{|m| m.is_a?(String) ? m.to_sym : m}.uniq
@@ -0,0 +1,120 @@
1
+ class Class
2
+
3
+ # @!visibility private
4
+ def inherited other
5
+ super if defined? super
6
+ ensure
7
+ ( @subclasses ||= [] ).push(other).uniq!
8
+ end
9
+
10
+ # Returns a list of classes that extend this class, directly or indirectly (as in subclasses of subclasses).
11
+ #
12
+ # @example
13
+ # # Given the following class heirarchy:
14
+ # # A
15
+ # # |
16
+ # # +--B
17
+ # # | +--B1
18
+ # # | +--B2
19
+ # # |
20
+ # # +--C
21
+ #
22
+ # A.subclasses # => [B1, B2, C]
23
+ # A.subclasses(false) # => [B1, B2, C]
24
+ # A.subclasses(true) # => [B, B1, B2, C]
25
+ #
26
+ # @param include_subclassed_nodes If `true` then classes extended by other classes are returned. If `false` then you
27
+ # only get the end nodes.
28
+ # @return [Array<Class>] An array of all subclasses.
29
+ def subclasses(include_subclassed_nodes = false)
30
+ @subclasses ||= []
31
+ classes= @subclasses.inject( [] ) {|list, subclass| list.push subclass, *subclass.subclasses }
32
+ classes.reject! {|c| classes.any?{|i| c != i and c.subclasses.include?(i) }} unless include_subclassed_nodes
33
+ classes
34
+ end
35
+ end
36
+
37
+ class Object
38
+ # Returns the class hierarchy of a given instance or class.
39
+ #
40
+ # @example
41
+ # Fixnum.superclasses # <= [Fixnum, Integer, Numeric, Object, BasicObject]
42
+ # 100.superclasses # <= [Fixnum, Integer, Numeric, Object, BasicObject]
43
+ #
44
+ # @return [Array<Class>] An array of classes starting with the current class, descending to `BasicObject`.
45
+ def superclasses
46
+ if self == BasicObject
47
+ [self]
48
+ elsif self.is_a? Class
49
+ [self] + self.superclass.superclasses
50
+ else
51
+ self.class.superclasses
52
+ end
53
+ end
54
+
55
+ # Indicates that a type validation check has failed.
56
+ class TypeValidationError < RuntimeError
57
+ end
58
+
59
+ # Validates the type of the current object.
60
+ #
61
+ # @example
62
+ # 3 .validate_type nil, Numeric
63
+ # nil .validate_type nil, Numeric
64
+ # 'What'.validate_type nil, Numeric # <= raises TypeValidationError
65
+ #
66
+ # # Ensures that f_debug is boolean
67
+ # f_debug.validate_type! 'the debug flag', true, false
68
+ #
69
+ # @overload validate_type!(name = nil, *valid_classes)
70
+ # @param [nil|Symbol|String] name The name of the object being checked (i.e. `self`).
71
+ # This only used in the error message and has no functional impact.
72
+ # @param [Array<Class>] valid_classes One or more classes that this object is allowed to be. Ruby primatives will
73
+ # automatically be translated to the corresponding class.
74
+ # @return [self] If validation passes.
75
+ # @raise [TypeValidationError] If validation fails.
76
+ # @see Symbol#validate_lvar_type!
77
+ def validate_type!(*args)
78
+ name= args.first.is_a?(String) || args.first.is_a?(Symbol) ? args.shift : nil
79
+ classes= args.map{|a| RUBY_PRIMATIVE_CLASSES[a] || a }
80
+ raise "You must specify at least one valid class." if classes.empty?
81
+
82
+ unless classes.any?{|c| self.is_a? c }
83
+ for_name= name ? " for #{name}" : ''
84
+ raise TypeValidationError, "Invalid type#{for_name}: #{self.class}\nValid types are: #{classes.map(&:to_s).sort.join ', '}."
85
+ end
86
+ self
87
+ end
88
+
89
+
90
+ # @!visibility private
91
+ RUBY_PRIMATIVE_CLASSES= Hash[ [nil,true,false].map{|p|[p,p.class]} ].freeze
92
+ end
93
+
94
+ class Symbol
95
+ # Validates the type of a local variable.
96
+ #
97
+ # @example
98
+ # def save_person(name, eyes)
99
+ # # Validate args
100
+ # :name.validate_lvar_type!{ String }
101
+ # :eyes.validate_lvar_type!{ [nil,Fixnum] }
102
+ #
103
+ # # Do other stuff
104
+ # # ...
105
+ # end
106
+ #
107
+ # @yield Calls a given block once to get the list of valid classes. The block must have access to the local variable.
108
+ # @yieldreturn [Class|Array<Class>] The given block should return one or more classes.
109
+ # @return [true] If validation passes.
110
+ # @raise [TypeValidationError] If validation fails.
111
+ # @see Object#validate_type!
112
+ def validate_lvar_type!(&block)
113
+ name= self
114
+ raise "You must provide a block that returns one or more valid classes for #{name}." unless block
115
+ classes= [block.()].flatten
116
+ v= block.send(:binding).eval(name.to_s)
117
+ v.validate_type! name, *classes
118
+ true
119
+ end
120
+ end