protocol-http 0.51.1 → 0.52.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.
@@ -0,0 +1,130 @@
1
+ # URL Parsing
2
+
3
+ This guide explains how to use `Protocol::HTTP::URL` for parsing and manipulating URL components, particularly query strings and parameters.
4
+
5
+ ## Overview
6
+
7
+ {ruby Protocol::HTTP::URL} provides utilities for parsing and manipulating URL components, particularly query strings and parameters. It offers robust encoding/decoding capabilities for complex parameter structures.
8
+
9
+ While basic query parameter encoding follows the `application/x-www-form-urlencoded` standard, there is no universal standard for serializing complex nested structures (arrays, nested objects) in URLs. Different frameworks use varying conventions for these cases, and this implementation follows common patterns where possible.
10
+
11
+ ## Basic Query Parameter Parsing
12
+
13
+ ``` ruby
14
+ require 'protocol/http/url'
15
+
16
+ # Parse query parameters from a URL:
17
+ reference = Protocol::HTTP::Reference.parse("/search?q=ruby&category=programming&page=2")
18
+ parameters = Protocol::HTTP::URL.decode(reference.query)
19
+ # => {"q" => "ruby", "category" => "programming", "page" => "2"}
20
+
21
+ # Symbolize keys for easier access:
22
+ parameters = Protocol::HTTP::URL.decode(reference.query, symbolize_keys: true)
23
+ # => {:q => "ruby", :category => "programming", :page => "2"}
24
+ ```
25
+
26
+ ## Complex Parameter Structures
27
+
28
+ The URL module handles nested parameters, arrays, and complex data structures:
29
+
30
+ ``` ruby
31
+ # Array parameters:
32
+ query = "tags[]=ruby&tags[]=programming&tags[]=web"
33
+ parameters = Protocol::HTTP::URL.decode(query)
34
+ # => {"tags" => ["ruby", "programming", "web"]}
35
+
36
+ # Nested hash parameters:
37
+ query = "user[name]=John&user[email]=john@example.com&user[preferences][theme]=dark"
38
+ parameters = Protocol::HTTP::URL.decode(query)
39
+ # => {"user" => {"name" => "John", "email" => "john@example.com", "preferences" => {"theme" => "dark"}}}
40
+
41
+ # Mixed structures:
42
+ query = "filters[categories][]=books&filters[categories][]=movies&filters[price][min]=10&filters[price][max]=100"
43
+ parameters = Protocol::HTTP::URL.decode(query)
44
+ # => {"filters" => {"categories" => ["books", "movies"], "price" => {"min" => "10", "max" => "100"}}}
45
+ ```
46
+
47
+ ## Encoding Parameters to Query Strings
48
+
49
+ ``` ruby
50
+ # Simple parameters:
51
+ parameters = {"search" => "protocol-http", "limit" => "20"}
52
+ query = Protocol::HTTP::URL.encode(parameters)
53
+ # => "search=protocol-http&limit=20"
54
+
55
+ # Array parameters:
56
+ parameters = {"tags" => ["ruby", "http", "protocol"]}
57
+ query = Protocol::HTTP::URL.encode(parameters)
58
+ # => "tags[]=ruby&tags[]=http&tags[]=protocol"
59
+
60
+ # Nested parameters:
61
+ parameters = {
62
+ user: {
63
+ profile: {
64
+ name: "Alice",
65
+ settings: {
66
+ notifications: true,
67
+ theme: "light"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ query = Protocol::HTTP::URL.encode(parameters)
73
+ # => "user[profile][name]=Alice&user[profile][settings][notifications]=true&user[profile][settings][theme]=light"
74
+ ```
75
+
76
+ ## URL Escaping and Unescaping
77
+
78
+ ``` ruby
79
+ # Escape special characters:
80
+ Protocol::HTTP::URL.escape("hello world!")
81
+ # => "hello%20world%21"
82
+
83
+ # Escape path components (preserves path separators):
84
+ Protocol::HTTP::URL.escape_path("/path/with spaces/file.html")
85
+ # => "/path/with%20spaces/file.html"
86
+
87
+ # Unescape percent-encoded strings:
88
+ Protocol::HTTP::URL.unescape("hello%20world%21")
89
+ # => "hello world!"
90
+
91
+ # Handle Unicode characters:
92
+ Protocol::HTTP::URL.escape("café")
93
+ # => "caf%C3%A9"
94
+
95
+ Protocol::HTTP::URL.unescape("caf%C3%A9")
96
+ # => "café"
97
+ ```
98
+
99
+ ## Scanning and Processing Query Strings
100
+
101
+ For custom processing, you can scan query strings directly:
102
+
103
+ ``` ruby
104
+ query = "name=John&age=30&active=true"
105
+
106
+ Protocol::HTTP::URL.scan(query) do |key, value|
107
+ puts "#{key}: #{value}"
108
+ end
109
+ # Output:
110
+ # name: John
111
+ # age: 30
112
+ # active: true
113
+ ```
114
+
115
+ ## Security and Limits
116
+
117
+ The URL module includes built-in protection against deeply nested parameter attacks:
118
+
119
+ ``` ruby
120
+ # This will raise an error to prevent excessive nesting:
121
+ begin
122
+ Protocol::HTTP::URL.decode("a[b][c][d][e][f][g][h][i]=value")
123
+ rescue ArgumentError => error
124
+ puts error.message
125
+ # => "Key length exceeded limit!"
126
+ end
127
+
128
+ # You can adjust the maximum nesting level:
129
+ Protocol::HTTP::URL.decode("a[b][c]=value", 5) # Allow up to 5 levels of nesting
130
+ ```
@@ -21,7 +21,7 @@ module Protocol
21
21
  # The default wrappers to use for decoding content.
22
22
  DEFAULT_WRAPPERS = {
23
23
  "gzip" => Body::Inflate.method(:for),
24
- "identity" => ->(body) { body }, # Identity means no encoding
24
+ "identity" => ->(body) {body}, # Identity means no encoding
25
25
 
26
26
  # There is no point including this:
27
27
  # 'identity' => ->(body){body},
@@ -34,7 +34,7 @@ module Protocol
34
34
  @digest = digest
35
35
  @callback = callback
36
36
  end
37
-
37
+
38
38
  # @attribute [Digest] digest the digest object.
39
39
  attr :digest
40
40
 
@@ -34,7 +34,7 @@ module Protocol
34
34
 
35
35
  break unless chunk&.empty?
36
36
  end
37
-
37
+
38
38
  if chunk
39
39
  @output_length += chunk.bytesize
40
40
  elsif !stream.closed?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2023, by Genki Takiuchi.
6
6
 
7
7
  require_relative "buffered"
@@ -123,7 +123,7 @@ module Protocol
123
123
  if buffer.bytesize > length
124
124
  # This ensures the subsequent `slice!` works correctly.
125
125
  buffer.force_encoding(Encoding::BINARY)
126
-
126
+
127
127
  @buffer = buffer.byteslice(length, buffer.bytesize)
128
128
  buffer.slice!(length, buffer.bytesize)
129
129
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  module Protocol
7
7
  module HTTP
@@ -120,7 +120,7 @@ module Protocol
120
120
  # @parameter value_name [String] the directive name to search for (e.g., "max-age").
121
121
  # @returns [Integer | Nil] the parsed integer value, or `nil` if not found or invalid.
122
122
  def find_integer_value(value_name)
123
- if value = self.find { |value| value.start_with?(value_name) }
123
+ if value = self.find{|value| value.start_with?(value_name)}
124
124
  _, age = value.split("=", 2)
125
125
 
126
126
  if age =~ /\A[0-9]+\z/
@@ -64,13 +64,15 @@ module Protocol
64
64
  # Initialize the headers with the specified fields.
65
65
  #
66
66
  # @parameter fields [Array] An array of `[key, value]` pairs.
67
- # @parameter indexed [Hash] A hash table of normalized headers, if available.
68
- def initialize(fields = [], indexed = nil)
67
+ # @parameter tail [Integer | Nil] The index of the trailer start in the @fields array.
68
+ def initialize(fields = [], tail = nil, indexed: nil)
69
69
  @fields = fields
70
- @indexed = indexed
71
70
 
72
- # Marks where trailer start in the @fields array.
73
- @tail = nil
71
+ # Marks where trailer start in the @fields array:
72
+ @tail = tail
73
+
74
+ # The cached index of headers:
75
+ @indexed = nil
74
76
  end
75
77
 
76
78
  # Initialize a copy of the headers.
@@ -86,8 +88,8 @@ module Protocol
86
88
  # Clear all headers.
87
89
  def clear
88
90
  @fields.clear
89
- @indexed = nil
90
91
  @tail = nil
92
+ @indexed = nil
91
93
  end
92
94
 
93
95
  # Flatten trailer into the headers, in-place.
@@ -108,6 +110,14 @@ module Protocol
108
110
  # @attribute [Array] An array of `[key, value]` pairs.
109
111
  attr :fields
110
112
 
113
+ # @attribute [Integer | Nil] The index where trailers begin.
114
+ attr :tail
115
+
116
+ # @returns [Array] The fields of the headers.
117
+ def to_a
118
+ @fields
119
+ end
120
+
111
121
  # @returns [Boolean] Whether there are any trailers.
112
122
  def trailer?
113
123
  @tail != nil
@@ -142,24 +142,38 @@ module Protocol
142
142
  end
143
143
 
144
144
  # Update the reference with the given path, parameters and fragment.
145
- # @argument path [String] Append the string to this reference similar to `File.join`.
146
- # @argument parameters [Hash] Append the parameters to this reference.
147
- # @argument fragment [String] Set the fragment to this value.
148
- # @argument pop [Boolean] If the path contains a trailing filename, pop the last component of the path before appending the new path.
149
- # @argument merge [Boolean] If the parameters are specified, merge them with the existing parameters.
150
- def with(path: nil, parameters: nil, fragment: @fragment, pop: false, merge: true)
151
- if @parameters
152
- if parameters and merge
153
- parameters = @parameters.merge(parameters)
154
- else
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
155
162
  parameters = @parameters
156
163
  end
157
- end
158
-
159
- if @query and !merge
160
- query = nil
161
- else
164
+
162
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
163
177
  end
164
178
 
165
179
  if path
@@ -68,7 +68,7 @@ module Protocol
68
68
 
69
69
  # @attribute [Body::Readable] the request body. It should only be read once (it may not be idempotent).
70
70
  attr_accessor :body
71
-
71
+
72
72
  # @attribute [String | Array(String) | Nil] the request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. In HTTP/1, it is used to request a connection upgrade, and in HTTP/2 it is used to indicate a specfic protocol for the stream.
73
73
  attr_accessor :protocol
74
74
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative "body/buffered"
7
7
  require_relative "body/reader"
8
+ require_relative "headers"
8
9
 
9
10
  module Protocol
10
11
  module HTTP
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.51.1"
8
+ VERSION = "0.52.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -14,16 +14,30 @@ Provides abstractions for working with the HTTP protocol.
14
14
 
15
15
  Please see the [project documentation](https://socketry.github.io/protocol-http/) for more details.
16
16
 
17
- - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
18
-
19
17
  - [Getting Started](https://socketry.github.io/protocol-http/guides/getting-started/index) - This guide explains how to use `protocol-http` for building abstract HTTP interfaces.
20
18
 
19
+ - [Message Body](https://socketry.github.io/protocol-http/guides/message-body/index) - This guide explains how to work with HTTP request and response message bodies using `Protocol::HTTP::Body` classes.
20
+
21
+ - [Middleware](https://socketry.github.io/protocol-http/guides/middleware/index) - This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
22
+
23
+ - [Hypertext References](https://socketry.github.io/protocol-http/guides/hypertext-references/index) - This guide explains how to use `Protocol::HTTP::Reference` for constructing and manipulating hypertext references (URLs with parameters).
24
+
25
+ - [URL Parsing](https://socketry.github.io/protocol-http/guides/url-parsing/index) - This guide explains how to use `Protocol::HTTP::URL` for parsing and manipulating URL components, particularly query strings and parameters.
26
+
27
+ - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
28
+
21
29
  - [Design Overview](https://socketry.github.io/protocol-http/guides/design-overview/index) - This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
22
30
 
23
31
  ## Releases
24
32
 
25
33
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
26
34
 
35
+ ### v0.52.0
36
+
37
+ - Add `Protocol::HTTP::Headers#to_a` method that returns the fields array, providing compatibility with standard Ruby array conversion pattern.
38
+ - Expose `tail` in `Headers.new` so that trailers can be accurately reproduced.
39
+ - Add agent context.
40
+
27
41
  ### v0.51.0
28
42
 
29
43
  - `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v0.52.0
4
+
5
+ - Add `Protocol::HTTP::Headers#to_a` method that returns the fields array, providing compatibility with standard Ruby array conversion pattern.
6
+ - Expose `tail` in `Headers.new` so that trailers can be accurately reproduced.
7
+ - Add agent context.
8
+
3
9
  ## v0.51.0
4
10
 
5
11
  - `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.51.1
4
+ version: 0.52.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -53,6 +53,15 @@ executables: []
53
53
  extensions: []
54
54
  extra_rdoc_files: []
55
55
  files:
56
+ - agent.md
57
+ - context/design-overview.md
58
+ - context/getting-started.md
59
+ - context/hypertext-references.md
60
+ - context/index.yaml
61
+ - context/message-body.md
62
+ - context/middleware.md
63
+ - context/streaming.md
64
+ - context/url-parsing.md
56
65
  - lib/protocol/http.rb
57
66
  - lib/protocol/http/accept_encoding.rb
58
67
  - lib/protocol/http/body.rb
metadata.gz.sig CHANGED
Binary file