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.
- checksums.yaml +7 -0
- data/.document +4 -0
- data/.simplecov +9 -0
- data/ChangeLog +417 -0
- data/Events.md +20 -0
- data/History.md +4 -0
- data/LICENSE +29 -0
- data/Manifest.txt +72 -0
- data/Monitors.md +141 -0
- data/Nodes.md +0 -0
- data/Observers.md +72 -0
- data/Protocol.md +214 -0
- data/README.md +75 -0
- data/Rakefile +81 -0
- data/TODO.md +24 -0
- data/bin/amanagerd +10 -0
- data/bin/amonitord +12 -0
- data/bin/aobserverd +12 -0
- data/lib/arborist.rb +182 -0
- data/lib/arborist/client.rb +191 -0
- data/lib/arborist/event.rb +61 -0
- data/lib/arborist/event/node_acked.rb +18 -0
- data/lib/arborist/event/node_delta.rb +20 -0
- data/lib/arborist/event/node_matching.rb +34 -0
- data/lib/arborist/event/node_update.rb +19 -0
- data/lib/arborist/event/sys_reloaded.rb +15 -0
- data/lib/arborist/exceptions.rb +21 -0
- data/lib/arborist/manager.rb +508 -0
- data/lib/arborist/manager/event_publisher.rb +97 -0
- data/lib/arborist/manager/tree_api.rb +207 -0
- data/lib/arborist/mixins.rb +363 -0
- data/lib/arborist/monitor.rb +377 -0
- data/lib/arborist/monitor/socket.rb +163 -0
- data/lib/arborist/monitor_runner.rb +217 -0
- data/lib/arborist/node.rb +700 -0
- data/lib/arborist/node/host.rb +87 -0
- data/lib/arborist/node/root.rb +60 -0
- data/lib/arborist/node/service.rb +112 -0
- data/lib/arborist/observer.rb +176 -0
- data/lib/arborist/observer/action.rb +125 -0
- data/lib/arborist/observer/summarize.rb +105 -0
- data/lib/arborist/observer_runner.rb +181 -0
- data/lib/arborist/subscription.rb +82 -0
- data/spec/arborist/client_spec.rb +282 -0
- data/spec/arborist/event/node_update_spec.rb +71 -0
- data/spec/arborist/event_spec.rb +64 -0
- data/spec/arborist/manager/event_publisher_spec.rb +66 -0
- data/spec/arborist/manager/tree_api_spec.rb +458 -0
- data/spec/arborist/manager_spec.rb +442 -0
- data/spec/arborist/mixins_spec.rb +195 -0
- data/spec/arborist/monitor/socket_spec.rb +195 -0
- data/spec/arborist/monitor_runner_spec.rb +152 -0
- data/spec/arborist/monitor_spec.rb +251 -0
- data/spec/arborist/node/host_spec.rb +104 -0
- data/spec/arborist/node/root_spec.rb +29 -0
- data/spec/arborist/node/service_spec.rb +98 -0
- data/spec/arborist/node_spec.rb +552 -0
- data/spec/arborist/observer/action_spec.rb +205 -0
- data/spec/arborist/observer/summarize_spec.rb +294 -0
- data/spec/arborist/observer_spec.rb +146 -0
- data/spec/arborist/subscription_spec.rb +71 -0
- data/spec/arborist_spec.rb +146 -0
- data/spec/data/monitors/pings.rb +80 -0
- data/spec/data/monitors/port_checks.rb +27 -0
- data/spec/data/monitors/system_resources.rb +30 -0
- data/spec/data/monitors/web_services.rb +17 -0
- data/spec/data/nodes/duir.rb +20 -0
- data/spec/data/nodes/localhost.rb +15 -0
- data/spec/data/nodes/sidonie.rb +29 -0
- data/spec/data/nodes/yevaud.rb +26 -0
- data/spec/data/observers/auditor.rb +23 -0
- data/spec/data/observers/webservices.rb +18 -0
- data/spec/spec_helper.rb +117 -0
- 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
|