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 +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
|
+
[![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
|
-
|
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
|
+
[![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
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
|