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