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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 37ea1203ca474e34d99624eb7e7cf43935197a4cec1a0a49e110b8674f5a1cec
|
4
|
+
data.tar.gz: 5ef6f167bd64ac72f6a8b78324c871a88734f06e39faecd4e7aaee1abe5a2f0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
# Furi
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/furi)
|
4
|
+
[](https://github.com/bogdan/furi/actions)
|
5
|
+
[](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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
134
|
+
Contribute in the way you want. Branch names and other bla-bla-bla do not matter.
|
135
|
+
|
136
|
+
## License
|
137
|
+
|
138
|
+
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fbogdan%2Ffuri?ref=badge_large)
|
data/Rakefile
CHANGED
data/furi.gemspec
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
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
|
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.
|
17
|
-
spec.
|
18
|
-
spec.
|
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.
|
22
|
-
|
23
|
-
|
24
|
-
spec.
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
18
|
-
"http" => 80,
|
19
|
-
"https" => 443,
|
20
|
-
"ftp" => 21,
|
21
|
-
"tftp" => 69,
|
22
|
-
"sftp" => 22,
|
23
|
-
"ssh" => 22,
|
24
|
-
"svn
|
25
|
-
"
|
26
|
-
"
|
27
|
-
"
|
28
|
-
"
|
29
|
-
"
|
30
|
-
"
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
53
|
+
SSL_MAPPING = {
|
54
|
+
'http' => 'https',
|
55
|
+
'ftp' => 'sftp',
|
56
|
+
'svn' => 'svn+ssh',
|
57
|
+
}
|
40
58
|
|
41
|
-
|
42
|
-
|
43
|
-
|
59
|
+
WEB_PROTOCOL = ['http', 'https']
|
60
|
+
|
61
|
+
ROOT = '/'
|
44
62
|
|
45
|
-
|
46
|
-
|
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)
|
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
|
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
|
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
|
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
|
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
|
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
|