iri 0.10.0 → 0.11.1
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 +4 -4
- data/.0pdd.yml +2 -21
- data/.github/workflows/actionlint.yml +4 -22
- data/.github/workflows/codecov.yml +5 -22
- data/.github/workflows/copyrights.yml +5 -22
- data/.github/workflows/markdown-lint.yml +5 -22
- data/.github/workflows/pdd.yml +4 -21
- data/.github/workflows/rake.yml +4 -23
- data/.github/workflows/reuse.yml +19 -0
- data/.github/workflows/typos.yml +19 -0
- data/.github/workflows/xcop.yml +4 -21
- data/.github/workflows/yamllint.yml +4 -21
- data/.gitignore +7 -2
- data/.rubocop.yml +5 -24
- data/.rultor.yml +4 -22
- data/Gemfile +13 -27
- data/Gemfile.lock +54 -37
- data/LICENSES/MIT.txt +21 -0
- data/README.md +9 -9
- data/REUSE.toml +36 -0
- data/Rakefile +8 -35
- data/iri.gemspec +3 -22
- data/lib/iri.rb +246 -99
- data/test/test__helper.rb +25 -20
- data/test/test_iri.rb +94 -30
- metadata +7 -6
data/lib/iri.rb
CHANGED
@@ -1,76 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# (
|
4
|
-
#
|
5
|
-
# Copyright (c) 2019-2025 Yegor Bugayenko
|
6
|
-
#
|
7
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
-
# of this software and associated documentation files (the 'Software'), to deal
|
9
|
-
# in the Software without restriction, including without limitation the rights
|
10
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
-
# copies of the Software, and to permit persons to whom the Software is
|
12
|
-
# furnished to do so, subject to the following conditions:
|
13
|
-
#
|
14
|
-
# The above copyright notice and this permission notice shall be included in all
|
15
|
-
# copies or substantial portions of the Software.
|
16
|
-
#
|
17
|
-
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
-
# SOFTWARE.
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
24
5
|
|
25
6
|
require 'uri'
|
26
7
|
require 'cgi'
|
27
8
|
|
28
|
-
#
|
9
|
+
# Iri is a simple, immutable URI builder with a fluent interface.
|
29
10
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
# .del(:q) // remove this query parameter
|
34
|
-
# .del('limit') // remove this one too
|
35
|
-
# .over(q: 'books about tennis', limit: 10) // replace these params
|
36
|
-
# .scheme('https')
|
37
|
-
# .host('localhost')
|
38
|
-
# .port('443')
|
39
|
-
# .to_s
|
11
|
+
# The Iri class provides methods to manipulate different parts of a URI,
|
12
|
+
# including the scheme, host, port, path, query parameters, and fragment.
|
13
|
+
# Each method returns a new Iri instance, maintaining immutability.
|
40
14
|
#
|
41
|
-
#
|
15
|
+
# @example Creating and manipulating a URI
|
16
|
+
# require 'iri'
|
17
|
+
# url = Iri.new('http://google.com/')
|
18
|
+
# .add(q: 'books about OOP', limit: 50)
|
19
|
+
# .del(:q) # remove this query parameter
|
20
|
+
# .del('limit') # remove this one too
|
21
|
+
# .over(q: 'books about tennis', limit: 10) # replace these params
|
22
|
+
# .scheme('https')
|
23
|
+
# .host('localhost')
|
24
|
+
# .port('443')
|
25
|
+
# .to_s
|
26
|
+
#
|
27
|
+
# @example Using the local option
|
28
|
+
# Iri.new('/path?foo=bar', local: true).to_s # => "/path?foo=bar"
|
29
|
+
#
|
30
|
+
# @example Using the safe mode
|
31
|
+
# Iri.new('invalid://uri', safe: true).to_s # => "/" (no exception thrown)
|
32
|
+
# Iri.new('invalid://uri', safe: false) # => raises Iri::InvalidURI
|
33
|
+
#
|
34
|
+
# For more information read the
|
42
35
|
# {README}[https://github.com/yegor256/iri/blob/master/README.md] file.
|
43
36
|
#
|
44
37
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
45
38
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
46
39
|
# License:: MIT
|
47
40
|
class Iri
|
48
|
-
#
|
41
|
+
# Exception raised when a URI is not valid and safe mode is disabled.
|
49
42
|
class InvalidURI < StandardError; end
|
50
43
|
|
51
|
-
#
|
44
|
+
# Exception raised when arguments to .add(), .over(), or .del() are not valid Hashes.
|
52
45
|
class InvalidArguments < StandardError; end
|
53
46
|
|
54
|
-
#
|
47
|
+
# Creates a new Iri object for URI manipulation.
|
55
48
|
#
|
56
|
-
# You can even ignore the argument, which will produce an empty URI.
|
49
|
+
# You can even ignore the argument, which will produce an empty URI ("/").
|
57
50
|
#
|
58
51
|
# By default, this class will never throw any exceptions, even if your URI
|
59
|
-
# is not valid. It will just assume that the URI is"/". However,
|
60
|
-
# you can turn this mode off
|
52
|
+
# is not valid. It will just assume that the URI is "/". However,
|
53
|
+
# you can turn this safe mode off by specifying safe as FALSE, which will
|
54
|
+
# cause InvalidURI to be raised if the URI is malformed.
|
55
|
+
#
|
56
|
+
# The local parameter can be used if you only want to work with the path,
|
57
|
+
# query, and fragment portions of a URI, without the scheme, host, and port.
|
61
58
|
#
|
62
|
-
# @param [String] uri URI
|
63
|
-
# @param [Boolean] local
|
64
|
-
# @param [Boolean] safe
|
59
|
+
# @param [String] uri URI string to parse
|
60
|
+
# @param [Boolean] local When true, ignores scheme, host and port parts
|
61
|
+
# @param [Boolean] safe When true, prevents InvalidURI exceptions
|
62
|
+
# @raise [InvalidURI] If the URI is malformed and safe is false
|
65
63
|
def initialize(uri = '', local: false, safe: true)
|
66
|
-
|
64
|
+
raise ArgumentError, "The uri can't be nil" if uri.nil?
|
65
|
+
@uri = uri.to_s
|
67
66
|
@local = local
|
68
67
|
@safe = safe
|
69
68
|
end
|
70
69
|
|
71
|
-
#
|
70
|
+
# Converts the Iri object to a string representation of the URI.
|
72
71
|
#
|
73
|
-
#
|
72
|
+
# When local mode is enabled, only the path, query, and fragment parts are included.
|
73
|
+
# Otherwise, the full URI including scheme, host, and port is returned.
|
74
|
+
#
|
75
|
+
# @return [String] String representation of the URI
|
74
76
|
def to_s
|
75
77
|
u = the_uri
|
76
78
|
if @local
|
@@ -84,44 +86,60 @@ class Iri
|
|
84
86
|
end
|
85
87
|
end
|
86
88
|
|
87
|
-
#
|
89
|
+
# Returns a string representation of the Iri object for inspection purposes.
|
90
|
+
#
|
91
|
+
# This method is used when the object is displayed in irb/console or with puts/p.
|
88
92
|
#
|
89
|
-
# @return [String]
|
93
|
+
# @return [String] String representation for inspection
|
90
94
|
def inspect
|
91
95
|
@uri.to_s.inspect
|
92
96
|
end
|
93
97
|
|
94
|
-
#
|
98
|
+
# Converts the Iri object to a Ruby standard library URI object.
|
95
99
|
#
|
96
|
-
# @return [
|
100
|
+
# @return [URI] A cloned URI object from the underlying URI
|
97
101
|
def to_uri
|
98
102
|
the_uri.clone
|
99
103
|
end
|
100
104
|
|
101
|
-
#
|
102
|
-
# only the local address, for example, converting "https://google.com/foo"
|
103
|
-
# into "/foo".
|
105
|
+
# Creates a new Iri object with only the local parts of the URI.
|
104
106
|
#
|
105
|
-
#
|
107
|
+
# Removes the host, the port, and the scheme, returning only the local address.
|
108
|
+
# For example, converting "https://google.com/foo" into "/foo".
|
109
|
+
# The path, query string, and fragment are preserved.
|
110
|
+
#
|
111
|
+
# @return [Iri] A new Iri object with local:true and the same URI
|
112
|
+
# @see #initialize
|
106
113
|
def to_local
|
107
114
|
Iri.new(@uri, local: true, safe: @safe)
|
108
115
|
end
|
109
116
|
|
110
|
-
#
|
117
|
+
# Adds query parameters to the URI.
|
118
|
+
#
|
119
|
+
# This method appends query parameters to existing ones. If a parameter with the same
|
120
|
+
# name already exists, both values will be present in the resulting URI.
|
111
121
|
#
|
112
|
-
#
|
122
|
+
# @example Adding query parameters
|
123
|
+
# Iri.new('https://google.com').add(q: 'test', limit: 10)
|
124
|
+
# # => "https://google.com?q=test&limit=10"
|
113
125
|
#
|
114
|
-
#
|
126
|
+
# @example Adding parameters with the same name
|
127
|
+
# Iri.new('https://google.com?q=foo').add(q: 'bar')
|
128
|
+
# # => "https://google.com?q=foo&q=bar"
|
115
129
|
#
|
116
|
-
# You can
|
117
|
-
# URI, even if their names are the same. In order to make sure you have
|
118
|
-
# only one instance of a query argument, use +del+ first:
|
130
|
+
# You can ensure only one instance of a parameter by using +del+ first:
|
119
131
|
#
|
120
|
-
#
|
132
|
+
# @example Replacing a parameter by deleting it first
|
133
|
+
# Iri.new('https://google.com?q=foo').del(:q).add(q: 'test')
|
134
|
+
# # => "https://google.com?q=test"
|
121
135
|
#
|
122
|
-
# @param [Hash] hash Hash of names/values to
|
123
|
-
# @return [Iri] A new
|
136
|
+
# @param [Hash] hash Hash of parameter names/values to add to the query part
|
137
|
+
# @return [Iri] A new Iri instance
|
138
|
+
# @raise [InvalidArguments] If the argument is not a Hash
|
139
|
+
# @see #del
|
140
|
+
# @see #over
|
124
141
|
def add(hash)
|
142
|
+
raise ArgumentError, "The hash can't be nil" if hash.nil?
|
125
143
|
raise InvalidArguments unless hash.is_a?(Hash)
|
126
144
|
modify_query do |params|
|
127
145
|
hash.each do |k, v|
|
@@ -130,15 +148,24 @@ class Iri
|
|
130
148
|
end
|
131
149
|
end
|
132
150
|
end
|
151
|
+
alias with add
|
133
152
|
|
134
|
-
#
|
153
|
+
# Deletes query parameters from the URI.
|
135
154
|
#
|
136
|
-
#
|
155
|
+
# This method removes all instances of the specified parameters from the query string.
|
137
156
|
#
|
138
|
-
#
|
157
|
+
# @example Deleting a query parameter
|
158
|
+
# Iri.new('https://google.com?q=test&limit=10').del(:q)
|
159
|
+
# # => "https://google.com?limit=10"
|
139
160
|
#
|
140
|
-
# @
|
141
|
-
#
|
161
|
+
# @example Deleting multiple parameters
|
162
|
+
# Iri.new('https://google.com?q=test&limit=10&sort=asc').del(:q, :limit)
|
163
|
+
# # => "https://google.com?sort=asc"
|
164
|
+
#
|
165
|
+
# @param [Array<Symbol, String>] keys List of parameter names to delete
|
166
|
+
# @return [Iri] A new Iri instance
|
167
|
+
# @see #add
|
168
|
+
# @see #over
|
142
169
|
def del(*keys)
|
143
170
|
modify_query do |params|
|
144
171
|
keys.each do |k|
|
@@ -146,94 +173,178 @@ class Iri
|
|
146
173
|
end
|
147
174
|
end
|
148
175
|
end
|
176
|
+
alias without del
|
149
177
|
|
150
|
-
#
|
178
|
+
# Replaces query parameters in the URI.
|
179
|
+
#
|
180
|
+
# Unlike #add, this method replaces any existing parameters with the same name
|
181
|
+
# rather than adding additional instances. If a parameter doesn't exist,
|
182
|
+
# it will be added.
|
183
|
+
#
|
184
|
+
# @example Replacing a query parameter
|
185
|
+
# Iri.new('https://google.com?q=test').over(q: 'hey you!')
|
186
|
+
# # => "https://google.com?q=hey+you%21"
|
151
187
|
#
|
152
|
-
#
|
188
|
+
# @example Replacing multiple parameters
|
189
|
+
# Iri.new('https://google.com?q=test&limit=5').over(q: 'books', limit: 10)
|
190
|
+
# # => "https://google.com?q=books&limit=10"
|
153
191
|
#
|
154
|
-
# @param [Hash] hash Hash of names/values to
|
155
|
-
# @return [Iri] A new
|
192
|
+
# @param [Hash] hash Hash of parameter names/values to replace in the query part
|
193
|
+
# @return [Iri] A new Iri instance
|
194
|
+
# @raise [InvalidArguments] If the argument is not a Hash
|
195
|
+
# @see #add
|
196
|
+
# @see #del
|
156
197
|
def over(hash)
|
198
|
+
raise ArgumentError, "The hash can't be nil" if hash.nil?
|
157
199
|
raise InvalidArguments unless hash.is_a?(Hash)
|
158
200
|
modify_query do |params|
|
159
201
|
hash.each do |k, v|
|
160
|
-
params[k.to_s] = [] unless params[k]
|
202
|
+
params[k.to_s] = [] unless params[k.to_s]
|
161
203
|
params[k.to_s] = [v]
|
162
204
|
end
|
163
205
|
end
|
164
206
|
end
|
165
207
|
|
166
|
-
#
|
208
|
+
# Replaces the scheme part of the URI.
|
209
|
+
#
|
210
|
+
# @example Changing the scheme
|
211
|
+
# Iri.new('http://google.com').scheme('https')
|
212
|
+
# # => "https://google.com"
|
167
213
|
#
|
168
214
|
# @param [String] val New scheme to set, like "https" or "http"
|
169
|
-
# @return [Iri] A new
|
215
|
+
# @return [Iri] A new Iri instance
|
216
|
+
# @see #host
|
217
|
+
# @see #port
|
170
218
|
def scheme(val)
|
219
|
+
raise ArgumentError, "The scheme can't be nil" if val.nil?
|
220
|
+
raise ArgumentError, 'The scheme must be a String' unless val.is_a?(String)
|
221
|
+
raise ArgumentError, "The scheme can't be empty" if val.empty?
|
171
222
|
modify do |c|
|
172
223
|
c.scheme = val
|
173
224
|
end
|
174
225
|
end
|
175
226
|
|
176
|
-
#
|
227
|
+
# Replaces the host part of the URI.
|
177
228
|
#
|
178
|
-
# @
|
179
|
-
#
|
229
|
+
# @example Changing the host
|
230
|
+
# Iri.new('https://google.com').host('example.com')
|
231
|
+
# # => "https://example.com"
|
232
|
+
#
|
233
|
+
# @param [String] val New host to set, like "example.com" or "192.168.0.1"
|
234
|
+
# @return [Iri] A new Iri instance
|
235
|
+
# @see #scheme
|
236
|
+
# @see #port
|
180
237
|
def host(val)
|
238
|
+
raise ArgumentError, "The host can't be nil" if val.nil?
|
239
|
+
raise ArgumentError, 'The host must be a String' unless val.is_a?(String)
|
240
|
+
raise ArgumentError, "The host can't be empty" if val.empty?
|
181
241
|
modify do |c|
|
182
242
|
c.host = val
|
183
243
|
end
|
184
244
|
end
|
185
245
|
|
186
|
-
#
|
246
|
+
# Replaces the port part of the URI.
|
247
|
+
#
|
248
|
+
# @example Changing the port
|
249
|
+
# Iri.new('https://example.com').port('8443')
|
250
|
+
# # => "https://example.com:8443"
|
187
251
|
#
|
188
252
|
# @param [String] val New TCP port to set, like "8080" or "443"
|
189
|
-
# @return [Iri] A new
|
253
|
+
# @return [Iri] A new Iri instance
|
254
|
+
# @see #scheme
|
255
|
+
# @see #host
|
190
256
|
def port(val)
|
257
|
+
raise ArgumentError, "The port can't be nil" if val.nil?
|
258
|
+
raise ArgumentError, 'The port must be an Integer' unless val.is_a?(Integer)
|
259
|
+
raise ArgumentError, "The port can'be negative" if val.negative?
|
260
|
+
raise ArgumentError, "The port can'be zero" if val.zero?
|
261
|
+
raise ArgumentError, "The port can'be larger than 65536" if val > 65_536
|
191
262
|
modify do |c|
|
192
263
|
c.port = val
|
193
264
|
end
|
194
265
|
end
|
195
266
|
|
196
|
-
#
|
267
|
+
# Replaces the path part of the URI.
|
268
|
+
#
|
269
|
+
# @example Changing the path
|
270
|
+
# Iri.new('https://example.com/foo').path('/bar/baz')
|
271
|
+
# # => "https://example.com/bar/baz"
|
197
272
|
#
|
198
273
|
# @param [String] val New path to set, like "/foo/bar"
|
199
|
-
# @return [Iri] A new
|
274
|
+
# @return [Iri] A new Iri instance
|
275
|
+
# @see #query
|
276
|
+
# @see #fragment
|
200
277
|
def path(val)
|
278
|
+
raise ArgumentError, "The path can't be nil" if val.nil?
|
279
|
+
raise ArgumentError, 'The path must be a String' unless val.is_a?(String)
|
280
|
+
raise ArgumentError, "The path can't be empty" if val.empty?
|
201
281
|
modify do |c|
|
202
282
|
c.path = val
|
203
283
|
end
|
204
284
|
end
|
205
285
|
|
206
|
-
#
|
286
|
+
# Replaces the fragment part of the URI (the part after #).
|
287
|
+
#
|
288
|
+
# @example Setting a fragment
|
289
|
+
# Iri.new('https://example.com/page').fragment('section2')
|
290
|
+
# # => "https://example.com/page#section2"
|
207
291
|
#
|
208
|
-
# @param [String] val New fragment to set, like "
|
209
|
-
# @return [Iri] A new
|
292
|
+
# @param [String] val New fragment to set, like "section2"
|
293
|
+
# @return [Iri] A new Iri instance
|
294
|
+
# @see #path
|
295
|
+
# @see #query
|
210
296
|
def fragment(val)
|
297
|
+
raise ArgumentError, "The fragment can't be nil" if val.nil?
|
298
|
+
val = val.to_s
|
211
299
|
modify do |c|
|
212
300
|
c.fragment = val.to_s
|
213
301
|
end
|
214
302
|
end
|
215
303
|
|
216
|
-
#
|
304
|
+
# Replaces the entire query part of the URI.
|
305
|
+
#
|
306
|
+
# Use this method to completely replace the query string. For modifying
|
307
|
+
# individual parameters, see #add, #del, and #over.
|
308
|
+
#
|
309
|
+
# @example Setting a query string
|
310
|
+
# Iri.new('https://example.com/search').query('q=ruby&limit=10')
|
311
|
+
# # => "https://example.com/search?q=ruby&limit=10"
|
217
312
|
#
|
218
|
-
# @param [String] val New query to set, like "a=1&b=2"
|
219
|
-
# @return [Iri] A new
|
313
|
+
# @param [String] val New query string to set, like "a=1&b=2"
|
314
|
+
# @return [Iri] A new Iri instance
|
315
|
+
# @see #add
|
316
|
+
# @see #del
|
317
|
+
# @see #over
|
220
318
|
def query(val)
|
319
|
+
raise ArgumentError, "The query can't be nil" if val.nil?
|
320
|
+
raise ArgumentError, 'The query must be a String' unless val.is_a?(String)
|
221
321
|
modify do |c|
|
222
322
|
c.query = val
|
223
323
|
end
|
224
324
|
end
|
225
325
|
|
226
|
-
#
|
326
|
+
# Removes the entire path, query, and fragment parts and sets a new path.
|
227
327
|
#
|
228
|
-
#
|
328
|
+
# This method is useful for "cutting off" everything after the host:port
|
329
|
+
# and setting a new path, effectively removing query string and fragment.
|
229
330
|
#
|
230
|
-
#
|
331
|
+
# @example Cutting off path/query/fragment and setting a new path
|
332
|
+
# Iri.new('https://google.com/a/b?q=test').cut('/hello')
|
333
|
+
# # => "https://google.com/hello"
|
231
334
|
#
|
232
|
-
#
|
335
|
+
# @example Resetting to root path
|
336
|
+
# Iri.new('https://google.com/a/b?q=test#section2').cut()
|
337
|
+
# # => "https://google.com/"
|
233
338
|
#
|
234
|
-
# @param [String] path New path to set,
|
235
|
-
# @return [Iri] A new
|
339
|
+
# @param [String] path New path to set, defaults to "/"
|
340
|
+
# @return [Iri] A new Iri instance
|
341
|
+
# @see #path
|
342
|
+
# @see #query
|
343
|
+
# @see #fragment
|
236
344
|
def cut(path = '/')
|
345
|
+
raise ArgumentError, "The path can't be nil" if path.nil?
|
346
|
+
raise ArgumentError, 'The path must be a String' unless path.is_a?(String)
|
347
|
+
raise ArgumentError, "The path can't be empty" if path.empty?
|
237
348
|
modify do |c|
|
238
349
|
c.query = nil
|
239
350
|
c.path = path
|
@@ -241,17 +352,30 @@ class Iri
|
|
241
352
|
end
|
242
353
|
end
|
243
354
|
|
244
|
-
#
|
355
|
+
# Appends a new segment to the existing path.
|
245
356
|
#
|
246
|
-
#
|
357
|
+
# This method adds a new segment to the existing path, automatically handling
|
358
|
+
# the slash between segments and URL encoding the new segment.
|
247
359
|
#
|
248
|
-
#
|
360
|
+
# @example Appending a path segment
|
361
|
+
# Iri.new('https://example.com/a/b?q=test').append('hello')
|
362
|
+
# # => "https://example.com/a/b/hello?q=test"
|
249
363
|
#
|
250
|
-
#
|
364
|
+
# @example Appending to a path with a trailing slash
|
365
|
+
# Iri.new('https://example.com/a/').append('hello')
|
366
|
+
# # => "https://example.com/a/hello?q=test"
|
251
367
|
#
|
252
|
-
# @
|
253
|
-
#
|
368
|
+
# @example Appending a segment that needs URL encoding
|
369
|
+
# Iri.new('https://example.com/docs').append('section 1')
|
370
|
+
# # => "https://example.com/docs/section%201"
|
371
|
+
#
|
372
|
+
# @param [String, #to_s] part New segment to add to the existing path
|
373
|
+
# @return [Iri] A new Iri instance
|
374
|
+
# @see #path
|
254
375
|
def append(part)
|
376
|
+
raise ArgumentError, "The part can't be nil" if part.nil?
|
377
|
+
raise ArgumentError, 'The part must be a String' unless part.is_a?(String)
|
378
|
+
raise ArgumentError, "The part can't be empty" if part.empty?
|
255
379
|
modify do |c|
|
256
380
|
tail = (c.path.end_with?('/') ? '' : '/') + CGI.escape(part.to_s)
|
257
381
|
c.path = c.path + tail
|
@@ -260,6 +384,14 @@ class Iri
|
|
260
384
|
|
261
385
|
private
|
262
386
|
|
387
|
+
# Parses the URI string into a URI object.
|
388
|
+
#
|
389
|
+
# This method handles the safe mode by catching and handling invalid URI errors.
|
390
|
+
# When safe mode is enabled (default), invalid URIs will return the root path URI "/"
|
391
|
+
# instead of raising an exception.
|
392
|
+
#
|
393
|
+
# @return [URI] The parsed URI object
|
394
|
+
# @raise [InvalidURI] If the URI is invalid and safe mode is disabled
|
263
395
|
def the_uri
|
264
396
|
@the_uri ||= URI(@uri)
|
265
397
|
rescue URI::InvalidURIError => e
|
@@ -267,12 +399,27 @@ class Iri
|
|
267
399
|
@the_uri = URI('/')
|
268
400
|
end
|
269
401
|
|
402
|
+
# Creates a new Iri object after modifying the underlying URI.
|
403
|
+
#
|
404
|
+
# This helper method clones the current URI, yields it to a block for modification,
|
405
|
+
# and then creates a new Iri object with the modified URI, preserving the local and safe flags.
|
406
|
+
#
|
407
|
+
# @yield [URI] The cloned URI object for modification
|
408
|
+
# @return [Iri] A new Iri instance with the modified URI
|
270
409
|
def modify
|
271
410
|
c = the_uri.clone
|
272
411
|
yield c
|
273
412
|
Iri.new(c, local: @local, safe: @safe)
|
274
413
|
end
|
275
414
|
|
415
|
+
# Creates a new Iri object after modifying the query parameters.
|
416
|
+
#
|
417
|
+
# This helper method parses the current query string into a hash of parameter names
|
418
|
+
# to arrays of values, yields this hash for modification, and then encodes it back
|
419
|
+
# into a query string. It uses the modify method to create a new Iri object.
|
420
|
+
#
|
421
|
+
# @yield [Hash] The parsed query parameters for modification
|
422
|
+
# @return [Iri] A new Iri instance with the modified query string
|
276
423
|
def modify_query
|
277
424
|
modify do |c|
|
278
425
|
params = CGI.parse(the_uri.query || '').map do |p, a|
|
data/test/test__helper.rb
CHANGED
@@ -1,26 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
-
#
|
5
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
-
# in the Software without restriction, including without limitation the rights
|
8
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
# copies of the Software, and to permit persons to whom the Software is
|
10
|
-
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be included in all
|
13
|
-
# copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
# SOFTWARE.
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
22
5
|
|
23
6
|
$stdout.sync = true
|
24
7
|
|
25
8
|
require 'simplecov'
|
26
|
-
|
9
|
+
require 'simplecov-cobertura'
|
10
|
+
unless SimpleCov.running
|
11
|
+
SimpleCov.command_name('test')
|
12
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
13
|
+
[
|
14
|
+
SimpleCov::Formatter::HTMLFormatter,
|
15
|
+
SimpleCov::Formatter::CoberturaFormatter
|
16
|
+
]
|
17
|
+
)
|
18
|
+
SimpleCov.minimum_coverage 100
|
19
|
+
SimpleCov.minimum_coverage_by_file 100
|
20
|
+
SimpleCov.start do
|
21
|
+
add_filter 'test/'
|
22
|
+
add_filter 'vendor/'
|
23
|
+
add_filter 'target/'
|
24
|
+
track_files 'lib/**/*.rb'
|
25
|
+
track_files '*.rb'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'minitest/autorun'
|
30
|
+
require 'minitest/reporters'
|
31
|
+
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|