protocol-http 0.54.0 → 0.56.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.
@@ -1,253 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
5
-
6
- require_relative "url"
7
-
8
- module Protocol
9
- module HTTP
10
- # A relative reference, excluding any authority. The path part of an HTTP request.
11
- class Reference
12
- include Comparable
13
-
14
- # Generate a reference from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
15
- def self.parse(path = "/", parameters = nil)
16
- base, fragment = path.split("#", 2)
17
- path, query = base.split("?", 2)
18
-
19
- self.new(path, query, fragment, parameters)
20
- end
21
-
22
- # Initialize the reference.
23
- #
24
- # @parameter path [String] The path component, e.g. `/foo/bar/index.html`.
25
- # @parameter query [String | Nil] The un-parsed query string, e.g. 'x=10&y=20'.
26
- # @parameter fragment [String | Nil] The fragment, the part after the '#'.
27
- # @parameter parameters [Hash | Nil] User supplied parameters that will be appended to the query part.
28
- def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
29
- @path = path
30
- @query = query
31
- @fragment = fragment
32
- @parameters = parameters
33
- end
34
-
35
- # @attribute [String] The path component, e.g. `/foo/bar/index.html`.
36
- attr_accessor :path
37
-
38
- # @attribute [String] The un-parsed query string, e.g. 'x=10&y=20'.
39
- attr_accessor :query
40
-
41
- # @attribute [String] The fragment, the part after the '#'.
42
- attr_accessor :fragment
43
-
44
- # @attribute [Hash] User supplied parameters that will be appended to the query part.
45
- attr_accessor :parameters
46
-
47
- # Freeze the reference.
48
- #
49
- # @returns [Reference] The frozen reference.
50
- def freeze
51
- return self if frozen?
52
-
53
- @path.freeze
54
- @query.freeze
55
- @fragment.freeze
56
- @parameters.freeze
57
-
58
- super
59
- end
60
-
61
- # Implicit conversion to an array.
62
- #
63
- # @returns [Array] The reference as an array, `[path, query, fragment, parameters]`.
64
- def to_ary
65
- [@path, @query, @fragment, @parameters]
66
- end
67
-
68
- # Compare two references.
69
- #
70
- # @parameter other [Reference] The other reference to compare.
71
- # @returns [Integer] -1, 0, 1 if the reference is less than, equal to, or greater than the other reference.
72
- def <=> other
73
- to_ary <=> other.to_ary
74
- end
75
-
76
- # Type-cast a reference.
77
- #
78
- # @parameter reference [Reference | String] The reference to type-cast.
79
- # @returns [Reference] The type-casted reference.
80
- def self.[] reference
81
- if reference.is_a? self
82
- return reference
83
- else
84
- return self.parse(reference)
85
- end
86
- end
87
-
88
- # @returns [Boolean] Whether the reference has parameters.
89
- def parameters?
90
- @parameters and !@parameters.empty?
91
- end
92
-
93
- # @returns [Boolean] Whether the reference has a query string.
94
- def query?
95
- @query and !@query.empty?
96
- end
97
-
98
- # @returns [Boolean] Whether the reference has a fragment.
99
- def fragment?
100
- @fragment and !@fragment.empty?
101
- end
102
-
103
- # Append the reference to the given buffer.
104
- def append(buffer = String.new)
105
- if query?
106
- buffer << URL.escape_path(@path) << "?" << @query
107
- buffer << "&" << URL.encode(@parameters) if parameters?
108
- else
109
- buffer << URL.escape_path(@path)
110
- buffer << "?" << URL.encode(@parameters) if parameters?
111
- end
112
-
113
- if fragment?
114
- buffer << "#" << URL.escape(@fragment)
115
- end
116
-
117
- return buffer
118
- end
119
-
120
- # Convert the reference to a string, e.g. `/foo/bar/index.html?x=10&y=20#section`
121
- #
122
- # @returns [String] The reference as a string.
123
- def to_s
124
- append
125
- end
126
-
127
- # Merges two references as specified by RFC2396, similar to `URI.join`.
128
- def + other
129
- other = self.class[other]
130
-
131
- self.class.new(
132
- expand_path(self.path, other.path, true),
133
- other.query,
134
- other.fragment,
135
- other.parameters,
136
- )
137
- end
138
-
139
- # Just the base path, without any query string, parameters or fragment.
140
- def base
141
- self.class.new(@path, nil, nil, nil)
142
- end
143
-
144
- # Update the reference with the given path, parameters and fragment.
145
- #
146
- # @parameter path [String] Append the string to this reference similar to `File.join`.
147
- # @parameter parameters [Hash] Append the parameters to this reference.
148
- # @parameter fragment [String] Set the fragment to this value.
149
- # @parameter pop [Boolean] If the path contains a trailing filename, pop the last component of the path before appending the new path.
150
- # @parameter merge [Boolean] If the parameters are specified, merge them with the existing parameters, otherwise replace them (including query string).
151
- def with(path: nil, parameters: false, fragment: @fragment, pop: false, merge: true)
152
- if merge
153
- # Merge mode: combine new parameters with existing, keep query:
154
- # parameters = (@parameters || {}).merge(parameters || {})
155
- if @parameters
156
- if parameters
157
- parameters = @parameters.merge(parameters)
158
- else
159
- parameters = @parameters
160
- end
161
- elsif !parameters
162
- parameters = @parameters
163
- end
164
-
165
- query = @query
166
- else
167
- # Replace mode: use new parameters if provided, clear query when replacing:
168
- if parameters == false
169
- # No new parameters provided, keep existing:
170
- parameters = @parameters
171
- query = @query
172
- else
173
- # New parameters provided, replace and clear query:
174
- # parameters = parameters
175
- query = nil
176
- end
177
- end
178
-
179
- if path
180
- path = expand_path(@path, path, pop)
181
- else
182
- path = @path
183
- end
184
-
185
- self.class.new(path, query, fragment, parameters)
186
- end
187
-
188
- private
189
-
190
- def split(path)
191
- if path.empty?
192
- [path]
193
- else
194
- path.split("/", -1)
195
- end
196
- end
197
-
198
- def expand_absolute_path(path, parts)
199
- parts.each do |part|
200
- if part == ".."
201
- path.pop
202
- elsif part == "."
203
- # Do nothing.
204
- else
205
- path << part
206
- end
207
- end
208
-
209
- if path.first != ""
210
- path.unshift("")
211
- end
212
- end
213
-
214
- def expand_relative_path(path, parts)
215
- parts.each do |part|
216
- if part == ".." and path.any?
217
- path.pop
218
- elsif part == "."
219
- # Do nothing.
220
- else
221
- path << part
222
- end
223
- end
224
- end
225
-
226
- # @param pop [Boolean] whether to remove the last path component of the base path, to conform to URI merging behaviour, as defined by RFC2396.
227
- def expand_path(base, relative, pop = true)
228
- if relative.start_with? "/"
229
- return relative
230
- end
231
-
232
- path = split(base)
233
-
234
- # RFC2396 Section 5.2:
235
- # 6) a) All but the last segment of the base URI's path component is
236
- # copied to the buffer. In other words, any characters after the
237
- # last (right-most) slash character, if any, are excluded.
238
- path.pop if pop or path.last == ""
239
-
240
- parts = split(relative)
241
-
242
- # Absolute path:
243
- if path.first == ""
244
- expand_absolute_path(path, parts)
245
- else
246
- expand_relative_path(path, parts)
247
- end
248
-
249
- return path.join("/")
250
- end
251
- end
252
- end
253
- end
@@ -1,149 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
5
- # Copyright, 2022, by Herrick Fang.
6
-
7
- module Protocol
8
- module HTTP
9
- # Helpers for working with URLs.
10
- module URL
11
- # Escapes a string using percent encoding, e.g. `a b` -> `a%20b`.
12
- #
13
- # @parameter string [String] The string to escape.
14
- # @returns [String] The escaped string.
15
- def self.escape(string, encoding = string.encoding)
16
- string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
17
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
18
- end.force_encoding(encoding)
19
- end
20
-
21
- # Unescapes a percent encoded string, e.g. `a%20b` -> `a b`.
22
- #
23
- # @parameter string [String] The string to unescape.
24
- # @returns [String] The unescaped string.
25
- def self.unescape(string, encoding = string.encoding)
26
- string.b.gsub(/%(\h\h)/) do |hex|
27
- Integer($1, 16).chr
28
- end.force_encoding(encoding)
29
- end
30
-
31
- # 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.
32
- NON_PATH_CHARACTER_PATTERN = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze
33
-
34
- # 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.
35
- #
36
- # @parameter path [String] The path to escape.
37
- # @returns [String] The escaped path.
38
- def self.escape_path(path)
39
- encoding = path.encoding
40
- path.b.gsub(NON_PATH_CHARACTER_PATTERN) do |m|
41
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
42
- end.force_encoding(encoding)
43
- end
44
-
45
- # Encodes a hash or array into a query string. This method is used to encode query parameters in a URL. For example, `{"a" => 1, "b" => 2}` is encoded as `a=1&b=2`.
46
- #
47
- # @parameter value [Hash | Array | Nil] The value to encode.
48
- # @parameter prefix [String] The prefix to use for keys.
49
- def self.encode(value, prefix = nil)
50
- case value
51
- when Array
52
- return value.map {|v|
53
- self.encode(v, "#{prefix}[]")
54
- }.join("&")
55
- when Hash
56
- return value.map {|k, v|
57
- self.encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
58
- }.reject(&:empty?).join("&")
59
- when nil
60
- return prefix
61
- else
62
- raise ArgumentError, "value must be a Hash" if prefix.nil?
63
-
64
- return "#{prefix}=#{escape(value.to_s)}"
65
- end
66
- end
67
-
68
- # Scan a string for URL-encoded key/value pairs.
69
- # @yields {|key, value| ...}
70
- # @parameter key [String] The unescaped key.
71
- # @parameter value [String] The unescaped key.
72
- def self.scan(string)
73
- string.split("&") do |assignment|
74
- next if assignment.empty?
75
-
76
- key, value = assignment.split("=", 2)
77
-
78
- yield unescape(key), value.nil? ? value : unescape(value)
79
- end
80
- end
81
-
82
- # Split a key into parts, e.g. `a[b][c]` -> `["a", "b", "c"]`.
83
- #
84
- # @parameter name [String] The key to split.
85
- # @returns [Array(String)] The parts of the key.
86
- def self.split(name)
87
- name.scan(/([^\[]+)|(?:\[(.*?)\])/)&.tap do |parts|
88
- parts.flatten!
89
- parts.compact!
90
- end
91
- end
92
-
93
- # Assign a value to a nested hash.
94
- #
95
- # @parameter keys [Array(String)] The parts of the key.
96
- # @parameter value [Object] The value to assign.
97
- # @parameter parent [Hash] The parent hash.
98
- def self.assign(keys, value, parent)
99
- top, *middle = keys
100
-
101
- middle.each_with_index do |key, index|
102
- if key.nil? or key.empty?
103
- parent = (parent[top] ||= Array.new)
104
- top = parent.size
105
-
106
- if nested = middle[index+1] and last = parent.last
107
- top -= 1 unless last.include?(nested)
108
- end
109
- else
110
- parent = (parent[top] ||= Hash.new)
111
- top = key
112
- end
113
- end
114
-
115
- parent[top] = value
116
- end
117
-
118
- # Decode a URL-encoded query string into a hash.
119
- #
120
- # @parameter string [String] The query string to decode.
121
- # @parameter maximum [Integer] The maximum number of keys in a path.
122
- # @parameter symbolize_keys [Boolean] Whether to symbolize keys.
123
- # @returns [Hash] The decoded query string.
124
- def self.decode(string, maximum = 8, symbolize_keys: false)
125
- parameters = {}
126
-
127
- self.scan(string) do |name, value|
128
- keys = self.split(name)
129
-
130
- if keys.empty?
131
- raise ArgumentError, "Invalid key path: #{name.inspect}!"
132
- end
133
-
134
- if keys.size > maximum
135
- raise ArgumentError, "Key length exceeded limit!"
136
- end
137
-
138
- if symbolize_keys
139
- keys.collect!{|key| key.empty? ? nil : key.to_sym}
140
- end
141
-
142
- self.assign(keys, value, parameters)
143
- end
144
-
145
- return parameters
146
- end
147
- end
148
- end
149
- end