furi 0.0.2 → 0.2.3

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
- SHA1:
3
- metadata.gz: 208063d16947ce85a4d80a12bd3834cb26c0f6ab
4
- data.tar.gz: ab3eff6b787f0df3e04781908477bb69d3924333
2
+ SHA256:
3
+ metadata.gz: 37ea1203ca474e34d99624eb7e7cf43935197a4cec1a0a49e110b8674f5a1cec
4
+ data.tar.gz: 5ef6f167bd64ac72f6a8b78324c871a88734f06e39faecd4e7aaee1abe5a2f0a
5
5
  SHA512:
6
- metadata.gz: 500c3e4bb3ed0866c0cef1ca68794d9d82e9d2b679bb1590f69e14428bb1020a64d5c357c166e25b0c522902b670e5138269e649f395455f48acc4bca50785eb
7
- data.tar.gz: bae87908fe1a04c26d1073d569199adda1645bc25f561ec0aa9b2e05943e55d0993223c3d329335c07afcac8c814e87c2a738573a2837be2996cb38339e3078f
6
+ metadata.gz: db8b15ea5e9d25852df4be4e83a67bf60d8ed85d703fba1fa7a1ee28fbe7d2e547dc4ea182d52ef396316f92ef3634264750b4d76d0016c51c6a9afe78b19e11
7
+ data.tar.gz: 9617142f8e989e885c8bd17a874fa221b6df1f2bea1398a786e9291f4d87d9d6ae4913f8904c3273902b8c0a1d008b0ec3f0aa8918aef34d9d5538fca93a0861
@@ -0,0 +1,19 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ ruby: [2.4, 2.5, 2.6, 2.7, 3.0]
11
+ name: Ruby ${{ matrix.ruby }}
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - run: bundle exec rake
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # v0.2.3
2
+
3
+ * Ruby 3.0 support #2
4
+ * Escaping special characters in anchor
5
+
data/Gemfile CHANGED
@@ -1,4 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in furi.gemspec
4
6
  gemspec
7
+
8
+ gem "pry-byebug", "~> 3.9"
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.10"
11
+ gem "bump"
data/README.md CHANGED
@@ -1,12 +1,19 @@
1
1
  # Furi
2
2
 
3
- TODO: Write a gem description
3
+ [![Gem Version](https://badge.fury.io/rb/furi.svg)](https://badge.fury.io/rb/furi)
4
+ [![Build Status](https://github.com/bogdan/furi/workflows/CI/badge.svg?branch=master)](https://github.com/bogdan/furi/actions)
5
+ [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbogdan%2Ffuri.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbogdan%2Ffuri?ref=badge_shield)
6
+
7
+ Furi is a Friendly URI parsing library.
8
+ Furi's philosophy is to make any operation possible in ONE LINE OF CODE.
9
+
10
+ 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
11
 
5
12
  ## Installation
6
13
 
7
14
  Add this line to your application's Gemfile:
8
15
 
9
- ```ruby
16
+ ``` ruby
10
17
  gem 'furi'
11
18
  ```
12
19
 
@@ -20,12 +27,112 @@ Or install it yourself as:
20
27
 
21
28
  ## Usage
22
29
 
23
- TODO: Write usage instructions here
30
+ I'll say it again: any operation should take exacly one line of code!
31
+ Here are basic:
32
+
33
+ ### Utility Methods
34
+
35
+ Parsing the URI fragments:
36
+
37
+ ``` ruby
38
+ Furi.host("http://gusiev.com") # => "gusiev.com"
39
+ Furi.port("http://gusiev.com") # => nil
40
+ Furi.port!("http://gusiev.com") # => 80
41
+ ```
42
+
43
+ Updating the URI parts:
44
+
45
+ ``` ruby
46
+ Furi.update("http://gusiev.com", protocol: '') # => "//gusiev.com"
47
+ Furi.update("http://gusiev.com?source=google", query: {email: "a@b.com"})
48
+ # => "http://gusiev.com?source=google&email=a@b.com"
49
+ Furi.replace("http://gusiev.com?source=google", query: {email: "a@b.com"})
50
+ # => "http://gusiev.com?email=a@b.com"
51
+
52
+ Furi.defaults("http://gusiev.com", subdomain: 'www') # => "http://www.gusiev.com"
53
+ Furi.defaults("http://blog.gusiev.com", subdomain: 'www') # => "http://blog.gusiev.com"
54
+ ```
55
+
56
+ Building an URI from initial parts:
57
+
58
+ ``` ruby
59
+ Furi.build(protocol: '//', host: 'gusiev.com', path: '/assets/application.js')
60
+ # => "//gusiev.com/assets/application.js"
61
+ ```
62
+
63
+ ### Working with Object
64
+
65
+ ``` ruby
66
+ uri = Furi.parse("gusiev.com")
67
+ # => #<Furi::Uri "gusiev.com">
68
+
69
+ uri.port # => nil
70
+ uri.port! # => 80
71
+ uri.path # => nil
72
+ uri.path! # => '/'
73
+ uri.subdomain ||= 'www'
74
+ uri.protocol = "//" # protocol abstract URL
75
+ ```
76
+
77
+ ### Processing Query String
78
+
79
+ ``` ruby
80
+ uri = Furi.parse("/?person[first_name]=Bogdan&person[last_name]=Gusiev")
81
+
82
+ uri.query_string # => "person[first_name]=Bogdan&person[last_name]=Gusiev"
83
+ uri.query_tokens # => [person[first_name]=Bogdan, person[last_name]=Gusiev]
84
+ uri.query # => {person: {first_name: Bogdan, last_name: 'Gusiev'}}
85
+
86
+ uri.merge_query(person: {email: 'a@b.com'})
87
+ # => {person: {email: 'a@b.com', first_name: Bogdan, last_name: 'Gusiev'}}
88
+
89
+ uri.merge_query(person: {email: 'a@b.com'})
90
+ # => {person: {email: 'a@b.com', first_name: Bogdan, last_name: 'Gusiev'}}
91
+ ```
92
+
93
+ ## Reference
94
+
95
+ ```
96
+ location resource
97
+ | ___|___
98
+ _______|_______ / \
99
+ / \ / \
100
+ / authority request \
101
+ / __________|_________ | \
102
+ / / \ ______|______ \
103
+ / userinfo hostinfo / \ \
104
+ / __|___ ___|___ / \ \
105
+ / / \ / \ / \ \
106
+ / username password host port path query anchor
107
+ / __|___ __|__ ______|______ | _________|__________ ____|____ |
108
+ / / \ / \ / \ / \/ \ / \ / \
109
+ http://username:zhongguo@www.example.com:80/hello/world/article.html?name=bogdan#info
110
+ \_/ \_/ \___/ \_/ \__________/\ / \_/
111
+ | | | | | \___/ |
112
+ protocol subdomain | domainzone directory | extension
113
+ | | filename |
114
+ domainname / \_____/
115
+ \___/ |
116
+ | file
117
+ domain
118
+ ```
119
+
120
+ Originated from [URI.js](http://medialize.github.io/URI.js/about-uris.html) parsing library.
121
+ Giving credit...
122
+
123
+ ## TODO
124
+
125
+ * Improve URI.join algorithm to match the one used in Addressable library
126
+ * Implement filename
127
+ * Encoding/Decoding special characters:
128
+ * path
129
+ * query
130
+ * fragment
24
131
 
25
132
  ## Contributing
26
133
 
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
134
+ Contribute in the way you want. Branch names and other bla-bla-bla do not matter.
135
+
136
+ ## License
137
+
138
+ [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbogdan%2Ffuri.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbogdan%2Ffuri?ref=badge_large)
data/Rakefile CHANGED
@@ -1,2 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
2
7
 
8
+ task default: :spec
data/furi.gemspec CHANGED
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'furi/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/furi/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "furi"
@@ -9,17 +8,17 @@ Gem::Specification.new do |spec|
9
8
  spec.authors = ["Bogdan Gusiev"]
10
9
  spec.email = ["agresso@gmail.com"]
11
10
  spec.summary = %q{Make URI processing as easy as it should be}
12
- spec.description = %q{The phylosophy of this gem is to make any URI modification or parsing operation to take only one line of code and never more}
13
- spec.homepage = ""
11
+ spec.description = %q{The philosophy of this gem is to make any URI modification or parsing operation to take only one line of code and never more}
12
+ spec.homepage = "https://github.com/bogdan/furi"
14
13
  spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
20
19
 
21
- spec.add_development_dependency "bundler", "~> 1.7"
22
- spec.add_development_dependency "rake", "~> 10.0"
23
- spec.add_development_dependency "rspec"
24
- spec.add_development_dependency "byebug"
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
22
+ end
23
+ spec.require_paths = ["lib"]
25
24
  end
data/lib/furi.rb CHANGED
@@ -3,49 +3,73 @@ require "uri"
3
3
 
4
4
  module Furi
5
5
 
6
- PARTS = [
7
- :anchor, :protocol, :query_string,
8
- :path, :hostname, :port, :username, :password
6
+ autoload :QueryToken, 'furi/query_token'
7
+ autoload :Uri, 'furi/uri'
8
+ autoload :Utils, 'furi/utils'
9
+
10
+ ESSENTIAL_PARTS = [
11
+ :anchor, :protocol, :query_tokens,
12
+ :path, :host, :port, :username, :password
13
+ ]
14
+ COMBINED_PARTS = [
15
+ :hostinfo, :userinfo, :authority, :ssl, :domain, :domainname,
16
+ :domainzone, :request, :location, :query,
17
+ :directory, :extension, :file
9
18
  ]
19
+ PARTS = ESSENTIAL_PARTS + COMBINED_PARTS
20
+
10
21
  ALIASES = {
11
- protocol: [:schema],
22
+ protocol: [:schema, :scheme],
12
23
  anchor: [:fragment],
24
+ host: [:hostname],
25
+ username: [:user],
26
+ request: [:request_uri]
13
27
  }
14
28
 
15
- DELEGATES = [:port!]
16
-
17
- PORT_MAPPING = {
18
- "http" => 80,
19
- "https" => 443,
20
- "ftp" => 21,
21
- "tftp" => 69,
22
- "sftp" => 22,
23
- "ssh" => 22,
24
- "svn+ssh" => 22,
25
- "telnet" => 23,
26
- "nntp" => 119,
27
- "gopher" => 70,
28
- "wais" => 210,
29
- "ldap" => 389,
30
- "prospero" => 1525
29
+ DELEGATES = [:port!, :host!, :path!, :home_page?]
30
+
31
+ PROTOCOLS = {
32
+ "http" => {port: 80, ssl: false},
33
+ "https" => {port: 443, ssl: true},
34
+ "ftp" => {port: 21},
35
+ "tftp" => {port: 69},
36
+ "sftp" => {port: 22},
37
+ "ssh" => {port: 22, ssl: true},
38
+ "svn" => {port: 3690},
39
+ "svn+ssh" => {port: 22, ssl: true},
40
+ "telnet" => {port: 23},
41
+ "nntp" => {port: 119},
42
+ "gopher" => {port: 70},
43
+ "wais" => {port: 210},
44
+ "ldap" => {port: 389},
45
+ "prospero" => {port: 1525},
46
+ "file" => {port: nil},
47
+ "postgres" => {port: 5432},
48
+ "mysql" => {port: 3306},
49
+ "mailto" => {port: nil}
31
50
  }
32
51
 
33
- class Expressions
34
- attr_accessor :protocol
35
52
 
36
- def initialize
37
- @protocol = /^[a-z][a-z0-9.+-]*$/i
38
- end
39
- end
53
+ SSL_MAPPING = {
54
+ 'http' => 'https',
55
+ 'ftp' => 'sftp',
56
+ 'svn' => 'svn+ssh',
57
+ }
40
58
 
41
- def self.expressions
42
- Expressions.new
43
- end
59
+ WEB_PROTOCOL = ['http', 'https']
60
+
61
+ ROOT = '/'
44
62
 
45
- def self.parse(argument)
46
- Uri.new(argument)
63
+ # Parses a given string and return an URL object
64
+ # Optionally accepts parts to update the parsed URL object
65
+ def self.parse(argument, parts = nil)
66
+ Uri.new(argument).update(parts)
47
67
  end
48
68
 
69
+ # Builds an URL from given parts
70
+ #
71
+ # Furi.build(path: "/dashboard", host: 'example.com', protocol: "https")
72
+ # # => "https://example.com/dashboard"
49
73
  def self.build(argument)
50
74
  Uri.new(argument).to_s
51
75
  end
@@ -53,14 +77,119 @@ module Furi
53
77
  class << self
54
78
  (PARTS + ALIASES.values.flatten + DELEGATES).each do |part|
55
79
  define_method(part) do |string|
56
- Uri.new(string).send(part)
80
+ Uri.new(string)[part]
57
81
  end
58
82
  end
59
83
  end
60
84
 
85
+ # Replaces a given URL string with given parts
86
+ #
87
+ # Furi.update("http://gusiev.com", protocol: 'https', subdomain: 'www')
88
+ # # => "https://www.gusiev.com"
61
89
  def self.update(string, parts)
62
90
  parse(string).update(parts).to_s
63
91
  end
92
+ class << self
93
+ alias :merge :update
94
+ end
95
+
96
+ # Puts the default values for given URL that are not defined
97
+ #
98
+ # Furi.defaults("gusiev.com/hello.html", protocol: 'http', path: '/index.html')
99
+ # # => "http://gusiev.com/hello.html"
100
+ def self.defaults(string, parts)
101
+ parse(string).defaults(parts).to_s
102
+ end
103
+
104
+ # Replaces a given URL string with given parts.
105
+ # Same as update but works different for URL query parameter:
106
+ # replaces newly specified parameters instead of merging to existing ones
107
+ #
108
+ # Furi.update("/hello.html?a=1", host: 'gusiev.com', query: {b: 2})
109
+ # # => "gusiev.com/hello.html?a=1&b=2"
110
+ #
111
+ def self.replace(string, parts)
112
+ parse(string).replace(parts).to_s
113
+ end
114
+
115
+
116
+
117
+ # Parses a query into nested paramters hash using a rack convension with square brackets.
118
+ #
119
+ # Furi.parse_query("a[]=1&a[]=2") # => {a: [1,2]}
120
+ # Furi.parse_query("p[email]=a&a[two]=2") # => {a: {one: 1, two: 2}}
121
+ # Furi.parse_query("p[one]=1&a[two]=2") # => {a: {one: 1, two: 2}}
122
+ # Furi.serialize({p: {name: 'Bogdan Gusiev', email: 'bogdan@example.com', data: {one: 1, two: 2}}})
123
+ # # => "p%5Bname%5D=Bogdan&p%5Bemail%5D=bogdan%40example.com&p%5Bdata%5D%5Bone%5D=1&p%5Bdata%5D%5Btwo%5D=2"
124
+ def self.parse_query(query)
125
+ return Furi::Utils.stringify_keys(query) if query.is_a?(Hash)
126
+
127
+ params = {}
128
+ query_tokens(query).each do |token|
129
+ parse_query_token(params, token.name, token.value)
130
+ end
131
+
132
+ return params
133
+ end
134
+
135
+ # Parses query key/value pairs from query string and returns them raw
136
+ # without organising them into hashes and without normalising them.
137
+ #
138
+ # Furi.query_tokens("a=1&b=2").map {|k,v| "#{k} -> #{v}"} # => ['a -> 1', 'b -> 2']
139
+ # Furi.query_tokens("a=1&a=1&a=2").map {|k,v| "#{k} -> #{v}"} # => ['a -> 1', 'a -> 1', 'a -> 2']
140
+ # Furi.query_tokens("name=Bogdan&email=bogdan%40example.com") # => [name=Bogdan, email=bogdan@example.com]
141
+ # Furi.query_tokens("a[one]=1&a[two]=2") # => [a[one]=1, a[two]=2]
142
+ def self.query_tokens(query)
143
+ case query
144
+ when Enumerable, Enumerator
145
+ query.map do |token|
146
+ QueryToken.parse(token)
147
+ end
148
+ when nil, ''
149
+ []
150
+ when String
151
+ query.gsub(/\A\?/, '').split(/[&;] */n, -1).map do |p|
152
+ QueryToken.parse(p)
153
+ end
154
+ else
155
+ raise QueryParseError, "can not parse #{query.inspect} query tokens"
156
+ end
157
+ end
158
+
159
+ # Serializes query parameters into query string.
160
+ # Optionaly accepts a basic name space.
161
+ #
162
+ # Furi.serialize({a: 1, b: 2}) # => "a=1&b=2"
163
+ # Furi.serialize({a: [1,2]}) # => "a[]=1&a[]=2"
164
+ # Furi.serialize({a: {b: 1, c:2}}) # => "a[b]=1&a[c]=2"
165
+ # Furi.serialize({name: 'Bogdan', email: 'bogdan@example.com'}, "person")
166
+ # # => "person[name]=Bogdan&person[email]=bogdan%40example.com"
167
+ #
168
+ def self.serialize(query, namespace = nil)
169
+ serialize_tokens(query, namespace).join("&")
170
+ end
171
+
172
+ def self.join(*uris)
173
+ uris.map do |uri|
174
+ Furi.parse(uri)
175
+ end.reduce do |memo, uri|
176
+ memo.send(:join, uri)
177
+ end
178
+ end
179
+
180
+ class Error < StandardError
181
+ end
182
+
183
+ class FormattingError < Error
184
+ end
185
+
186
+ class ParseError < Error
187
+ end
188
+
189
+ class QueryParseError < Error
190
+ end
191
+
192
+ protected
64
193
 
65
194
  def self.serialize_tokens(query, namespace = nil)
66
195
  case query
@@ -77,13 +206,13 @@ module Furi
77
206
  result
78
207
  when Array
79
208
  if namespace.nil? || namespace.empty?
80
- raise ArgumentError, "Can not serialize Array without namespace"
209
+ raise FormattingError, "Can not serialize Array without namespace"
81
210
  end
82
211
 
83
212
  namespace = "#{namespace}[]"
84
213
  query.map do |item|
85
214
  if item.is_a?(Array)
86
- raise ArgumentError, "Can not serialize #{item.inspect} as element of an Array"
215
+ raise FormattingError, "Can not serialize #{item.inspect} as element of an Array"
87
216
  end
88
217
  serialize_tokens(item, namespace)
89
218
  end
@@ -96,37 +225,6 @@ module Furi
96
225
  end
97
226
  end
98
227
 
99
- def self.parse_nested_query(qs)
100
-
101
- params = {}
102
- query_tokens(qs).each do |token|
103
- parse_query_token(params, token.name, token.value)
104
- end
105
-
106
- return params
107
- end
108
-
109
- def self.query_tokens(query)
110
- if query.is_a?(Array)
111
- query.map do |token|
112
- case token
113
- when QueryToken
114
- token
115
- when String
116
- QueryToken.parse(token)
117
- when Array
118
- QueryToken.new(*token)
119
- else
120
- raise ArgumentError, "Can not parse query token #{token.inspect}"
121
- end
122
- end
123
- else
124
- (query || '').split(/[&;] */n).map do |p|
125
- QueryToken.parse(p)
126
- end
127
- end
128
- end
129
-
130
228
  def self.parse_query_token(params, name, value)
131
229
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
132
230
  namespace = $1 || ''
@@ -140,14 +238,14 @@ module Furi
140
238
  elsif after == "[]"
141
239
  current ||= []
142
240
  unless current.is_a?(Array)
143
- raise TypeError, "expected Array (got #{current.class}) for param `#{namespace}'"
241
+ raise QueryParseError, "expected Array (got #{current.class}) for param `#{namespace}'"
144
242
  end
145
243
  current << value
146
244
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
147
245
  child_key = $1
148
246
  current ||= []
149
247
  unless current.is_a?(Array)
150
- raise TypeError, "expected Array (got #{current.class}) for param `#{namespace}'"
248
+ raise QueryParseError, "expected Array (got #{current.class}) for param `#{namespace}'"
151
249
  end
152
250
  if current.last.is_a?(Hash) && !current.last.key?(child_key)
153
251
  parse_query_token(current.last, child_key, value)
@@ -157,7 +255,7 @@ module Furi
157
255
  else
158
256
  current ||= {}
159
257
  unless current.is_a?(Hash)
160
- raise TypeError, "expected Hash (got #{current.class}) for param `#{namespace}'"
258
+ raise QueryParseError, "expected Hash (got #{current.class}) for param `#{namespace}'"
161
259
  end
162
260
  current = parse_query_token(current, after, value)
163
261
  end
@@ -166,228 +264,4 @@ module Furi
166
264
  return params
167
265
  end
168
266
 
169
- def self.serialize(string, namespace = nil)
170
- serialize_tokens(string, namespace).join("&")
171
- end
172
-
173
- class QueryToken
174
- attr_reader :name, :value
175
-
176
- def self.parse(token)
177
- k,v = token.split('=', 2).map { |s| ::URI.decode_www_form_component(s) }
178
- new(k,v)
179
- end
180
-
181
- def initialize(name, value)
182
- @name = name
183
- @value = value
184
- end
185
-
186
- def to_a
187
- [name, value]
188
- end
189
-
190
- def to_s
191
- "#{::URI.encode_www_form_component(name.to_s)}=#{::URI.encode_www_form_component(value.to_s)}"
192
- end
193
-
194
- def inspect
195
- [name, value].join('=')
196
- end
197
- end
198
-
199
- class Uri
200
-
201
- attr_reader(*PARTS)
202
-
203
- ALIASES.each do |origin, aliases|
204
- aliases.each do |aliaz|
205
- define_method(aliaz) do
206
- send(origin)
207
- end
208
-
209
- define_method(:"#{aliaz}=") do |*args|
210
- send(:"#{origin}=", *args)
211
- end
212
- end
213
- end
214
-
215
- def initialize(argument)
216
- case argument
217
- when String
218
- parse_uri_string(argument)
219
- when Hash
220
- update(argument)
221
- end
222
- end
223
-
224
- def update(parts)
225
- parts.each do |part, value|
226
- send(:"#{part}=", value)
227
- end
228
- self
229
- end
230
-
231
- def merge(parts)
232
- parts.each do |part, value|
233
-
234
- end
235
- end
236
-
237
- def userinfo
238
- if username
239
- [username, password].compact.join(":")
240
- elsif password
241
- raise FormattingError, "can not build URI with password but without username"
242
- else
243
- nil
244
- end
245
- end
246
-
247
- def host
248
- if hostname
249
- [hostname, explicit_port].compact.join(":")
250
- elsif port
251
- raise FormattingError, "can not build URI with port but without hostname"
252
- else
253
- nil
254
- end
255
- end
256
-
257
- def host=(string)
258
- @port = nil
259
- if string.include?(":")
260
- string, @port = string.split(":", 2)
261
- @port = @port.to_i
262
- end
263
- @hostname = string.empty? ? nil : string
264
- end
265
-
266
- def to_s
267
- result = []
268
- if protocol
269
- result.push(protocol.empty? ? "//" : "#{protocol}://")
270
- end
271
- if userinfo
272
- result << userinfo
273
- end
274
- result << host if host
275
- result << path
276
- if query_string
277
- result << "?" << query_string
278
- end
279
- if anchor
280
- result << "#" << anchor
281
- end
282
- result.join
283
- end
284
-
285
- def query
286
- return @query if query_level?
287
- @query = Furi.parse_nested_query(@query_string)
288
- end
289
-
290
-
291
- def query=(value)
292
- @query = nil
293
- case value
294
- when String
295
- @query_string = value
296
- when Array
297
- @query = Furi.query_tokens(value)
298
- when Hash
299
- @query = value
300
- when nil
301
- else
302
- raise ArgumentError, 'Query can only be Hash or String'
303
- end
304
- end
305
-
306
- def hostname=(hostname)
307
- @hostname = hostname
308
- end
309
-
310
- def port=(port)
311
- @port = port.to_i
312
- if @port == 0
313
- raise ArgumentError, "port should be an Integer > 0"
314
- end
315
- @port
316
- end
317
-
318
- def username=(username)
319
- @username = username
320
- end
321
-
322
- def password=(password)
323
- @password = password
324
- end
325
-
326
- def protocol=(protocol)
327
- @protocol = protocol ? protocol.gsub(%r{:/?/?\Z}, "") : nil
328
- end
329
-
330
- def query_string
331
- return @query_string unless query_level?
332
- Furi.serialize(@query)
333
- end
334
-
335
- def expressions
336
- Furi.expressions
337
- end
338
-
339
- def port!
340
- port || default_port
341
- end
342
-
343
- def default_port
344
- protocol ? PORT_MAPPING[protocol] : nil
345
- end
346
-
347
- protected
348
-
349
- def query_level?
350
- !!@query
351
- end
352
-
353
- def explicit_port
354
- port == default_port ? nil : port
355
- end
356
-
357
- def parse_uri_string(string)
358
- string, *@anchor = string.split("#")
359
- @anchor = @anchor.empty? ? nil : @anchor.join("#")
360
- if string.include?("?")
361
- string, query_string = string.split("?", 2)
362
- @query_string = query_string
363
- end
364
-
365
- if string.include?("://")
366
- @protocol, string = string.split(":", 2)
367
- @protocol = '' if @protocol.empty?
368
- end
369
- if string.start_with?("//")
370
- @protocol ||= ''
371
- string = string[2..-1]
372
- end
373
- parse_authority(string)
374
- end
375
-
376
- def parse_authority(string)
377
- if string.include?("/")
378
- string, @path = string.split("/", 2)
379
- @path = "/" + @path
380
- end
381
-
382
- if string.include?("@")
383
- userinfo, string = string.split("@", 2)
384
- @username, @password = userinfo.split(":", 2)
385
- end
386
- self.host = string
387
- end
388
-
389
- end
390
-
391
- class FormattingError < StandardError
392
- end
393
267
  end