xap_ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +59 -0
- data/Rakefile +3 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/xap.rb +30 -0
- data/lib/xap/parser.rb +6 -0
- data/lib/xap/parser/parse_xap.rb +47 -0
- data/lib/xap/parser/xap.treetop +47 -0
- data/lib/xap/parser/xap_nodes.rb +120 -0
- data/lib/xap/schema.rb +7 -0
- data/lib/xap/schema/xap_bsc.rb +416 -0
- data/lib/xap/schema/xap_bsc_device.rb +384 -0
- data/lib/xap/xap_address.rb +169 -0
- data/lib/xap/xap_dev.rb +76 -0
- data/lib/xap/xap_handler.rb +197 -0
- data/lib/xap/xap_msg.rb +194 -0
- data/lib/xap_ruby.rb +6 -0
- data/lib/xap_ruby/version.rb +3 -0
- data/xap_ruby.gemspec +43 -0
- metadata +163 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
# An XapDevice model of an xAP Basic Status and Control device.
|
2
|
+
# (C)2012 Mike Bourgeous
|
3
|
+
|
4
|
+
module Xap
|
5
|
+
module Schema
|
6
|
+
# Represents an xAP BSC Device. See the xAP Basic Status and Control Schema.
|
7
|
+
# http://www.xapautomation.org/index.php?title=Basic_Status_and_Control_Schema
|
8
|
+
class XapBscDevice < XapDevice
|
9
|
+
# Initializes an XapBscDevice with the given address, uid. Endpoints
|
10
|
+
# is an array of hashes containing :State, :Level, :Text, and/or
|
11
|
+
# optionally :DisplayText. :State should be a boolean value or nil,
|
12
|
+
# :Level should be an array of [numerator, denominator], and :Text and
|
13
|
+
# :DisplayText should be Strings. Each input and output block must
|
14
|
+
# also have an :endpoint key that contains the endpoint name for the
|
15
|
+
# given block and may include a :uid key that contains an integer from
|
16
|
+
# 1 to 254. Endpoint names and UIDs must be unique within this device.
|
17
|
+
# Output hashes (i.e. those endpoints that can be changed via xAP) are
|
18
|
+
# identified by including a :callback key that is a proc to be called
|
19
|
+
# with the endpoint hash when any of the output endpoint's attributes
|
20
|
+
# are changed by an incoming xAP message.
|
21
|
+
#
|
22
|
+
# See add_endpoint for a summary of endpoint fields.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# XapBscDevice.new XapAddress.new('vendor', 'dev', 'hostname'), Xap.random_uid,
|
27
|
+
# [
|
28
|
+
# { :endpoint => 'Input 1', :uid => 1, :State => true },
|
29
|
+
# { :endpoint => 'Output 1', :State => true, :callback => proc { |ep| puts 'Output 1' } }
|
30
|
+
# ]
|
31
|
+
def initialize address, uid, endpoints, interval = 5
|
32
|
+
super address, uid, interval
|
33
|
+
|
34
|
+
# TODO: Make endpoints a hash, with what is currently the
|
35
|
+
# :endpoint field as the hash key, then store the hash key back
|
36
|
+
# into the hash (will simplify calls to XapBscDevice.new, so
|
37
|
+
# the user can type => instead of :endpoint)
|
38
|
+
|
39
|
+
@input_count = 0
|
40
|
+
@output_count = 0
|
41
|
+
@endpoints = {} # Mapping from endpoint name to endpoint hash
|
42
|
+
@uids = []
|
43
|
+
@outputs = [] # Array containing only output endpoints to simplify handling command messages
|
44
|
+
endpoints.each do |ep|
|
45
|
+
add_endpoint ep
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Assigns the handler that owns this device, then sends the initial
|
50
|
+
# state of all input and output blocks as xAPBSC.info messages.
|
51
|
+
def handler= handler
|
52
|
+
super handler
|
53
|
+
announce_endpoints
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets the xAP UID of this virtual device, then sends an xAPBSC.info
|
57
|
+
# message for all endpoints.
|
58
|
+
def uid= uid
|
59
|
+
super uid
|
60
|
+
announce_endpoints
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sets the xAP address of this virtual device, then sends an
|
64
|
+
# xAPBSC.info message for all endpoints.
|
65
|
+
def set_address address
|
66
|
+
super address
|
67
|
+
announce_endpoints
|
68
|
+
end
|
69
|
+
|
70
|
+
# Called when a message targeting this device's address is received.
|
71
|
+
def receive_message msg
|
72
|
+
if msg.is_a? XapBscCommand
|
73
|
+
Xap.log "Command message for #{self}"
|
74
|
+
|
75
|
+
if @output_count > 0
|
76
|
+
eps = []
|
77
|
+
if msg.target_addr.wildcard?
|
78
|
+
@outputs.each do |out|
|
79
|
+
eps << out if msg.target_addr.endpoint_match out[:endpoint]
|
80
|
+
end
|
81
|
+
else
|
82
|
+
ep = @endpoints[msg.target_addr.endpoint.downcase]
|
83
|
+
eps << ep if ep
|
84
|
+
end
|
85
|
+
|
86
|
+
# For each message block, if ID=*, apply the
|
87
|
+
# change to all matched endpoints. If ID!=*,
|
88
|
+
# apply the change to the matching endpoint,
|
89
|
+
# iff that endpoint is in the eps list.
|
90
|
+
msg.each_block do |blk|
|
91
|
+
id = blk.id
|
92
|
+
if id == nil || id == '*'
|
93
|
+
eps.each do |ep|
|
94
|
+
update_endpoint ep, blk
|
95
|
+
end
|
96
|
+
else
|
97
|
+
ep = @uids[id.to_i]
|
98
|
+
if ep && eps.include?(ep)
|
99
|
+
update_endpoint ep, blk
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
elsif msg.is_a? XapBscQuery
|
106
|
+
Xap.log "Query message for #{self}, target #{msg.target_addr}, wildcard #{msg.target_addr.wildcard?}"
|
107
|
+
|
108
|
+
if msg.target_addr.wildcard?
|
109
|
+
@endpoints.each do |name, ep|
|
110
|
+
if msg.target_addr.endpoint_match name
|
111
|
+
Xap.log "Matching endpoint found: #{ep[:endpoint]}"
|
112
|
+
send_info ep
|
113
|
+
end
|
114
|
+
end
|
115
|
+
elsif msg.target_addr.endpoint
|
116
|
+
ep = @endpoints[msg.target_addr.endpoint.to_s.downcase]
|
117
|
+
if ep
|
118
|
+
Xap.log "Matching endpoint found: #{ep[:endpoint]}"
|
119
|
+
send_info ep
|
120
|
+
else
|
121
|
+
Xap.log "No matching endpoint found"
|
122
|
+
end
|
123
|
+
else
|
124
|
+
Xap.log "Error: No endpoint was given in the query"
|
125
|
+
end
|
126
|
+
|
127
|
+
elsif msg.is_a? XapBscInfo
|
128
|
+
Xap.log "Info message for #{self}"
|
129
|
+
|
130
|
+
elsif msg.is_a? XapBscEvent
|
131
|
+
Xap.log "Event message for #{self}"
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Adds a new endpoint hash to the list of endpoints, generating an
|
137
|
+
# xAPBSC.info message if the addition is successful. The endpoint's
|
138
|
+
# name must be unique. UID collision will result in an exception being
|
139
|
+
# raised. If the UID is not specified, the lowest available UID will
|
140
|
+
# be used.
|
141
|
+
#
|
142
|
+
# Summary of endpoint fields:
|
143
|
+
# :endpoint - Name of endpoint - mandatory, must be unique when downcased, String
|
144
|
+
# :uid - UID of endpoint - optional, must be unique, Fixnum 1-254
|
145
|
+
# :callback - Output change callback - mandatory for outputs, may be nil
|
146
|
+
# :State - On/off/? state - mandatory according to xAP BSC spec
|
147
|
+
# :Level - numerator / denominator - optional
|
148
|
+
# :Text - Stream text - optional (mutually exclusive with :Level according to xAP BSC spec)
|
149
|
+
# :DisplayText - UI display text - optional
|
150
|
+
#
|
151
|
+
# Example:
|
152
|
+
# add_endpoint { :endpoint => 'Input 1', :uid => 4, :State => false, :Level => [ 0, 30 ] }
|
153
|
+
def add_endpoint ep
|
154
|
+
unless ep.include?(:endpoint) && ep.include?(:State) && (!ep.include?(:uid) || ep[:uid].is_a?(Fixnum))
|
155
|
+
raise 'An endpoint is missing one or more required fields (:endpoint, :State).'
|
156
|
+
end
|
157
|
+
|
158
|
+
raise "Duplicate endpoint name #{ep[:endpoint]}." if @endpoints.include? ep[:endpoint].downcase
|
159
|
+
|
160
|
+
ep[:uid] ||= find_free_uid
|
161
|
+
raise "Duplicate UID #{ep[:uid]}." if @uids[ep[:uid]]
|
162
|
+
|
163
|
+
# TODO: Additional verification of :Level, :Text, and :DisplayText
|
164
|
+
|
165
|
+
if ep.include? :callback
|
166
|
+
@output_count = @output_count + 1
|
167
|
+
@outputs << ep
|
168
|
+
else
|
169
|
+
@input_count = @input_count + 1
|
170
|
+
end
|
171
|
+
|
172
|
+
@endpoints[ep[:endpoint].downcase] = ep
|
173
|
+
@uids[ep[:uid]] = ep
|
174
|
+
|
175
|
+
send_info ep if @handler
|
176
|
+
end
|
177
|
+
|
178
|
+
# Removes the given endpoint, which may be the endpoint hash or name.
|
179
|
+
# Doesn't verify that the endpoint actually exists.
|
180
|
+
def remove_endpoint ep
|
181
|
+
if ep.is_a? String
|
182
|
+
ep = @endpoints[ep.downcase]
|
183
|
+
end
|
184
|
+
@endpoints.delete ep[:endpoint].downcase
|
185
|
+
@uids[ep[:uid]] = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns the endpoint having the given UID, if any.
|
189
|
+
def uid_endpoint uid
|
190
|
+
@uids[uid]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Finds and returns the lowest-available endpoint UID, or nil if there
|
194
|
+
# are no free endpoint IDs.
|
195
|
+
def find_free_uid
|
196
|
+
for idx in 1..254
|
197
|
+
return idx unless @uids[idx]
|
198
|
+
end
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns the UID for the endpoint with the given name, or nil if no
|
203
|
+
# such endpoint exists.
|
204
|
+
def get_uid endpoint
|
205
|
+
@endpoints[endpoint.downcase][:uid]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the State field of the endpoint with the given name.
|
209
|
+
def get_state endpoint
|
210
|
+
@endpoints[endpoint.downcase][:State]
|
211
|
+
end
|
212
|
+
|
213
|
+
# Sets the State field of the endpoint with the given name. If the new
|
214
|
+
# state is different from the old state, an event message will be
|
215
|
+
# generated. Otherwise, an info message will be generated.
|
216
|
+
def set_state endpoint, state
|
217
|
+
raise 'state must be true, false, or nil.' unless state == true || state == false || state == nil
|
218
|
+
|
219
|
+
ep = @endpoints[endpoint.downcase]
|
220
|
+
old = ep[:State]
|
221
|
+
ep[:State] = state
|
222
|
+
|
223
|
+
if state != old
|
224
|
+
send_event ep
|
225
|
+
else
|
226
|
+
send_info ep
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the Level field of the endpoint with the given name.
|
231
|
+
def get_level endpoint
|
232
|
+
@endpoints[endpoint.downcase][:Level]
|
233
|
+
end
|
234
|
+
|
235
|
+
# Sets the Level field of the endpoint with the given name. If level
|
236
|
+
# is an array, then both the numerator and denominator are replaced.
|
237
|
+
# If level is a Fixnum, only the numerator is replaced. If the new
|
238
|
+
# level is different from the old level, an event message will be
|
239
|
+
# generated. Otherwise, an info message will be generated. Error
|
240
|
+
# checking is not performed on the level parameter. Pass nil to remove
|
241
|
+
# the level from this endpoint.
|
242
|
+
def set_level endpoint, level
|
243
|
+
ep = @endpoints[endpoint.downcase]
|
244
|
+
|
245
|
+
old = ep[:Level]
|
246
|
+
if level != nil
|
247
|
+
level = [ level, ep[:Level][1] ] if level.is_a? Fixnum
|
248
|
+
ep[:Level] = level
|
249
|
+
else
|
250
|
+
ep.delete :Level
|
251
|
+
end
|
252
|
+
|
253
|
+
if level != old
|
254
|
+
send_event ep
|
255
|
+
else
|
256
|
+
send_info ep
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Returns the Text field of the endpoint with the given name.
|
261
|
+
def get_text endpoint
|
262
|
+
@endpoints[endpoint.downcase][:Text]
|
263
|
+
end
|
264
|
+
|
265
|
+
# Sets the Text field of the endpoint with the given name. If the new
|
266
|
+
# state is different from the old state, an event message will be
|
267
|
+
# generated. Otherwise, an info message will be generated.
|
268
|
+
def set_text endpoint, text
|
269
|
+
ep = @endpoints[endpoint.downcase]
|
270
|
+
|
271
|
+
old = ep[:Text]
|
272
|
+
ep[:Text] = old
|
273
|
+
|
274
|
+
if text != old
|
275
|
+
send_event ep
|
276
|
+
else
|
277
|
+
send_info ep
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Returns the DisplayText field of the endpoint with the given name.
|
282
|
+
def get_display_text endpoint
|
283
|
+
@endpoints[endpoint.downcase][:DisplayText]
|
284
|
+
end
|
285
|
+
|
286
|
+
# Sets the DisplayText field of the endpoint with the given name. If
|
287
|
+
# the new state is different from the old state, an event message will
|
288
|
+
# be generated. Otherwise, an info message will be generated.
|
289
|
+
def set_display_text endpoint, text
|
290
|
+
ep = @endpoints[endpoint.downcase]
|
291
|
+
|
292
|
+
old = ep[:DisplayText]
|
293
|
+
ep[:DisplayText] = old
|
294
|
+
|
295
|
+
if text != old
|
296
|
+
send_event ep
|
297
|
+
else
|
298
|
+
send_info ep
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# FIXME: If multiple fields need to change at once, don't send
|
303
|
+
# info/event messages until after all the fields are changed.
|
304
|
+
private
|
305
|
+
# Send an xAPBSC.info message for all endpoints.
|
306
|
+
def announce_endpoints
|
307
|
+
if @endpoints
|
308
|
+
@endpoints.each do |name, ep|
|
309
|
+
send_info ep
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Send an xAPBSC.info message for the given endpoint hash.
|
315
|
+
def send_info ep
|
316
|
+
# Send info message for endpoint (TODO: Store an info
|
317
|
+
# message in the endpoint hash instead of continually
|
318
|
+
# creating new info messages?)
|
319
|
+
msg = XapBscInfo.new(address.for_endpoint(ep[:endpoint]), uid_for(ep[:uid]), !ep.include?(:callback))
|
320
|
+
msg.state = ep[:State] if ep.include? :State
|
321
|
+
msg.level = ep[:Level] if ep.include? :Level
|
322
|
+
msg.text = ep[:Text] if ep.include? :Text
|
323
|
+
msg.display_text = ep[:DisplayText] if ep.include? :DisplayText
|
324
|
+
|
325
|
+
send_message msg
|
326
|
+
end
|
327
|
+
|
328
|
+
# Send an xAPBSC.event message for the given endpoint hash.
|
329
|
+
def send_event ep
|
330
|
+
# Send event message for endpoint (TODO: Store an event
|
331
|
+
# message in the endpoint hash instead of continually
|
332
|
+
# creating new info messages?)
|
333
|
+
msg = XapBscEvent.new(address.for_endpoint(ep[:endpoint]), uid_for(ep[:uid]), !ep.include?(:callback))
|
334
|
+
msg.state = ep[:State] if ep.include? :State
|
335
|
+
msg.level = ep[:Level] if ep.include? :Level
|
336
|
+
msg.text = ep[:Text] if ep.include? :Text
|
337
|
+
msg.display_text = ep[:DisplayText] if ep.include? :DisplayText
|
338
|
+
|
339
|
+
send_message msg
|
340
|
+
end
|
341
|
+
|
342
|
+
# Generates a UID string for the given integer sub-UID between 1 and 254.
|
343
|
+
def uid_for uid
|
344
|
+
sprintf "#{@uid.slice(0,6)}%02X", uid
|
345
|
+
end
|
346
|
+
|
347
|
+
# Updates the given endpoint with values from the given XapBscBlock
|
348
|
+
def update_endpoint ep, block
|
349
|
+
old = ep.clone
|
350
|
+
|
351
|
+
if block.state != nil
|
352
|
+
case block.state
|
353
|
+
when true
|
354
|
+
ep[:State] = true
|
355
|
+
when false
|
356
|
+
ep[:State] = false
|
357
|
+
when 'toggle'
|
358
|
+
ep[:State] = !ep[:State]
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
if ep.include?(:Level) && block.level
|
363
|
+
case block.level[1]
|
364
|
+
when '%', Fixnum
|
365
|
+
div = block.level[1] == '%' ? 100 : block.level[1]
|
366
|
+
ep[:Level][0] = block.level[0] * ep[:Level][1] / div
|
367
|
+
when nil
|
368
|
+
ep[:Level][0] = block.level[0]
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
ep[:Text] = block.text if ep.include?(:Text) && block.text
|
373
|
+
ep[:DisplayText] = block.display_text if ep.include?(:DisplayText) && block.display_text
|
374
|
+
|
375
|
+
if ep != old
|
376
|
+
send_event ep
|
377
|
+
ep[:callback].call(ep)
|
378
|
+
else
|
379
|
+
send_info ep
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Represents an xAP address.
|
2
|
+
# (C)2012 Mike Bourgeous
|
3
|
+
|
4
|
+
module Xap
|
5
|
+
# Represents an xAP address. Matching is case-insensitive.
|
6
|
+
#
|
7
|
+
# Wildcard characters supported: * and >. * matches anything within a
|
8
|
+
# subsection, but will not match across . or : (e.g. a.*.c will match a.b.c but
|
9
|
+
# not a.b.d.c). * must occur by itself in a subsection (e.g. *.a.b and a.*.b
|
10
|
+
# are OK, but ab*.c*d.*e is not). > matches anything to the end of the section
|
11
|
+
# if specified before a :, or anything to the end of an address being tested if
|
12
|
+
# at the end of the wildcarded address.
|
13
|
+
class XapAddress
|
14
|
+
attr_accessor :vendor, :product, :instance, :endpoint, :wildcard, :basewildcard, :epwildcard
|
15
|
+
|
16
|
+
# Parses the given address string into an XapAddress. If addr is nil,
|
17
|
+
# returns nil.
|
18
|
+
def self.parse addr
|
19
|
+
return nil unless addr
|
20
|
+
raise 'addr must be a String' unless addr.is_a? String
|
21
|
+
|
22
|
+
# As far as I can tell, the xAP spec isn't very clear on how
|
23
|
+
# long an address can be, whether the subaddress is specified
|
24
|
+
# by a colon or a period, etc.
|
25
|
+
#
|
26
|
+
# This section says that both instance and subaddr can have any depth
|
27
|
+
# http://www.xapautomation.org/index.php?title=Protocol_definition#Message_Addressing_Schemes
|
28
|
+
#
|
29
|
+
# This section has additional information on addresses
|
30
|
+
# (including vendor and device length limits of 8 characters,
|
31
|
+
# which are broken in many of the xAP BSC examples...)
|
32
|
+
# http://www.xapautomation.org/index.php?title=Protocol_definition#Message_Header_Structure
|
33
|
+
#
|
34
|
+
# This section makes the distinction between : and . less clear
|
35
|
+
# http://www.xapautomation.org/index.php?title=Protocol_definition#Wildcarding_of_Addresses_via_Header
|
36
|
+
#
|
37
|
+
# Here's documentation for an xAP plugin for some other
|
38
|
+
# software that ignores the UID rules entirely and uses the >
|
39
|
+
# wildcard character in the middle of an address
|
40
|
+
# http://www.erspearson.com/xAP/Slim/Manual.html#id616380
|
41
|
+
tokens = addr.strip.split ':', 2
|
42
|
+
addr = tokens[0].split '.', 3
|
43
|
+
subaddr = tokens[1]
|
44
|
+
|
45
|
+
self.new addr[0], addr[1], addr[2], subaddr
|
46
|
+
end
|
47
|
+
|
48
|
+
# vendor - xAP-assigned vendor ID (e.g. ACME)
|
49
|
+
# product - vendor-assigned product name (e.g. Controller)
|
50
|
+
# instance - user-assigned product instance (e.g. Apartment)
|
51
|
+
# endpoint - user- or device-assigned name (e.g. Zone1), or nil
|
52
|
+
def initialize vendor, product, instance, endpoint=nil
|
53
|
+
# This would be a nice application of macros (e.g. "check_type var, String")
|
54
|
+
raise 'vendor must be (convertible to) a String' unless vendor.respond_to? :to_s
|
55
|
+
raise 'product must be (convertible to) a String' unless product.respond_to? :to_s
|
56
|
+
raise 'instance must be (convertible to) a String' unless instance.respond_to? :to_s
|
57
|
+
raise 'endpoint must be (convertible to) a String' unless endpoint.respond_to? :to_s
|
58
|
+
|
59
|
+
# TODO: Validate characters in the address
|
60
|
+
|
61
|
+
@vendor = vendor.to_s
|
62
|
+
@product = product.to_s
|
63
|
+
@instance = instance.to_s
|
64
|
+
@endpoint = endpoint ? endpoint.to_s : nil
|
65
|
+
|
66
|
+
# Many of the xAP standard's own examples violate the length limits...
|
67
|
+
#raise 'vendor is too long' if @vendor.length > 8
|
68
|
+
#raise 'product is too long' if @product.length > 8
|
69
|
+
|
70
|
+
# Build the string representation of this address
|
71
|
+
@str = "#{@vendor}.#{@product}.#{@instance}"
|
72
|
+
@str << ":#{@endpoint}" if @endpoint
|
73
|
+
|
74
|
+
# Build a regex for matching wildcarded addresses
|
75
|
+
raise "Address #{@str} contains * in the middle of a word" if @str =~ /([^.:]\*)|(\*[^.:])/
|
76
|
+
raise "Address #{@str} contains > not at the end of a section" if @str =~ />(?!\:|$)/
|
77
|
+
|
78
|
+
@regex, @wildcard = build_regex @str
|
79
|
+
@baseregex, @basewildcard = build_regex("#{@vendor}.#{@product}.#{@instance}")
|
80
|
+
if @endpoint
|
81
|
+
@epregex, @epwildcard = build_regex(@endpoint)
|
82
|
+
elsif @str.end_with? '>'
|
83
|
+
# FIXME: I believe xAP wildcard addresses are supposed to treat . and : indistinguishably
|
84
|
+
@epregex = //
|
85
|
+
@epwildcard = true
|
86
|
+
else
|
87
|
+
@epregex = /^$/
|
88
|
+
@epwildcard = false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns true if all fields are == when converted to lowercase
|
93
|
+
def == other
|
94
|
+
if other.is_a? XapAddress
|
95
|
+
other.to_s.downcase == to_s.downcase
|
96
|
+
else
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns true if other == self or self is a wildcard address that
|
102
|
+
# matches other (which may either be an XapAddress or anything that can
|
103
|
+
# be converted to a String with to_s). Note that (self =~ other) !=
|
104
|
+
# (other =~ self).
|
105
|
+
def =~ other
|
106
|
+
other == self || (other.to_s =~ @regex) == 0
|
107
|
+
end
|
108
|
+
alias_method :match, :'=~'
|
109
|
+
|
110
|
+
# Returns an XapAddress that contains the base of this address with no
|
111
|
+
# endpoint. Wildcards in the base address components are preserved.
|
112
|
+
def base
|
113
|
+
XapAddress.new @vendor, @product, @instance
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a new XapAddress that contains this address's base components
|
117
|
+
# with the given endpoint.
|
118
|
+
def for_endpoint ep_name
|
119
|
+
XapAddress.new @vendor, @product, @instance, ep_name
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns true if this address's base components (i.e. everything
|
123
|
+
# before the colon) match the base components of the given other
|
124
|
+
# address. If matching a wildcarded address, call this on the
|
125
|
+
# wildcarded address (e.g. wild.base_match(other) rather than
|
126
|
+
# other.base_match(wild).
|
127
|
+
def base_match other
|
128
|
+
other.base.to_s =~ @baseregex
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns true if this address's endpoint matches the endpoint of the
|
132
|
+
# given other address. If other is a String, then it will be matched
|
133
|
+
# as if it were an XapAddress endpoint.
|
134
|
+
def endpoint_match other
|
135
|
+
if other.is_a? String
|
136
|
+
other =~ @epregex
|
137
|
+
elsif other.respond_to?(:endpoint)
|
138
|
+
other.endpoint =~ @epregex
|
139
|
+
else
|
140
|
+
false
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns a correctly-formatted string representation of the address,
|
145
|
+
# suitable for inclusion in an xAP message.
|
146
|
+
def to_s
|
147
|
+
@str
|
148
|
+
end
|
149
|
+
|
150
|
+
# Whether this address is a wildcard address.
|
151
|
+
def wildcard?
|
152
|
+
@wildcard
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
# Builds a regular expression for a wildcarded xAP string
|
157
|
+
def build_regex str
|
158
|
+
# FIXME: escape all regex characters with Regexp.escape, without breaking * wildcard
|
159
|
+
regex = str.gsub '.', '\\.'
|
160
|
+
wildcard = !!(str =~ /[*>]/)
|
161
|
+
if wildcard
|
162
|
+
regex = regex.gsub /(?<=\\\.|^)\*(?=\\\.|$)/, '\\1[^.:]*'
|
163
|
+
regex = regex.gsub />:/, '[^:]*:'
|
164
|
+
regex = regex.gsub />$/, '.*'
|
165
|
+
end
|
166
|
+
return Regexp.new("^#{regex}$", Regexp::IGNORECASE), wildcard
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|