golly-utils 0.0.1 → 0.6.0

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