furi 0.1.0 → 0.2.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 +4 -4
- data/README.md +105 -7
- data/lib/furi.rb +101 -374
- data/lib/furi/query_token.rb +57 -0
- data/lib/furi/uri.rb +533 -0
- data/lib/furi/utils.rb +16 -0
- data/lib/furi/version.rb +1 -1
- data/spec/furi_spec.rb +339 -46
- metadata +5 -2
@@ -0,0 +1,57 @@
|
|
1
|
+
module Furi
|
2
|
+
class QueryToken
|
3
|
+
attr_reader :name, :value
|
4
|
+
|
5
|
+
def self.parse(token)
|
6
|
+
case token
|
7
|
+
when QueryToken
|
8
|
+
token
|
9
|
+
when String
|
10
|
+
key, value = token.split('=', 2).map do |s|
|
11
|
+
::URI.decode_www_form_component(s)
|
12
|
+
end
|
13
|
+
key ||= ""
|
14
|
+
new(key, value)
|
15
|
+
when Array
|
16
|
+
QueryToken.new(*token)
|
17
|
+
else
|
18
|
+
raise_parse_error(token)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.raise_parse_error(token)
|
23
|
+
raise ArgumentError, "Can not parse query token #{token.inspect}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name, value)
|
27
|
+
@name = name
|
28
|
+
@value = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
[name, value]
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
other = self.class.parse(other)
|
37
|
+
return false unless other
|
38
|
+
to_s == other.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
encoded_key = ::URI.encode_www_form_component(name.to_s)
|
43
|
+
|
44
|
+
!value.nil? ?
|
45
|
+
"#{encoded_key}=#{::URI.encode_www_form_component(value.to_s)}" :
|
46
|
+
encoded_key
|
47
|
+
end
|
48
|
+
|
49
|
+
def as_json(options = nil)
|
50
|
+
to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
[name, value].join('=')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/furi/uri.rb
ADDED
@@ -0,0 +1,533 @@
|
|
1
|
+
module Furi
|
2
|
+
class Uri
|
3
|
+
|
4
|
+
attr_reader(*Furi::ESSENTIAL_PARTS)
|
5
|
+
|
6
|
+
Furi::ALIASES.each do |origin, aliases|
|
7
|
+
aliases.each do |aliaz|
|
8
|
+
define_method(aliaz) do
|
9
|
+
self[origin]
|
10
|
+
end
|
11
|
+
|
12
|
+
define_method(:"#{aliaz}=") do |arg|
|
13
|
+
self[origin] = arg
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(argument)
|
19
|
+
@query_tokens = []
|
20
|
+
case argument
|
21
|
+
when String
|
22
|
+
parse_uri_string(argument)
|
23
|
+
when Hash
|
24
|
+
update(argument)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(parts)
|
29
|
+
parts.each do |part, value|
|
30
|
+
self[part] = value
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def merge(parts)
|
36
|
+
parts.each do |part, value|
|
37
|
+
case part.to_sym
|
38
|
+
when :query, :query_tokens
|
39
|
+
merge_query(value)
|
40
|
+
else
|
41
|
+
self[part] = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def defaults(parts)
|
48
|
+
parts.each do |part, value|
|
49
|
+
case part.to_sym
|
50
|
+
when :query, :query_tokens
|
51
|
+
Furi.parse_query(value).each do |key, default_value|
|
52
|
+
unless query.key?(key)
|
53
|
+
query[key] = default_value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
else
|
57
|
+
unless self[part]
|
58
|
+
self[part] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def merge_query(query)
|
66
|
+
case query
|
67
|
+
when Hash
|
68
|
+
self.query = self.query.merge(Furi::Utils.stringify_keys(query))
|
69
|
+
when String, Array
|
70
|
+
self.query_tokens += Furi.query_tokens(query)
|
71
|
+
when nil
|
72
|
+
else
|
73
|
+
raise ArgumentError, "#{query.inspect} can not be merged"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def userinfo
|
78
|
+
if username
|
79
|
+
[username, password].compact.join(":")
|
80
|
+
elsif password
|
81
|
+
raise Furi::FormattingError, "can not build URI with password but without username"
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def host=(host)
|
88
|
+
@host = case host
|
89
|
+
when Array
|
90
|
+
join_domain(host)
|
91
|
+
when "", nil
|
92
|
+
nil
|
93
|
+
else
|
94
|
+
host.to_s
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def domainzone
|
99
|
+
parsed_host.last
|
100
|
+
end
|
101
|
+
|
102
|
+
def domainzone=(new_zone)
|
103
|
+
self.host = [subdomain, domainname, new_zone]
|
104
|
+
end
|
105
|
+
|
106
|
+
def domainname
|
107
|
+
parsed_host[1]
|
108
|
+
end
|
109
|
+
|
110
|
+
def domainname=(new_domainname)
|
111
|
+
self.domain = join_domain([subdomain, new_domainname, domainzone])
|
112
|
+
end
|
113
|
+
|
114
|
+
def domain
|
115
|
+
join_domain(parsed_host[1..2].flatten)
|
116
|
+
end
|
117
|
+
|
118
|
+
def domain=(new_domain)
|
119
|
+
self.host= [subdomain, new_domain]
|
120
|
+
end
|
121
|
+
|
122
|
+
def subdomain
|
123
|
+
parsed_host.first
|
124
|
+
end
|
125
|
+
|
126
|
+
def subdomain=(new_subdomain)
|
127
|
+
self.host = [new_subdomain, domain]
|
128
|
+
end
|
129
|
+
|
130
|
+
def hostinfo
|
131
|
+
return host unless explicit_port?
|
132
|
+
if port && !host
|
133
|
+
raise Furi::FormattingError, "can not build URI with port but without host"
|
134
|
+
end
|
135
|
+
[host, port].join(":")
|
136
|
+
end
|
137
|
+
|
138
|
+
def hostinfo=(string)
|
139
|
+
if string.match(%r{\A\[.+\]\z}) #ipv6 host
|
140
|
+
self.host = string
|
141
|
+
else
|
142
|
+
if match = string.match(/\A(.+):(.*)\z/)
|
143
|
+
self.host, self.port = match.captures
|
144
|
+
else
|
145
|
+
self.host = string
|
146
|
+
self.port = nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def authority
|
152
|
+
return hostinfo unless userinfo
|
153
|
+
[userinfo, hostinfo].join("@")
|
154
|
+
end
|
155
|
+
|
156
|
+
def authority=(string)
|
157
|
+
if string.include?("@")
|
158
|
+
userinfo, string = string.split("@", 2)
|
159
|
+
self.userinfo = userinfo
|
160
|
+
else
|
161
|
+
self.userinfo = nil
|
162
|
+
end
|
163
|
+
self.hostinfo = string
|
164
|
+
end
|
165
|
+
|
166
|
+
def to_s
|
167
|
+
result = []
|
168
|
+
result << location
|
169
|
+
result << (host ? path : path!)
|
170
|
+
if query_tokens.any?
|
171
|
+
result << "?" << query_string
|
172
|
+
end
|
173
|
+
if anchor
|
174
|
+
result << "#" << anchor
|
175
|
+
end
|
176
|
+
result.join
|
177
|
+
end
|
178
|
+
|
179
|
+
def location
|
180
|
+
if protocol
|
181
|
+
unless host
|
182
|
+
raise Furi::FormattingError, "can not build URI with protocol but without host"
|
183
|
+
end
|
184
|
+
[protocol.empty? ? "" : "#{protocol}:", authority].join("//")
|
185
|
+
else
|
186
|
+
authority
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def location=(string)
|
191
|
+
string ||= ""
|
192
|
+
string = string.gsub(%r(/\Z), '')
|
193
|
+
self.protocol = nil
|
194
|
+
string = parse_protocol(string)
|
195
|
+
self.authority = string
|
196
|
+
end
|
197
|
+
|
198
|
+
def request
|
199
|
+
result = []
|
200
|
+
result << path!
|
201
|
+
result << "?" << query_string if query_tokens.any?
|
202
|
+
result.join
|
203
|
+
end
|
204
|
+
|
205
|
+
def request=(string)
|
206
|
+
string = parse_anchor_and_query(string)
|
207
|
+
self.path = string
|
208
|
+
end
|
209
|
+
|
210
|
+
def home_page?
|
211
|
+
path! == Furi::ROOT || path! == "/index.html"
|
212
|
+
end
|
213
|
+
|
214
|
+
def query
|
215
|
+
return @query if query_level?
|
216
|
+
@query = Furi.parse_query(query_tokens)
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
def query=(value)
|
221
|
+
@query = nil
|
222
|
+
@query_tokens = []
|
223
|
+
case value
|
224
|
+
when String, Array
|
225
|
+
self.query_tokens = value
|
226
|
+
when Hash
|
227
|
+
self.query_tokens = value
|
228
|
+
@query = value
|
229
|
+
when nil
|
230
|
+
else
|
231
|
+
raise ArgumentError, 'Query can only be Hash or String'
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def port=(port)
|
236
|
+
@port = case port
|
237
|
+
when String
|
238
|
+
if port.empty?
|
239
|
+
nil
|
240
|
+
else
|
241
|
+
unless port =~ /\A\s*\d+\s*\z/
|
242
|
+
raise ArgumentError, "port should be an Integer >= 0"
|
243
|
+
end
|
244
|
+
port.to_i
|
245
|
+
end
|
246
|
+
when Integer
|
247
|
+
if port < 0
|
248
|
+
raise ArgumentError, "port should be an Integer >= 0"
|
249
|
+
end
|
250
|
+
port
|
251
|
+
when nil
|
252
|
+
nil
|
253
|
+
else
|
254
|
+
raise ArgumentError, "can not parse port: #{port.inspect}"
|
255
|
+
end
|
256
|
+
@port
|
257
|
+
end
|
258
|
+
|
259
|
+
def query_tokens=(tokens)
|
260
|
+
@query = nil
|
261
|
+
@query_tokens = Furi.query_tokens(tokens)
|
262
|
+
end
|
263
|
+
|
264
|
+
def username=(username)
|
265
|
+
@username = username.nil? ? nil : username.to_s
|
266
|
+
end
|
267
|
+
|
268
|
+
def password=(password)
|
269
|
+
@password = password.nil? ? nil : password.to_s
|
270
|
+
end
|
271
|
+
|
272
|
+
def userinfo=(userinfo)
|
273
|
+
username, password = (userinfo || "").split(":", 2)
|
274
|
+
self.username = username
|
275
|
+
self.password = password
|
276
|
+
end
|
277
|
+
|
278
|
+
def path=(path)
|
279
|
+
@path = path.to_s
|
280
|
+
if !@path.empty? && !@path.start_with?("/")
|
281
|
+
@path = "/" + @path
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def protocol=(protocol)
|
286
|
+
@protocol = protocol ? protocol.gsub(%r{:?/?/?\Z}, "") : nil
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
def directory
|
291
|
+
path_tokens[0..-2].join("/")
|
292
|
+
end
|
293
|
+
|
294
|
+
def directory=(string)
|
295
|
+
string ||= "/"
|
296
|
+
if filename && string !~ %r{/\z}
|
297
|
+
string += '/'
|
298
|
+
end
|
299
|
+
self.path = string + filename.to_s
|
300
|
+
end
|
301
|
+
|
302
|
+
def extension
|
303
|
+
return nil unless filename
|
304
|
+
file_tokens.size > 1 ? file_tokens.last : nil
|
305
|
+
end
|
306
|
+
|
307
|
+
def extension=(string)
|
308
|
+
tokens = file_tokens
|
309
|
+
case tokens.size
|
310
|
+
when 0
|
311
|
+
raise Furi::FormattingError, "can not assign extension when there is no filename"
|
312
|
+
when 1
|
313
|
+
tokens.push(string)
|
314
|
+
else
|
315
|
+
if string
|
316
|
+
tokens[-1] = string
|
317
|
+
else
|
318
|
+
tokens.pop
|
319
|
+
end
|
320
|
+
end
|
321
|
+
self.filename = tokens.join(".")
|
322
|
+
end
|
323
|
+
|
324
|
+
def filename=(name)
|
325
|
+
unless name
|
326
|
+
return unless path
|
327
|
+
else
|
328
|
+
name = name.gsub(%r{\A/}, "")
|
329
|
+
end
|
330
|
+
|
331
|
+
self.path = path_tokens.tap do |p|
|
332
|
+
filename_index = [p.size-1, 0].max
|
333
|
+
p[filename_index] = name
|
334
|
+
end.join("/")
|
335
|
+
end
|
336
|
+
|
337
|
+
def path_tokens
|
338
|
+
return [] unless path
|
339
|
+
path.split("/", -1)
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
def query_string
|
344
|
+
if query_level?
|
345
|
+
Furi.serialize(query)
|
346
|
+
else
|
347
|
+
query_tokens.any? ? query_tokens.join("&") : nil
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def query_string!
|
352
|
+
query_string || ""
|
353
|
+
end
|
354
|
+
|
355
|
+
def query_string=(string)
|
356
|
+
self.query_tokens = string.to_s
|
357
|
+
end
|
358
|
+
|
359
|
+
def port!
|
360
|
+
port || default_port
|
361
|
+
end
|
362
|
+
|
363
|
+
def default_port
|
364
|
+
protocol && Furi::PROTOCOLS[protocol] ? Furi::PROTOCOLS[protocol][:port] : nil
|
365
|
+
end
|
366
|
+
|
367
|
+
def ssl?
|
368
|
+
!!(protocol && Furi::PROTOCOLS[protocol][:ssl])
|
369
|
+
end
|
370
|
+
|
371
|
+
def ssl
|
372
|
+
ssl?
|
373
|
+
end
|
374
|
+
|
375
|
+
def ssl=(ssl)
|
376
|
+
self.protocol = find_protocol_for_ssl(ssl)
|
377
|
+
end
|
378
|
+
|
379
|
+
def filename
|
380
|
+
result = path_tokens.last
|
381
|
+
result == "" ? nil : result
|
382
|
+
end
|
383
|
+
|
384
|
+
def filename!
|
385
|
+
filename || ''
|
386
|
+
end
|
387
|
+
|
388
|
+
def default_web_port?
|
389
|
+
Furi::WEB_PROTOCOL.any? do |web_protocol|
|
390
|
+
Furi::PROTOCOLS[web_protocol][:port] == port!
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def web_protocol?
|
395
|
+
Furi::WEB_PROTOCOL.include?(protocol)
|
396
|
+
end
|
397
|
+
|
398
|
+
def resource
|
399
|
+
[request, anchor].compact.join("#")
|
400
|
+
end
|
401
|
+
|
402
|
+
def resource=(value)
|
403
|
+
self.anchor = nil
|
404
|
+
self.query_tokens = []
|
405
|
+
self.path = nil
|
406
|
+
value = parse_anchor_and_query(value)
|
407
|
+
self.path = value
|
408
|
+
end
|
409
|
+
|
410
|
+
def path!
|
411
|
+
path || Furi::ROOT
|
412
|
+
end
|
413
|
+
|
414
|
+
def resource!
|
415
|
+
[request]
|
416
|
+
end
|
417
|
+
|
418
|
+
def host!
|
419
|
+
host || ""
|
420
|
+
end
|
421
|
+
|
422
|
+
def ==(other)
|
423
|
+
to_s == other.to_s
|
424
|
+
end
|
425
|
+
|
426
|
+
def inspect
|
427
|
+
"#<#{self.class} #{to_s.inspect}>"
|
428
|
+
end
|
429
|
+
|
430
|
+
def anchor=(string)
|
431
|
+
string = string.to_s
|
432
|
+
@anchor = string.empty? ? nil : string
|
433
|
+
end
|
434
|
+
|
435
|
+
def [](part)
|
436
|
+
send(part)
|
437
|
+
end
|
438
|
+
|
439
|
+
def []=(part, value)
|
440
|
+
send(:"#{part}=", value)
|
441
|
+
end
|
442
|
+
|
443
|
+
def rfc3986?
|
444
|
+
uri = to_s
|
445
|
+
!!(uri.match(URI::RFC3986_Parser::RFC3986_Parser) ||
|
446
|
+
uri.match(URI::RFC3986_Parser::RFC3986_relative_ref))
|
447
|
+
end
|
448
|
+
|
449
|
+
protected
|
450
|
+
|
451
|
+
def file_tokens
|
452
|
+
filename ? filename.split('.') : []
|
453
|
+
end
|
454
|
+
|
455
|
+
def query_level?
|
456
|
+
!!@query
|
457
|
+
end
|
458
|
+
|
459
|
+
def explicit_port?
|
460
|
+
port && port != default_port
|
461
|
+
end
|
462
|
+
|
463
|
+
def parse_uri_string(string)
|
464
|
+
string = parse_anchor_and_query(string)
|
465
|
+
|
466
|
+
string = parse_protocol(string)
|
467
|
+
|
468
|
+
if string.include?("/")
|
469
|
+
string, path = string.split("/", 2)
|
470
|
+
self.path = "/" + path
|
471
|
+
end
|
472
|
+
|
473
|
+
self.authority = string
|
474
|
+
end
|
475
|
+
|
476
|
+
def find_protocol_for_ssl(ssl)
|
477
|
+
if Furi::SSL_MAPPING.key?(protocol)
|
478
|
+
ssl ? Furi::SSL_MAPPING[protocol] : protocol
|
479
|
+
elsif Furi::SSL_MAPPING.values.include?(protocol)
|
480
|
+
ssl ? protocol : Furi::SSL_MAPPING.invert[protocol]
|
481
|
+
else
|
482
|
+
raise ArgumentError, "Can not specify SSL for #{protocol.inspect} protocol"
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def join_domain(tokens)
|
487
|
+
tokens = tokens.compact
|
488
|
+
tokens.any? ? tokens.join(".") : nil
|
489
|
+
end
|
490
|
+
|
491
|
+
def parse_anchor_and_query(string)
|
492
|
+
string ||= ''
|
493
|
+
string, *anchor = string.split("#")
|
494
|
+
self.anchor = anchor.join("#")
|
495
|
+
if string && string.include?("?")
|
496
|
+
string, query_string = string.split("?", 2)
|
497
|
+
self.query_tokens = query_string
|
498
|
+
end
|
499
|
+
string
|
500
|
+
end
|
501
|
+
|
502
|
+
def parse_protocol(string)
|
503
|
+
if string.include?("://")
|
504
|
+
protocol, string = string.split(":", 2)
|
505
|
+
self.protocol = protocol
|
506
|
+
end
|
507
|
+
if string.start_with?("//")
|
508
|
+
self.protocol ||= ''
|
509
|
+
string = string[2..-1]
|
510
|
+
end
|
511
|
+
string
|
512
|
+
end
|
513
|
+
|
514
|
+
def parsed_host
|
515
|
+
return @parsed_host if @parsed_host
|
516
|
+
tokens = host_tokens
|
517
|
+
zone = []
|
518
|
+
subdomain = []
|
519
|
+
while tokens.any? && tokens.last.size <= 3 && tokens.size >= 2
|
520
|
+
zone.unshift tokens.pop
|
521
|
+
end
|
522
|
+
while tokens.size > 1
|
523
|
+
subdomain << tokens.shift
|
524
|
+
end
|
525
|
+
domainname = tokens.first
|
526
|
+
@parsed_host = [join_domain(subdomain), domainname, join_domain(zone)]
|
527
|
+
end
|
528
|
+
|
529
|
+
def host_tokens
|
530
|
+
host.split(".")
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|