protocol-url 0.10.0 → 0.12.0
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
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/url/absolute.rb +28 -15
- data/lib/protocol/url/encoding.rb +56 -49
- data/lib/protocol/url/path.rb +389 -139
- data/lib/protocol/url/reference.rb +25 -22
- data/lib/protocol/url/relative.rb +50 -26
- data/lib/protocol/url/version.rb +2 -2
- data/lib/protocol/url.rb +1 -1
- data/license.md +1 -1
- data/notes.md +17 -0
- data/readme.md +113 -3
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +6 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67f58bb48277d53b3817bb38b2b7c4546a8a8e1f3df62cea531c577705c4c833
|
|
4
|
+
data.tar.gz: 55bcfd924be9ae1eada0b2e5f8ee93fcd4a1a24c0693986ed3f8818b26072fd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7df97ce90f4ce934023217fbedf9cbf864febeee311b9b25e3ee952e647d75d8cc041f16665b84884fec1e1d2dad859f9f3d21a0bfdccf40f832938158a21924
|
|
7
|
+
data.tar.gz: bb2c1f9fece1eda9cb5ee8b96d216b28e0e15c6fe9605c6ec0d43a615b5b9a247f9bc299553e1d46778d9e6c5971e34d56bbd77f172202002ad63d7be89b53c3
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "relative"
|
|
7
7
|
|
|
@@ -14,9 +14,9 @@ module Protocol
|
|
|
14
14
|
#
|
|
15
15
|
# @parameter scheme [String] The URL scheme (e.g., "https", "http").
|
|
16
16
|
# @parameter authority [String] The authority component (e.g., "example.com", "user@host:port").
|
|
17
|
-
# @parameter path [String] The path component (defaults to "/").
|
|
18
|
-
# @parameter query [String
|
|
19
|
-
# @parameter fragment [String
|
|
17
|
+
# @parameter path [String | Path] The encoded path component (defaults to "/").
|
|
18
|
+
# @parameter query [String | Nil] The query string.
|
|
19
|
+
# @parameter fragment [String | Nil] The fragment identifier.
|
|
20
20
|
def initialize(scheme, authority, path = "/", query = nil, fragment = nil)
|
|
21
21
|
@scheme = scheme
|
|
22
22
|
@authority = authority
|
|
@@ -25,11 +25,22 @@ module Protocol
|
|
|
25
25
|
super(path, query, fragment)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
28
|
+
# Freeze the URL and its direct components.
|
|
29
|
+
# @returns [Absolute] The frozen URL.
|
|
30
|
+
def freeze
|
|
31
|
+
return self if frozen?
|
|
32
|
+
|
|
33
|
+
@scheme.freeze
|
|
34
|
+
@authority.freeze
|
|
35
|
+
|
|
36
|
+
return super
|
|
37
|
+
end
|
|
30
38
|
|
|
31
|
-
# @attribute [String] The
|
|
32
|
-
|
|
39
|
+
# @attribute [String | Nil] The URL scheme.
|
|
40
|
+
attr_accessor :scheme
|
|
41
|
+
|
|
42
|
+
# @attribute [String | Nil] The authority component.
|
|
43
|
+
attr_accessor :authority
|
|
33
44
|
|
|
34
45
|
# Check if the URL has a non-empty scheme.
|
|
35
46
|
#
|
|
@@ -97,7 +108,7 @@ module Protocol
|
|
|
97
108
|
end
|
|
98
109
|
else
|
|
99
110
|
# Relative path: merge with base path:
|
|
100
|
-
path =
|
|
111
|
+
path = @path.join(other.path)
|
|
101
112
|
Absolute.new(@scheme, @authority, path, other.query, other.fragment)
|
|
102
113
|
end
|
|
103
114
|
end
|
|
@@ -111,11 +122,11 @@ module Protocol
|
|
|
111
122
|
|
|
112
123
|
# Create a new Absolute URL with modified components.
|
|
113
124
|
#
|
|
114
|
-
# @parameter scheme [String
|
|
115
|
-
# @parameter authority [String
|
|
116
|
-
# @parameter path [String
|
|
117
|
-
# @parameter query [String
|
|
118
|
-
# @parameter fragment [String
|
|
125
|
+
# @parameter scheme [String | Nil] The scheme to use (nil to remove scheme).
|
|
126
|
+
# @parameter authority [String | Nil] The authority to use (nil to remove authority).
|
|
127
|
+
# @parameter path [String | Nil] The path to merge with the current path.
|
|
128
|
+
# @parameter query [String | Nil] The query string to use.
|
|
129
|
+
# @parameter fragment [String | Nil] The fragment to use.
|
|
119
130
|
# @parameter pop [Boolean] Whether to pop the last path component before merging.
|
|
120
131
|
# @returns [Absolute] A new Absolute URL with the modified components.
|
|
121
132
|
#
|
|
@@ -129,7 +140,9 @@ module Protocol
|
|
|
129
140
|
# updated = url.with(query: "query=python")
|
|
130
141
|
# updated.to_s # => "https://example.com/search?query=python"
|
|
131
142
|
def with(scheme: @scheme, authority: @authority, path: nil, query: @query, fragment: @fragment, pop: true)
|
|
132
|
-
|
|
143
|
+
path = @path.join(path, pop: pop) unless path.nil?
|
|
144
|
+
|
|
145
|
+
self.class.new(scheme, authority, path || @path, query, fragment)
|
|
133
146
|
end
|
|
134
147
|
|
|
135
148
|
# Convert the URL to an array representation.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Protocol
|
|
7
7
|
module URL
|
|
@@ -29,6 +29,7 @@ module Protocol
|
|
|
29
29
|
#
|
|
30
30
|
# @parameter string [String] The string to unescape.
|
|
31
31
|
# @returns [String] The unescaped string.
|
|
32
|
+
# @raises [ArgumentError] If the string contains malformed percent encoding.
|
|
32
33
|
#
|
|
33
34
|
# @example Unescape spaces and special characters.
|
|
34
35
|
# Encoding.unescape("hello%20world%21")
|
|
@@ -38,60 +39,66 @@ module Protocol
|
|
|
38
39
|
# Encoding.unescape("caf%C3%A9")
|
|
39
40
|
# # => "café"
|
|
40
41
|
def self.unescape(string, encoding = string.encoding)
|
|
41
|
-
string.b.gsub(/%(
|
|
42
|
-
|
|
42
|
+
string.b.gsub(/%([0-9A-Fa-f]{2})?/) do
|
|
43
|
+
unless hexadecimal = $1
|
|
44
|
+
raise ArgumentError, "String contains malformed percent encoding!"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Integer(hexadecimal, 16).chr
|
|
43
48
|
end.force_encoding(encoding)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
# Maps individual URL path segments to and from local filesystem components.
|
|
52
|
+
#
|
|
53
|
+
# Unlike generic URL decoding, this encoding rejects values which would turn one
|
|
54
|
+
# URL segment into multiple local path components.
|
|
55
|
+
module System
|
|
56
|
+
ENCODING = ::Encoding.find("filesystem")
|
|
57
|
+
INVALID_CHARACTER_PATTERN = Regexp.union(["\0", File::SEPARATOR, File::ALT_SEPARATOR].compact)
|
|
58
|
+
|
|
59
|
+
# Encode one local filesystem component as one URL path segment.
|
|
60
|
+
# @parameter component [String] The local filesystem component.
|
|
61
|
+
# @returns [String] The encoded URL segment.
|
|
62
|
+
# @raises [ArgumentError] If the component cannot be converted or contains a system path separator.
|
|
63
|
+
def self.escape(component)
|
|
64
|
+
validate(component)
|
|
65
|
+
Encoding.escape(transcode(component, ::Encoding::UTF_8))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Decode one URL path segment as one local filesystem component.
|
|
69
|
+
# @parameter segment [String] The encoded URL segment.
|
|
70
|
+
# @returns [String] The local filesystem component.
|
|
71
|
+
# @raises [ArgumentError] If the segment cannot map to one local filesystem component.
|
|
72
|
+
def self.unescape(segment)
|
|
73
|
+
component = Encoding.unescape(segment, ::Encoding::UTF_8)
|
|
74
|
+
validate(component)
|
|
75
|
+
transcode(component, ENCODING)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Transcode a path component to the requested character encoding.
|
|
79
|
+
def self.transcode(component, encoding)
|
|
80
|
+
component.encode(encoding)
|
|
81
|
+
rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
|
|
82
|
+
raise ArgumentError, "Path component could not be transcoded!"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validate that a string can represent exactly one local filesystem component.
|
|
86
|
+
def self.validate(component)
|
|
87
|
+
unless component.valid_encoding?
|
|
88
|
+
raise ArgumentError, "Path component has invalid encoding!"
|
|
89
|
+
end
|
|
64
90
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
hex # Keep as %2F or %5C
|
|
68
|
-
else
|
|
69
|
-
char
|
|
91
|
+
if INVALID_CHARACTER_PATTERN.match?(component)
|
|
92
|
+
raise ArgumentError, "Path component contains invalid characters!"
|
|
70
93
|
end
|
|
71
|
-
end
|
|
94
|
+
end
|
|
95
|
+
private_class_method :transcode, :validate
|
|
96
|
+
private_constant :ENCODING, :INVALID_CHARACTER_PATTERN
|
|
72
97
|
end
|
|
73
98
|
|
|
74
|
-
# Matches characters that are not allowed in a URI path segment. According to RFC 3986 Section 3.3 (https://tools.ietf.org/html/rfc3986#section-3.3), a valid path segment consists of "pchar" characters. This pattern identifies characters that must be percent-encoded when included in a URI path segment.
|
|
75
|
-
NON_PATH_CHARACTER_PATTERN = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze
|
|
76
|
-
|
|
77
99
|
# Matches characters that are not allowed in a URI fragment. According to RFC 3986 Section 3.5, a valid fragment consists of pchar / "/" / "?" characters.
|
|
78
100
|
NON_FRAGMENT_CHARACTER_PATTERN = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/\?]+)/.freeze
|
|
79
101
|
|
|
80
|
-
# Escapes non-path characters using percent encoding. In other words, this method escapes characters that are not allowed in a URI path segment. According to RFC 3986 Section 3.3 (https://tools.ietf.org/html/rfc3986#section-3.3), a valid path segment consists of "pchar" characters. This method percent-encodes characters that are not "pchar" characters.
|
|
81
|
-
#
|
|
82
|
-
# @parameter path [String] The path to escape.
|
|
83
|
-
# @returns [String] The escaped path.
|
|
84
|
-
#
|
|
85
|
-
# @example Escape spaces while preserving path separators.
|
|
86
|
-
# Encoding.escape_path("/documents/my reports/summary.pdf")
|
|
87
|
-
# # => "/documents/my%20reports/summary.pdf"
|
|
88
|
-
def self.escape_path(path)
|
|
89
|
-
encoding = path.encoding
|
|
90
|
-
path.b.gsub(NON_PATH_CHARACTER_PATTERN) do |m|
|
|
91
|
-
"%" + m.unpack("H2" * m.bytesize).join("%").upcase
|
|
92
|
-
end.force_encoding(encoding)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
102
|
# Escapes non-fragment characters using percent encoding. According to RFC 3986 Section 3.5, fragments can contain pchar / "/" / "?" characters.
|
|
96
103
|
#
|
|
97
104
|
# @parameter fragment [String] The fragment to escape.
|
|
@@ -118,13 +125,13 @@ module Protocol
|
|
|
118
125
|
def self.encode(value, prefix = nil)
|
|
119
126
|
case value
|
|
120
127
|
when Array
|
|
121
|
-
return value.map
|
|
128
|
+
return value.map do |v|
|
|
122
129
|
self.encode(v, "#{prefix}[]")
|
|
123
|
-
|
|
130
|
+
end.join("&")
|
|
124
131
|
when Hash
|
|
125
|
-
return value.map
|
|
132
|
+
return value.map do |k, v|
|
|
126
133
|
self.encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
|
|
127
|
-
|
|
134
|
+
end.reject(&:empty?).join("&")
|
|
128
135
|
when nil
|
|
129
136
|
return prefix
|
|
130
137
|
else
|