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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62a4088dd18cb4ac9857d1512d78497ea28edd82
4
- data.tar.gz: 3603fa3688b41b7b2ba700676db29f930bb11bf3
3
+ metadata.gz: 6b454ab79a6ac9b95fc85b8b8b2381c8e93715af
4
+ data.tar.gz: d84272345cd55a7aad224ce2b124423806f38301
5
5
  SHA512:
6
- metadata.gz: 762e38c65d27a36323bf0ed410b25633e95788ce10a8a97163195918a47e038610defe4fe8f7f5b635d554c42b09cc3c5d4fbc1974dee9ec5d4e645d646923c1
7
- data.tar.gz: 59f5e066fb0f09bcac768b99b11589d4257f391352e27d4d6874c2ae1d2d783f8bc56fde491906669b5aed5d22f16c17a8f424cbab486bd2b3123243c7b38873
6
+ metadata.gz: cc4227555a121e3c25d7c4ba32765659fa78678ff7aa2b500f0f6cbe384f9507e11bac3828bfd9eae097879e786395ac9b8113049447b5d1a1aff6a834a91108
7
+ data.tar.gz: 4b8d902ab961bff2d68a369da05c96f1cc142e6d71980a3cd24691de930807057b65016aa9ec16e8d7e52e2cdd5173068d108ac2d0128ced4ac9d497e671adbb
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Furi
2
2
 
3
- TODO: Write a gem description
3
+ Furi is a Friendly URI parsing library.
4
+ Furi's philosophy is to make any operation possible in ONE LINE OF CODE.
5
+
6
+ If there is an operation that takes more than one line of code to do with Furi, this is considered a terrible bug and you should create an issue.
4
7
 
5
8
  ## Installation
6
9
 
@@ -20,12 +23,107 @@ Or install it yourself as:
20
23
 
21
24
  ## Usage
22
25
 
23
- TODO: Write usage instructions here
26
+ I'll say it again: any operation should take exacly one line of code!
27
+ Here are basic:
28
+
29
+ ### Utility Methods
30
+
31
+
32
+ ``` ruby
33
+ Furi.host("http://gusiev.com") # => "gusiev.com"
34
+ Furi.port("http://gusiev.com") # => nil
35
+ Furi.port!("http://gusiev.com") # => 80
36
+
37
+ Furi.update("http://gusiev.com", protocol: '') # => "//gusiev.com"
38
+ Furi.update("http://gusiev.com?source=google", query: {email: "a@b.com"})
39
+ # => "http://gusiev.com?email=a@b.com"
40
+ Furi.merge("http://gusiev.com?source=google", query: {email: "a@b.com"})
41
+ # => "http://gusiev.com?source=google&email=a@b.com"
42
+
43
+ Furi.build(protocol: '//', host: 'gusiev.com', path: '/assets/application.js')
44
+ # => "//gusiev.com/assets/application.js"
45
+
46
+ Furi.defaults("http://gusiev.com", subdomain: 'www') # => "http://www.gusiev.com"
47
+ ```
48
+
49
+ ### Working with Object
50
+
51
+ ``` ruby
52
+ uri = Furi.parse("gusiev.com")
53
+ # => #<Furi::Uri "gusiev.com">
54
+
55
+ uri.port # => nil
56
+ uri.port! # => 80
57
+ uri.path # => nil
58
+ uri.path! # => '/'
59
+ uri.subdomain ||= 'www'
60
+ uri.protocol = "//" # protocol abstract URL
61
+ ```
62
+
63
+ ### Processing Query String
64
+
65
+ ``` ruby
66
+ uri = Furi.parse("/?person[first_name]=Bogdan&person[last_name]=Gusiev")
67
+
68
+ uri.query_string # => "person[first_name]=Bogdan&person[last_name]=Gusiev"
69
+ uri.query_tokens # => [person[first_name]=Bogdan, person[last_name]=Gusiev]
70
+ uri.query # => {person: {first_name: Bogdan, last_name: 'Gusiev'}}
71
+
72
+ uri.merge_query(person: {email: 'a@b.com'})
73
+ # => {person: {email: 'a@b.com', first_name: Bogdan, last_name: 'Gusiev'}}
74
+
75
+ uri.merge_query(person: {email: 'a@b.com'})
76
+ # => {person: {email: 'a@b.com', first_name: Bogdan, last_name: 'Gusiev'}}
77
+ ```
78
+
79
+ ## Reference
80
+
81
+ ```
82
+ location resource
83
+ | ___|___
84
+ _______|_______ / \
85
+ / \ / \
86
+ / authority request \
87
+ / __________|_________ | \
88
+ / / \ ______|______ \
89
+ / userinfo hostinfo / \ \
90
+ / __|___ ___|___ / \ \
91
+ / / \ / \ / \ \
92
+ / username password host port path query anchor
93
+ / __|___ __|__ ______|______ | _________|__________ ____|____ |
94
+ / / \ / \ / \ / \/ \ / \ / \
95
+ http://username:zhongguo@www.example.com:80/hello/world/article.html?name=bogdan#info
96
+ \_/ \_/ \___/ \_/ \__________/ \ \_/
97
+ | | | | | \ |
98
+ protocol subdomain | domainzone directory \ extension
99
+ | | \_____/
100
+ domainname / |
101
+ \___/ filename
102
+ |
103
+ domain
104
+ ```
105
+
106
+
107
+ Originated from [URI.js](http://medialize.github.io/URI.js/about-uris.html) parsing library.
108
+ Giving credit...
109
+
110
+
111
+ ## TODO
112
+
113
+ * rfc3986 validation
114
+ * mailto protocol
115
+ * escaping in path
116
+ * case insensetivity:
117
+ * domain
118
+ * protocol
119
+ * case sensitivity:
120
+ * path
121
+ * query
122
+ * anchor
123
+ * basic auth data ?
24
124
 
25
125
  ## Contributing
26
126
 
27
- 1. Fork it ( https://github.com/[my-github-username]/furi/fork )
28
- 2. Create your feature branch (`git checkout -b my-new-feature`)
29
- 3. Commit your changes (`git commit -am 'Add some feature'`)
30
- 4. Push to the branch (`git push origin my-new-feature`)
31
- 5. Create a new Pull Request
127
+ Contribute in the way you want. Branch names and other bla-bla-bla do not matter.
128
+
129
+
@@ -3,18 +3,30 @@ require "uri"
3
3
 
4
4
  module Furi
5
5
 
6
- PARTS = [
6
+ autoload :QueryToken, 'furi/query_token'
7
+ autoload :Uri, 'furi/uri'
8
+ autoload :Utils, 'furi/utils'
9
+
10
+ ESSENTIAL_PARTS = [
7
11
  :anchor, :protocol, :query_tokens,
8
12
  :path, :host, :port, :username, :password
9
13
  ]
14
+ COMBINED_PARTS = [
15
+ :hostinfo, :userinfo, :authority, :ssl, :domain, :domainname,
16
+ :domainzone, :request, :location, :query,
17
+ :extension, :filename
18
+ ]
19
+ PARTS = ESSENTIAL_PARTS + COMBINED_PARTS
20
+
10
21
  ALIASES = {
11
22
  protocol: [:schema, :scheme],
12
23
  anchor: [:fragment],
13
24
  host: [:hostname],
14
25
  username: [:user],
26
+ request: [:request_uri]
15
27
  }
16
28
 
17
- DELEGATES = [:port!, :host!]
29
+ DELEGATES = [:port!, :host!, :path!, :home_page?]
18
30
 
19
31
  PROTOCOLS = {
20
32
  "http" => {port: 80, ssl: false},
@@ -32,6 +44,7 @@ module Furi
32
44
  "prospero" => {port: 1525},
33
45
  }
34
46
 
47
+
35
48
  SSL_MAPPING = {
36
49
  'http' => 'https',
37
50
  'ftp' => 'sftp',
@@ -42,22 +55,15 @@ module Furi
42
55
 
43
56
  ROOT = '/'
44
57
 
45
- class Expressions
46
- attr_accessor :protocol
47
-
48
- def initialize
49
- @protocol = /^[a-z][a-z0-9.+-]*$/i
50
- end
51
- end
52
-
53
- def self.expressions
54
- Expressions.new
55
- end
56
-
58
+ # Parses a given string and return an URL object
57
59
  def self.parse(argument)
58
60
  Uri.new(argument)
59
61
  end
60
62
 
63
+ # Builds an URL from given parts
64
+ #
65
+ # Furi.build(path: "/dashboard", host: 'example.com', protocol: "https")
66
+ # # => "https://example.com/dashboard"
61
67
  def self.build(argument)
62
68
  Uri.new(argument).to_s
63
69
  end
@@ -65,19 +71,99 @@ module Furi
65
71
  class << self
66
72
  (PARTS + ALIASES.values.flatten + DELEGATES).each do |part|
67
73
  define_method(part) do |string|
68
- Uri.new(string).send(part)
74
+ Uri.new(string)[part]
69
75
  end
70
76
  end
71
77
  end
72
78
 
79
+ # Replaces a given URL string with given parts
80
+ #
81
+ # Furi.update("http://gusiev.com", protocol: 'https', subdomain: 'www')
82
+ # # => "https://www.gusiev.com"
73
83
  def self.update(string, parts)
74
84
  parse(string).update(parts).to_s
75
85
  end
76
86
 
87
+ # Puts the default values for given URL that are not defined
88
+ #
89
+ # Furi.defaults("gusiev.com/hello.html", protocol: 'http', path: '/index.html')
90
+ # # => "http://gusiev.com/hello.html"
91
+ def self.defaults(string, parts)
92
+ parse(string).defaults(parts).to_s
93
+ end
94
+
95
+ # Replaces a given URL string with given parts.
96
+ # Same as update but works different for URL query parameter:
97
+ # merges newly specified parameters instead of replacing existing ones
98
+ #
99
+ # Furi.merge("/hello.html?a=1", host: 'gusiev.com', query: {b: 2})
100
+ # # => "gusiev.com/hello.html?a=1&b=2"
101
+ #
77
102
  def self.merge(string, parts)
78
103
  parse(string).merge(parts).to_s
79
104
  end
80
105
 
106
+
107
+ # Parses a query into nested paramters hash using a rack convension with square brackets.
108
+ #
109
+ # Furi.parse_query("a[]=1&a[]=2") # => {a: [1,2]}
110
+ # Furi.parse_query("p[email]=a&a[two]=2") # => {a: {one: 1, two: 2}}
111
+ # Furi.parse_query("p[one]=1&a[two]=2") # => {a: {one: 1, two: 2}}
112
+ # Furi.serialize({p: {name: 'Bogdan Gusiev', email: 'bogdan@example.com', data: {one: 1, two: 2}}})
113
+ # # => "p%5Bname%5D=Bogdan&p%5Bemail%5D=bogdan%40example.com&p%5Bdata%5D%5Bone%5D=1&p%5Bdata%5D%5Btwo%5D=2"
114
+ def self.parse_query(query)
115
+ return Furi::Utils.stringify_keys(query) if query.is_a?(Hash)
116
+
117
+ params = {}
118
+ query_tokens(query).each do |token|
119
+ parse_query_token(params, token.name, token.value)
120
+ end
121
+
122
+ return params
123
+ end
124
+
125
+ # Parses query key/value pairs from query string and returns them raw
126
+ # without organising them into hashes and without normalising them.
127
+ #
128
+ # Furi.query_tokens("a=1&b=2").map {|k,v| "#{k} -> #{v}"} # => ['a -> 1', 'b -> 2']
129
+ # Furi.query_tokens("a=1&a=1&a=2").map {|k,v| "#{k} -> #{v}"} # => ['a -> 1', 'a -> 1', 'a -> 2']
130
+ # Furi.query_tokens("name=Bogdan&email=bogdan%40example.com") # => [name=Bogdan, email=bogdan@example.com]
131
+ # Furi.query_tokens("a[one]=1&a[two]=2") # => [a[one]=1, a[two]=2]
132
+ def self.query_tokens(query)
133
+ case query
134
+ when Enumerable, Enumerator
135
+ query.map do |token|
136
+ QueryToken.parse(token)
137
+ end
138
+ when nil, ''
139
+ []
140
+ when String
141
+ query.gsub(/\A\?/, '').split(/[&;] */n, -1).map do |p|
142
+ QueryToken.parse(p)
143
+ end
144
+ else
145
+ raise ArgumentError, "Can not parse #{query.inspect} query tokens"
146
+ end
147
+ end
148
+
149
+ # Serializes query parameters into query string.
150
+ # Optionaly accepts a basic name space.
151
+ #
152
+ # Furi.serialize({a: 1, b: 2}) # => "a=1&b=2"
153
+ # Furi.serialize({a: [1,2]}) # => "a[]=1&a[]=2"
154
+ # Furi.serialize({a: {b: 1, c:2}}) # => "a[b]=1&a[c]=2"
155
+ # Furi.serialize({name: 'Bogdan', email: 'bogdan@example.com'}, "person")
156
+ # # => "person[name]=Bogdan&person[email]=bogdan%40example.com"
157
+ #
158
+ def self.serialize(query, namespace = nil)
159
+ serialize_tokens(query, namespace).join("&")
160
+ end
161
+
162
+ class FormattingError < StandardError
163
+ end
164
+
165
+ protected
166
+
81
167
  def self.serialize_tokens(query, namespace = nil)
82
168
  case query
83
169
  when Hash
@@ -112,42 +198,6 @@ module Furi
112
198
  end
113
199
  end
114
200
 
115
- def self.parse_nested_query(qs)
116
-
117
- params = {}
118
- query_tokens(qs).each do |token|
119
- parse_query_token(params, token.name, token.value)
120
- end
121
-
122
- return params
123
- end
124
-
125
- def self.query_tokens(query)
126
- case query
127
- when Enumerable, Enumerator
128
- query.map do |token|
129
- case token
130
- when QueryToken
131
- token
132
- when String
133
- QueryToken.parse(token)
134
- when Array
135
- QueryToken.new(*token)
136
- else
137
- raise ArgumentError, "Can not parse query token #{token.inspect}"
138
- end
139
- end
140
- when nil, ''
141
- []
142
- when String
143
- query.gsub(/\A\?/, '').split(/[&;] */n).map do |p|
144
- QueryToken.parse(p)
145
- end
146
- else
147
- raise ArgumentError, "Can not parse #{query.inspect} query tokens"
148
- end
149
- end
150
-
151
201
  def self.parse_query_token(params, name, value)
152
202
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
153
203
  namespace = $1 || ''
@@ -187,327 +237,4 @@ module Furi
187
237
  return params
188
238
  end
189
239
 
190
- def self.serialize(query, namespace = nil)
191
- serialize_tokens(query, namespace).join("&")
192
- end
193
-
194
- class QueryToken
195
- attr_reader :name, :value
196
-
197
- def self.parse(token)
198
- k,v = token.split('=', 2).map { |s| ::URI.decode_www_form_component(s) }
199
- new(k,v)
200
- end
201
-
202
- def initialize(name, value)
203
- @name = name
204
- @value = value
205
- end
206
-
207
- def to_a
208
- [name, value]
209
- end
210
-
211
- def ==(other)
212
- to_s == other.to_s
213
- end
214
-
215
- def to_s
216
- "#{::URI.encode_www_form_component(name.to_s)}=#{::URI.encode_www_form_component(value.to_s)}"
217
- end
218
-
219
- def inspect
220
- [name, value].join('=')
221
- end
222
- end
223
-
224
- class Uri
225
-
226
- attr_reader(*PARTS)
227
-
228
- ALIASES.each do |origin, aliases|
229
- aliases.each do |aliaz|
230
- define_method(aliaz) do
231
- send(origin)
232
- end
233
-
234
- define_method(:"#{aliaz}=") do |*args|
235
- send(:"#{origin}=", *args)
236
- end
237
- end
238
- end
239
-
240
- def initialize(argument)
241
- @query_tokens = []
242
- case argument
243
- when String
244
- parse_uri_string(argument)
245
- when Hash
246
- update(argument)
247
- end
248
- end
249
-
250
- def update(parts)
251
- parts.each do |part, value|
252
- send(:"#{part}=", value)
253
- end
254
- self
255
- end
256
-
257
- def merge(parts)
258
- parts.each do |part, value|
259
- case part.to_sym
260
- when :query
261
- merge_query(value)
262
- else
263
- send(:"#{part}=", value)
264
- end
265
- end
266
- self
267
- end
268
-
269
- def merge_query(query)
270
- case query
271
- when Hash
272
- self.query = self.query.merge(Furi::Utils.stringify_keys(query))
273
- when String, Array
274
- self.query_tokens += Furi.query_tokens(query)
275
- when nil
276
- else
277
- raise ArgumentError, "#{query.inspect} can not be merged"
278
- end
279
- end
280
-
281
- def userinfo
282
- if username
283
- [username, password].compact.join(":")
284
- elsif password
285
- raise FormattingError, "can not build URI with password but without username"
286
- else
287
- nil
288
- end
289
- end
290
-
291
- def host=(host)
292
- @host = host
293
- end
294
-
295
- def to_s
296
- result = []
297
- if protocol
298
- result.push(protocol.empty? ? "//" : "#{protocol}://")
299
- end
300
- if userinfo
301
- result << userinfo
302
- end
303
- result << host if host
304
- result << ":" << port if explicit_port
305
- result << (host ? path : path!)
306
- if query_tokens.any?
307
- result << "?" << query_string
308
- end
309
- if anchor
310
- result << "#" << anchor
311
- end
312
- result.join
313
- end
314
-
315
-
316
- def request
317
- result = []
318
- result << path!
319
- result << "?" << query_string if query_tokens.any?
320
- result.join
321
- end
322
-
323
- def request_uri
324
- request
325
- end
326
-
327
- def query
328
- return @query if query_level?
329
- @query = Furi.parse_nested_query(query_tokens)
330
- end
331
-
332
-
333
- def query=(value)
334
- @query = nil
335
- @query_tokens = []
336
- case value
337
- when String, Array
338
- @query_tokens = Furi.query_tokens(value)
339
- when Hash
340
- @query = value
341
- @query_tokens = Furi.serialize_tokens(value)
342
- when nil
343
- else
344
- raise ArgumentError, 'Query can only be Hash or String'
345
- end
346
- end
347
-
348
- def port=(port)
349
- if port != nil
350
- @port = port.to_i
351
- if @port == 0
352
- raise ArgumentError, "port should be an Integer > 0"
353
- end
354
- else
355
- @port = nil
356
- end
357
- @port
358
- end
359
-
360
- def query_tokens=(tokens)
361
- @query = nil
362
- @query_tokens = tokens
363
- end
364
-
365
- def username=(username)
366
- @username = username.nil? ? nil : username.to_s
367
- end
368
-
369
- def password=(password)
370
- @password = password.nil? ? nil : password.to_s
371
- end
372
-
373
- def path=(path)
374
- @path = path.to_s
375
- end
376
-
377
- def protocol=(protocol)
378
- @protocol = protocol ? protocol.gsub(%r{:/?/?\Z}, "") : nil
379
- end
380
-
381
- def query_string
382
- if query_level?
383
- Furi.serialize(@query)
384
- else
385
- query_tokens.join("&")
386
- end
387
- end
388
-
389
- def expressions
390
- Furi.expressions
391
- end
392
-
393
- def port!
394
- port || default_port
395
- end
396
-
397
- def default_port
398
- protocol && PROTOCOLS[protocol] ? PROTOCOLS[protocol][:port] : nil
399
- end
400
-
401
- def ssl?
402
- !!(protocol && PROTOCOLS[protocol][:ssl])
403
- end
404
-
405
- def ssl
406
- ssl?
407
- end
408
-
409
- def ssl=(ssl)
410
- self.protocol = find_protocol_for_ssl(ssl)
411
- end
412
-
413
- def filename
414
- path.split("/").last
415
- end
416
-
417
- def default_web_port?
418
- WEB_PROTOCOL.any? do |web_protocol|
419
- PROTOCOLS[web_protocol][:port] == port!
420
- end
421
- end
422
-
423
- def web_protocol?
424
- WEB_PROTOCOL.include?(protocol)
425
- end
426
-
427
- def resource
428
- [request, anchor].compact.join("#")
429
- end
430
-
431
- def path!
432
- path || ROOT
433
- end
434
-
435
- def host!
436
- host || ""
437
- end
438
-
439
- def ==(other)
440
- to_s == other.to_s
441
- end
442
-
443
- protected
444
-
445
- def query_level?
446
- !!@query
447
- end
448
-
449
- def explicit_port
450
- port == default_port ? nil : port
451
- end
452
-
453
- def parse_uri_string(string)
454
- string, *@anchor = string.split("#")
455
- @anchor = @anchor.empty? ? nil : @anchor.join("#")
456
- if string.include?("?")
457
- string, query_string = string.split("?", 2)
458
- self.query_tokens = Furi.query_tokens(query_string)
459
- end
460
-
461
- if string.include?("://")
462
- @protocol, string = string.split(":", 2)
463
- @protocol = '' if @protocol.empty?
464
- end
465
- if string.start_with?("//")
466
- @protocol ||= ''
467
- string = string[2..-1]
468
- end
469
- parse_authority(string)
470
- end
471
-
472
- def parse_authority(string)
473
- if string.include?("/")
474
- string, @path = string.split("/", 2)
475
- @path = "/" + @path
476
- end
477
-
478
- if string.include?("@")
479
- userinfo, string = string.split("@", 2)
480
- @username, @password = userinfo.split(":", 2)
481
- end
482
- host, port = string.split(":", 2)
483
- self.host = host if host
484
- self.port = port if port
485
- end
486
-
487
- def find_protocol_for_ssl(ssl)
488
- if SSL_MAPPING.key?(protocol)
489
- ssl ? SSL_MAPPING[protocol] : protocol
490
- elsif SSL_MAPPING.values.include?(protocol)
491
- ssl ? protocol : SSL_MAPPING.invert[protocol]
492
- else
493
- raise ArgumentError, "Can not specify ssl for #{protocol.inspect} protocol"
494
- end
495
- end
496
-
497
- end
498
-
499
- class FormattingError < StandardError
500
- end
501
-
502
- class Utils
503
- class << self
504
- def stringify_keys(hash)
505
- result = {}
506
- hash.each_key do |key|
507
- result[key.to_s] = hash[key]
508
- end
509
- result
510
- end
511
- end
512
- end
513
240
  end