arborist 0.0.1.pre20160106113421

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,97 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'msgpack'
5
+ require 'loggability'
6
+ require 'rbczmq'
7
+ require 'arborist/manager' unless defined?( Arborist::Manager )
8
+
9
+
10
+ class Arborist::Manager::EventPublisher < ZMQ::Handler
11
+ extend Loggability,
12
+ Arborist::MethodUtilities
13
+
14
+ # Loggability API -- log to arborist's logger
15
+ log_to :arborist
16
+
17
+
18
+ ### Create a new EventPublish that will publish events emitted by
19
+ ### emitters on the specified +manager+ on the given +pollable+.
20
+ def initialize( pollitem, manager, reactor )
21
+ self.log.debug "Setting up a %p for %p (socket %p)" %
22
+ [ self.class, pollitem, pollitem.pollable ]
23
+ @pollable = pollitem.pollable
24
+ @pollitem = pollitem
25
+ @manager = manager
26
+ @reactor = reactor
27
+ @registered = true
28
+ @event_queue = []
29
+ end
30
+
31
+
32
+ ######
33
+ public
34
+ ######
35
+
36
+ ##
37
+ # True if the publisher is currently registered with the reactor (i.e., waiting
38
+ # to write published events).
39
+ attr_predicate :registered
40
+
41
+
42
+ ### Publish the specified +event+.
43
+ def publish( identifier, event )
44
+ @event_queue << [ identifier, MessagePack.pack(event.to_hash) ]
45
+ self.register
46
+ return self
47
+ end
48
+
49
+
50
+ ### ZMQ::Handler API -- write events to the socket as it becomes writable.
51
+ def on_writable
52
+ unless @event_queue.empty?
53
+ tuple = @event_queue.shift
54
+ identifier, payload = *tuple
55
+
56
+ pollsocket = self.pollitem.pollable
57
+ pollsocket.sendm( identifier )
58
+ pollsocket.send( payload )
59
+ end
60
+ self.unregister if @event_queue.empty?
61
+ return true
62
+ end
63
+
64
+
65
+ #########
66
+ protected
67
+ #########
68
+
69
+ ### Register the publisher with the reactor if it's not already.
70
+ def register
71
+ count ||= 0
72
+ @reactor.register( self.pollitem ) unless @registered
73
+ @registered = true
74
+ rescue => err
75
+ # :TODO:
76
+ # This is to work around a weird error that happens sometimes when registering:
77
+ # Sys error location: loop.c:424
78
+ # which then raises a RuntimeError with "Socket operation on non-socket". This is
79
+ # probably due to a race somewhere, but we can't find it. So this works (at least
80
+ # for the specs) in the meantime.
81
+ raise unless err.message.include?( "Socket operation on non-socket" )
82
+ count += 1
83
+ raise "Gave up trying to register %p with the reactor" % [ self.pollitem ] if count > 5
84
+ warn "Retrying registration!"
85
+ retry
86
+ end
87
+
88
+
89
+ ### Unregister the publisher from the reactor if it's registered.
90
+ def unregister
91
+ @reactor.remove( self.pollitem ) if @registered
92
+ @registered = false
93
+ end
94
+
95
+
96
+ end # class Arborist::Manager::EventPublisher
97
+
@@ -0,0 +1,207 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'msgpack'
5
+ require 'loggability'
6
+ require 'rbczmq'
7
+ require 'arborist/manager' unless defined?( Arborist::Manager )
8
+
9
+
10
+ class Arborist::Manager::TreeAPI < ZMQ::Handler
11
+ extend Loggability
12
+
13
+
14
+ # Loggability API -- log to the arborist logger
15
+ log_to :arborist
16
+
17
+
18
+ ### Create the TreeAPI handler that will read requests from the specified +pollable+
19
+ ### and call into the +manager+ to respond to them.
20
+ def initialize( pollable, manager )
21
+ self.log.debug "Setting up a %p" % [ self.class ]
22
+ @pollitem = pollable
23
+ @manager = manager
24
+ end
25
+
26
+
27
+ ### ZMQ::Handler API -- Read and handle an incoming request.
28
+ def on_readable
29
+ request = self.recv
30
+ response = self.handle_request( request )
31
+ self.send( response )
32
+ end
33
+
34
+
35
+ ### Handle the specified +raw_request+ and return a response.
36
+ def handle_request( raw_request )
37
+ self.log.debug "Handling request: %p" % [ raw_request ]
38
+
39
+ header, body = self.parse_request( raw_request )
40
+ return self.dispatch_request( header, body )
41
+
42
+ rescue => err
43
+ self.log.error "%p: %s" % [ err.class, err.message ]
44
+ err.backtrace.each {|frame| self.log.debug " #{frame}" }
45
+
46
+ errtype = err.is_a?( Arborist::RequestError ) ? 'client' : 'server'
47
+ return self.error_response( errtype, err.message )
48
+ end
49
+
50
+
51
+ ### Attempt to dispatch a request given its +header+ and +body+, and return the
52
+ ### serialized response.
53
+ def dispatch_request( header, body )
54
+ self.log.debug "Dispatching request %p -> %p" % [ header, body ]
55
+ handler = self.lookup_request_action( header ) or
56
+ raise Arborist::RequestError, "No such action '%s'" % [ header['action'] ]
57
+
58
+ response = handler.call( header, body )
59
+
60
+ self.log.debug "Returning response: %p" % [ response ]
61
+ return response
62
+ end
63
+
64
+
65
+ ### Given a request +header+, return a #call-able object that can handle the response.
66
+ def lookup_request_action( header )
67
+ raise Arborist::RequestError, "unsupported version %d" % [ header['version'] ] unless
68
+ header['version'] == 1
69
+
70
+ handler_name = "handle_%s_request" % [ header['action'] ]
71
+ return nil unless self.respond_to?( handler_name )
72
+
73
+ return self.method( handler_name )
74
+ end
75
+
76
+
77
+ ### Build an error response message for the specified +category+ and +reason+.
78
+ def error_response( category, reason )
79
+ msg = [
80
+ { category: category, reason: reason, success: false, version: 1 }
81
+ ]
82
+ self.log.debug "Returning error response: %p" % [ msg ]
83
+ return MessagePack.pack( msg )
84
+ end
85
+
86
+
87
+ ### Build a successful response with the specified +body+.
88
+ def successful_response( body )
89
+ msg = [
90
+ { success: true, version: 1 },
91
+ body
92
+ ]
93
+ self.log.debug "Returning successful response: %p" % [ msg ]
94
+ return MessagePack.pack( msg )
95
+ end
96
+
97
+
98
+ ### Validate and return a parsed msgpack +raw_request+.
99
+ def parse_request( raw_request )
100
+ tuple = begin
101
+ MessagePack.unpack( raw_request )
102
+ rescue => err
103
+ raise Arborist::RequestError, err.message
104
+ end
105
+
106
+ self.log.debug "Parsed request: %p" % [ tuple ]
107
+
108
+ raise Arborist::RequestError, 'not a tuple' unless tuple.is_a?( Array )
109
+ raise Arborist::RequestError, 'incorrect length' if tuple.length.zero? || tuple.length > 2
110
+
111
+ header, body = *tuple
112
+ raise Arborist::RequestError, "header is not a Map" unless
113
+ header.is_a?( Hash )
114
+ raise Arborist::RequestError, "missing required header 'version'" unless
115
+ header.key?( 'version' )
116
+ raise Arborist::RequestError, "missing required header 'action'" unless
117
+ header.key?( 'action' )
118
+
119
+ raise Arborist::RequestError, "body must be a Map or Nil" unless
120
+ body.is_a?( Hash ) || body.nil?
121
+
122
+ return header, body
123
+ end
124
+
125
+
126
+ ### Return a response to the `status` action.
127
+ def handle_status_request( header, body )
128
+ self.log.info "STATUS: %p" % [ header ]
129
+ return successful_response(
130
+ server_version: Arborist::VERSION,
131
+ state: @manager.running? ? 'running' : 'not running',
132
+ uptime: @manager.uptime,
133
+ nodecount: @manager.nodecount
134
+ )
135
+ end
136
+
137
+
138
+ ### Return a response to the `subscribe` action.
139
+ def handle_subscribe_request( header, body )
140
+ self.log.info "SUBSCRIBE: %p" % [ header ]
141
+ event_type = header[ 'event_type' ]
142
+ node_identifier = header[ 'identifier' ]
143
+ subscription = @manager.create_subscription( node_identifier, event_type, body )
144
+
145
+ return successful_response([ subscription.id ])
146
+ end
147
+
148
+
149
+ ### Return a response to the `unsubscribe` action.
150
+ def handle_unsubscribe_request( header, body )
151
+ self.log.info "UNSUBSCRIBE: %p" % [ header ]
152
+ subscription_id = header[ 'subscription_id' ] or
153
+ return error_response( 'client', 'No identifier specified for UNSUBSCRIBE.' )
154
+ subscription = @manager.remove_subscription( subscription_id ) or
155
+ return successful_response( nil )
156
+
157
+ return successful_response(
158
+ event_type: subscription.event_type,
159
+ criteria: subscription.criteria
160
+ )
161
+ end
162
+
163
+
164
+ ### Return a repsonse to the `list` action.
165
+ def handle_list_request( header, body )
166
+ self.log.info "LIST: %p" % [ header ]
167
+ from = header['from'] || '_'
168
+
169
+ start_node = @manager.nodes[ from ]
170
+ self.log.debug " Listing nodes under %p" % [ start_node ]
171
+ iter = @manager.enumerator_for( start_node )
172
+ data = iter.map( &:to_hash )
173
+ self.log.debug " got data for %d nodes" % [ data.length ]
174
+
175
+ return successful_response( data )
176
+ end
177
+
178
+
179
+ ### Return a response to the 'fetch' action.
180
+ def handle_fetch_request( header, body )
181
+ self.log.info "FETCH: %p" % [ header ]
182
+
183
+ include_down = header['include_down']
184
+ values = if header.key?( 'return' )
185
+ header['return'] || []
186
+ else
187
+ nil
188
+ end
189
+ states = @manager.fetch_matching_node_states( body, values, include_down )
190
+
191
+ return successful_response( states )
192
+ end
193
+
194
+
195
+ ### Update nodes using the data from the update request's +body+.
196
+ def handle_update_request( header, body )
197
+ self.log.info "UPDATE: %p" % [ header ]
198
+
199
+ body.each do |identifier, properties|
200
+ @manager.update_node( identifier, properties )
201
+ end
202
+
203
+ return successful_response( nil )
204
+ end
205
+
206
+ end # class Arborist::Manager::TreeAPI
207
+
@@ -0,0 +1,363 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ # A collection of generically-useful mixins
5
+ module Arborist
6
+
7
+ # A collection of methods for declaring other methods.
8
+ #
9
+ # class MyClass
10
+ # extend Arborist::MethodUtilities
11
+ #
12
+ # singleton_attr_accessor :types
13
+ # singleton_method_alias :kinds, :types
14
+ # end
15
+ #
16
+ # MyClass.types = [ :pheno, :proto, :stereo ]
17
+ # MyClass.kinds # => [:pheno, :proto, :stereo]
18
+ #
19
+ module MethodUtilities
20
+
21
+ ### Creates instance variables and corresponding methods that return their
22
+ ### values for each of the specified +symbols+ in the singleton of the
23
+ ### declaring object (e.g., class instance variables and methods if declared
24
+ ### in a Class).
25
+ def singleton_attr_reader( *symbols )
26
+ singleton_class.instance_exec( symbols ) do |attrs|
27
+ attr_reader( *attrs )
28
+ end
29
+ end
30
+
31
+ ### Create instance variables and corresponding methods that return
32
+ ### true or false values for each of the specified +symbols+ in the singleton
33
+ ### of the declaring object.
34
+ def singleton_predicate_reader( *symbols )
35
+ singleton_class.extend( Arborist::MethodUtilities )
36
+ singleton_class.attr_predicate( *symbols )
37
+ end
38
+
39
+ ### Creates methods that allow assignment to the attributes of the singleton
40
+ ### of the declaring object that correspond to the specified +symbols+.
41
+ def singleton_attr_writer( *symbols )
42
+ singleton_class.instance_exec( symbols ) do |attrs|
43
+ attr_writer( *attrs )
44
+ end
45
+ end
46
+
47
+ ### Creates readers and writers that allow assignment to the attributes of
48
+ ### the singleton of the declaring object that correspond to the specified
49
+ ### +symbols+.
50
+ def singleton_attr_accessor( *symbols )
51
+ symbols.each do |sym|
52
+ singleton_class.__send__( :attr_accessor, sym )
53
+ end
54
+ end
55
+
56
+ ### Create predicate methods and writers that allow assignment to the attributes
57
+ ### of the singleton of the declaring object that correspond to the specified
58
+ ### +symbols+.
59
+ def singleton_predicate_accessor( *symbols )
60
+ singleton_class.extend( Arborist::MethodUtilities )
61
+ singleton_class.attr_predicate_accessor( *symbols )
62
+ end
63
+
64
+ ### Creates an alias for the +original+ method named +newname+.
65
+ def singleton_method_alias( newname, original )
66
+ singleton_class.__send__( :alias_method, newname, original )
67
+ end
68
+
69
+
70
+ ### Create a reader in the form of a predicate for the given +attrname+.
71
+ def attr_predicate( attrname )
72
+ attrname = attrname.to_s.chomp( '?' )
73
+ define_method( "#{attrname}?" ) do
74
+ instance_variable_get( "@#{attrname}" ) ? true : false
75
+ end
76
+ end
77
+
78
+
79
+ ### Create a reader in the form of a predicate for the given +attrname+
80
+ ### as well as a regular writer method.
81
+ def attr_predicate_accessor( attrname )
82
+ attrname = attrname.to_s.chomp( '?' )
83
+ attr_writer( attrname )
84
+ attr_predicate( attrname )
85
+ end
86
+
87
+ end # module MethodUtilities
88
+
89
+
90
+ # Functions for time calculations
91
+ module TimeFunctions
92
+
93
+ ###############
94
+ module_function
95
+ ###############
96
+
97
+ ### Calculate the (approximate) number of seconds that are in +count+ of the
98
+ ### given +unit+ of time.
99
+ ###
100
+ def calculate_seconds( count, unit )
101
+ return case unit
102
+ when :seconds, :second
103
+ count
104
+ when :minutes, :minute
105
+ count * 60
106
+ when :hours, :hour
107
+ count * 3600
108
+ when :days, :day
109
+ count * 86400
110
+ when :weeks, :week
111
+ count * 604800
112
+ when :fortnights, :fortnight
113
+ count * 1209600
114
+ when :months, :month
115
+ count * 2592000
116
+ when :years, :year
117
+ count * 31557600
118
+ else
119
+ raise ArgumentError, "don't know how to calculate seconds in a %p" % [ unit ]
120
+ end
121
+ end
122
+ end # module TimeFunctions
123
+
124
+
125
+ # Refinements to Numeric and Time to add convenience methods
126
+ module TimeRefinements
127
+ refine Numeric do
128
+
129
+ ### Number of seconds (returns receiver unmodified)
130
+ def seconds
131
+ return self
132
+ end
133
+ alias_method :second, :seconds
134
+
135
+ ### Returns number of seconds in <receiver> minutes
136
+ def minutes
137
+ return TimeFunctions.calculate_seconds( self, :minutes )
138
+ end
139
+ alias_method :minute, :minutes
140
+
141
+ ### Returns the number of seconds in <receiver> hours
142
+ def hours
143
+ return TimeFunctions.calculate_seconds( self, :hours )
144
+ end
145
+ alias_method :hour, :hours
146
+
147
+ ### Returns the number of seconds in <receiver> days
148
+ def days
149
+ return TimeFunctions.calculate_seconds( self, :day )
150
+ end
151
+ alias_method :day, :days
152
+
153
+ ### Return the number of seconds in <receiver> weeks
154
+ def weeks
155
+ return TimeFunctions.calculate_seconds( self, :weeks )
156
+ end
157
+ alias_method :week, :weeks
158
+
159
+ ### Returns the number of seconds in <receiver> fortnights
160
+ def fortnights
161
+ return TimeFunctions.calculate_seconds( self, :fortnights )
162
+ end
163
+ alias_method :fortnight, :fortnights
164
+
165
+ ### Returns the number of seconds in <receiver> months (approximate)
166
+ def months
167
+ return TimeFunctions.calculate_seconds( self, :months )
168
+ end
169
+ alias_method :month, :months
170
+
171
+ ### Returns the number of seconds in <receiver> years (approximate)
172
+ def years
173
+ return TimeFunctions.calculate_seconds( self, :years )
174
+ end
175
+ alias_method :year, :years
176
+
177
+
178
+ ### Returns the Time <receiver> number of seconds before the
179
+ ### specified +time+. E.g., 2.hours.before( header.expiration )
180
+ def before( time )
181
+ return time - self
182
+ end
183
+
184
+
185
+ ### Returns the Time <receiver> number of seconds ago. (e.g.,
186
+ ### expiration > 2.hours.ago )
187
+ def ago
188
+ return self.before( ::Time.now )
189
+ end
190
+
191
+
192
+ ### Returns the Time <receiver> number of seconds after the given +time+.
193
+ ### E.g., 10.minutes.after( header.expiration )
194
+ def after( time )
195
+ return time + self
196
+ end
197
+
198
+
199
+ ### Return a new Time <receiver> number of seconds from now.
200
+ def from_now
201
+ return self.after( ::Time.now )
202
+ end
203
+
204
+ end # refine Numeric
205
+
206
+
207
+ refine Time do
208
+
209
+ # Approximate Time Constants (in seconds)
210
+ MINUTES = 60
211
+ HOURS = 60 * MINUTES
212
+ DAYS = 24 * HOURS
213
+ WEEKS = 7 * DAYS
214
+ MONTHS = 30 * DAYS
215
+ YEARS = 365.25 * DAYS
216
+
217
+
218
+ ### Returns +true+ if the receiver is a Time in the future.
219
+ def future?
220
+ return self > Time.now
221
+ end
222
+
223
+
224
+ ### Returns +true+ if the receiver is a Time in the past.
225
+ def past?
226
+ return self < Time.now
227
+ end
228
+
229
+
230
+ ### Return a description of the receiving Time object in relation to the current
231
+ ### time.
232
+ ###
233
+ ### Example:
234
+ ###
235
+ ### "Saved %s ago." % object.updated_at.as_delta
236
+ def as_delta
237
+ now = Time.now
238
+ if now > self
239
+ seconds = now - self
240
+ return "%s ago" % [ timeperiod(seconds) ]
241
+ else
242
+ seconds = self - now
243
+ return "%s from now" % [ timeperiod(seconds) ]
244
+ end
245
+ end
246
+
247
+
248
+ ### Return a description of +seconds+ as the nearest whole unit of time.
249
+ def timeperiod( seconds )
250
+ return case
251
+ when seconds < MINUTES - 5
252
+ 'less than a minute'
253
+ when seconds < 50 * MINUTES
254
+ if seconds <= 89
255
+ "a minute"
256
+ else
257
+ "%d minutes" % [ (seconds.to_f / MINUTES).ceil ]
258
+ end
259
+ when seconds < 90 * MINUTES
260
+ 'about an hour'
261
+ when seconds < 18 * HOURS
262
+ "%d hours" % [ (seconds.to_f / HOURS).ceil ]
263
+ when seconds < 30 * HOURS
264
+ 'about a day'
265
+ when seconds < WEEKS
266
+ "%d days" % [ (seconds.to_f / DAYS).ceil ]
267
+ when seconds < 2 * WEEKS
268
+ 'about a week'
269
+ when seconds < 3 * MONTHS
270
+ "%d weeks" % [ (seconds.to_f / WEEKS).round ]
271
+ when seconds < 18 * MONTHS
272
+ "%d months" % [ (seconds.to_f / MONTHS).ceil ]
273
+ else
274
+ "%d years" % [ (seconds.to_f / YEARS).ceil ]
275
+ end
276
+ end
277
+
278
+ end # refine Time
279
+
280
+ end # module TimeRefinements
281
+
282
+
283
+ ### A collection of utilities for working with Hashes.
284
+ module HashUtilities
285
+
286
+ ###############
287
+ module_function
288
+ ###############
289
+
290
+ ### Return a version of the given +hash+ with its keys transformed
291
+ ### into Strings from whatever they were before.
292
+ def stringify_keys( hash )
293
+ newhash = {}
294
+
295
+ hash.each do |key,val|
296
+ if val.is_a?( Hash )
297
+ newhash[ key.to_s ] = stringify_keys( val )
298
+ else
299
+ newhash[ key.to_s ] = val
300
+ end
301
+ end
302
+
303
+ return newhash
304
+ end
305
+
306
+
307
+ ### Return a duplicate of the given +hash+ with its identifier-like keys
308
+ ### transformed into symbols from whatever they were before.
309
+ def symbolify_keys( hash )
310
+ newhash = {}
311
+
312
+ hash.each do |key,val|
313
+ keysym = key.to_s.dup.untaint.to_sym
314
+
315
+ if val.is_a?( Hash )
316
+ newhash[ keysym ] = symbolify_keys( val )
317
+ else
318
+ newhash[ keysym ] = val
319
+ end
320
+ end
321
+
322
+ return newhash
323
+ end
324
+ alias_method :internify_keys, :symbolify_keys
325
+
326
+
327
+ # Recursive hash-merge function
328
+ def merge_recursively( key, oldval, newval )
329
+ case oldval
330
+ when Hash
331
+ case newval
332
+ when Hash
333
+ oldval.merge( newval, &method(:merge_recursively) )
334
+ else
335
+ newval
336
+ end
337
+
338
+ when Array
339
+ case newval
340
+ when Array
341
+ oldval | newval
342
+ else
343
+ newval
344
+ end
345
+
346
+ else
347
+ newval
348
+ end
349
+ end
350
+
351
+
352
+ # Recursively remove hash pairs in place whose value is nil.
353
+ def compact_hash( hash )
354
+ hash.each_key do |k|
355
+ hash.delete( k ) if hash[ k ].nil?
356
+ compact_hash( hash[k] ) if hash[k].is_a?( Hash )
357
+ end
358
+ end
359
+
360
+
361
+ end # HashUtilities
362
+
363
+ end # module Arborist