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