furi 0.1.0 → 0.2.0

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