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 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