arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
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