protocol-http 0.45.0 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/protocol/http/accept_encoding.rb +15 -3
  4. data/lib/protocol/http/body/buffered.rb +29 -2
  5. data/lib/protocol/http/body/completable.rb +13 -0
  6. data/lib/protocol/http/body/deflate.rb +33 -0
  7. data/lib/protocol/http/body/digestable.rb +19 -4
  8. data/lib/protocol/http/body/file.rb +37 -1
  9. data/lib/protocol/http/body/head.rb +8 -0
  10. data/lib/protocol/http/body/inflate.rb +10 -2
  11. data/lib/protocol/http/body/readable.rb +32 -11
  12. data/lib/protocol/http/body/reader.rb +17 -0
  13. data/lib/protocol/http/body/rewindable.rb +19 -1
  14. data/lib/protocol/http/body/stream.rb +34 -6
  15. data/lib/protocol/http/body/streamable.rb +46 -5
  16. data/lib/protocol/http/body/wrapper.rb +25 -3
  17. data/lib/protocol/http/body/writable.rb +48 -7
  18. data/lib/protocol/http/body.rb +16 -0
  19. data/lib/protocol/http/content_encoding.rb +13 -3
  20. data/lib/protocol/http/cookie.rb +23 -0
  21. data/lib/protocol/http/header/authorization.rb +10 -2
  22. data/lib/protocol/http/header/cache_control.rb +42 -10
  23. data/lib/protocol/http/header/connection.rb +18 -1
  24. data/lib/protocol/http/header/cookie.rb +10 -3
  25. data/lib/protocol/http/header/date.rb +9 -0
  26. data/lib/protocol/http/header/etag.rb +11 -0
  27. data/lib/protocol/http/header/etags.rb +35 -4
  28. data/lib/protocol/http/header/multiple.rb +9 -1
  29. data/lib/protocol/http/header/priority.rb +65 -0
  30. data/lib/protocol/http/header/split.rb +17 -3
  31. data/lib/protocol/http/header/vary.rb +10 -1
  32. data/lib/protocol/http/headers.rb +82 -19
  33. data/lib/protocol/http/methods.rb +5 -0
  34. data/lib/protocol/http/middleware/builder.rb +17 -0
  35. data/lib/protocol/http/middleware.rb +28 -0
  36. data/lib/protocol/http/peer.rb +9 -0
  37. data/lib/protocol/http/reference.rb +33 -6
  38. data/lib/protocol/http/request.rb +25 -0
  39. data/lib/protocol/http/response.rb +12 -0
  40. data/lib/protocol/http/url.rb +34 -8
  41. data/lib/protocol/http/version.rb +1 -1
  42. data/readme.md +4 -0
  43. data/releases.md +4 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +4 -2
  46. metadata.gz.sig +0 -0
@@ -12,19 +12,30 @@ module Protocol
12
12
  module HTTP
13
13
  # Encode a response according the the request's acceptable encodings.
14
14
  class ContentEncoding < Middleware
15
+ # The default wrappers to use for encoding content.
15
16
  DEFAULT_WRAPPERS = {
16
17
  "gzip" => Body::Deflate.method(:for)
17
18
  }
18
19
 
20
+ # The default content types to apply encoding to.
19
21
  DEFAULT_CONTENT_TYPES = %r{^(text/.*?)|(.*?/json)|(.*?/javascript)$}
20
22
 
21
- def initialize(app, content_types = DEFAULT_CONTENT_TYPES, wrappers = DEFAULT_WRAPPERS)
22
- super(app)
23
+ # Initialize the content encoding middleware.
24
+ #
25
+ # @parameter delegate [Middleware] The next middleware in the chain.
26
+ # @parameter content_types [Regexp] The content types to apply encoding to.
27
+ # @parameter wrappers [Hash] The encoding wrappers to use.
28
+ def initialize(delegate, content_types = DEFAULT_CONTENT_TYPES, wrappers = DEFAULT_WRAPPERS)
29
+ super(delegate)
23
30
 
24
31
  @content_types = content_types
25
32
  @wrappers = wrappers
26
33
  end
27
34
 
35
+ # Encode the response body according to the request's acceptable encodings.
36
+ #
37
+ # @parameter request [Request] The request.
38
+ # @returns [Response] The response.
28
39
  def call(request)
29
40
  response = super
30
41
 
@@ -40,7 +51,6 @@ module Protocol
40
51
 
41
52
  # TODO use http-accept and sort by priority
42
53
  if !response.body.empty? and accept_encoding = request.headers["accept-encoding"]
43
-
44
54
  if content_type = response.headers["content-type"] and @content_types =~ content_type
45
55
  body = response.body
46
56
 
@@ -10,24 +10,39 @@ module Protocol
10
10
  module HTTP
11
11
  # Represents an individual cookie key-value pair.
12
12
  class Cookie
13
+ # Initialize the cookie with the given name, value, and directives.
14
+ #
15
+ # @parameter name [String] The name of the cookiel, e.g. "session_id".
16
+ # @parameter value [String] The value of the cookie, e.g. "1234".
17
+ # @parameter directives [Hash] The directives of the cookie, e.g. `{"path" => "/"}`.
13
18
  def initialize(name, value, directives)
14
19
  @name = name
15
20
  @value = value
16
21
  @directives = directives
17
22
  end
18
23
 
24
+ # @attribute [String] The name of the cookie.
19
25
  attr :name
26
+
27
+ # @attribute [String] The value of the cookie.
20
28
  attr :value
29
+
30
+ # @attribute [Hash] The directives of the cookie.
21
31
  attr :directives
22
32
 
33
+ # Encode the name of the cookie.
23
34
  def encoded_name
24
35
  URL.escape(@name)
25
36
  end
26
37
 
38
+ # Encode the value of the cookie.
27
39
  def encoded_value
28
40
  URL.escape(@value)
29
41
  end
30
42
 
43
+ # Convert the cookie to a string.
44
+ #
45
+ # @returns [String] The string representation of the cookie.
31
46
  def to_s
32
47
  buffer = String.new.b
33
48
 
@@ -49,6 +64,10 @@ module Protocol
49
64
  return buffer
50
65
  end
51
66
 
67
+ # Parse a string into a cookie.
68
+ #
69
+ # @parameter string [String] The string to parse.
70
+ # @returns [Cookie] The parsed cookie.
52
71
  def self.parse(string)
53
72
  head, *directives = string.split(/\s*;\s*/)
54
73
 
@@ -62,6 +81,10 @@ module Protocol
62
81
  )
63
82
  end
64
83
 
84
+ # Parse a list of strings into a hash of directives.
85
+ #
86
+ # @parameter strings [Array(String)] The list of strings to parse.
87
+ # @returns [Hash] The hash of directives.
65
88
  def self.parse_directives(strings)
66
89
  strings.collect do |string|
67
90
  key, value = string.split("=", 2)
@@ -12,13 +12,21 @@ module Protocol
12
12
  # ~~~ ruby
13
13
  # headers.add('authorization', Authorization.basic("my_username", "my_password"))
14
14
  # ~~~
15
+ #
16
+ # TODO Support other authorization mechanisms, e.g. bearer token.
15
17
  class Authorization < String
16
- # Splits the header and
17
- # @return [Tuple(String, String)]
18
+ # Splits the header into the credentials.
19
+ #
20
+ # @returns [Tuple(String, String)] The username and password.
18
21
  def credentials
19
22
  self.split(/\s+/, 2)
20
23
  end
21
24
 
25
+ # Generate a new basic authorization header, encoding the given username and password.
26
+ #
27
+ # @parameter username [String] The username.
28
+ # @parameter password [String] The password.
29
+ # @returns [Authorization] The basic authorization header.
22
30
  def self.basic(username, password)
23
31
  strict_base64_encoded = ["#{username}:#{password}"].pack("m0")
24
32
 
@@ -9,86 +9,118 @@ require_relative "split"
9
9
  module Protocol
10
10
  module HTTP
11
11
  module Header
12
+ # Represents the `cache-control` header, which is a list of cache directives.
12
13
  class CacheControl < Split
14
+ # The `private` directive indicates that the response is intended for a single user and must not be stored by shared caches.
13
15
  PRIVATE = "private"
16
+
17
+ # The `public` directive indicates that the response may be stored by any cache, even if it would normally be considered non-cacheable.
14
18
  PUBLIC = "public"
19
+
20
+ # The `no-cache` directive indicates that caches must revalidate the response with the origin server before serving it to clients.
15
21
  NO_CACHE = "no-cache"
22
+
23
+ # The `no-store` directive indicates that caches must not store the response under any circumstances.
16
24
  NO_STORE = "no-store"
25
+
26
+ # The `max-age` directive indicates the maximum amount of time, in seconds, that a response is considered fresh.
17
27
  MAX_AGE = "max-age"
28
+
29
+ # The `s-maxage` directive is similar to `max-age` but applies only to shared caches. If both `s-maxage` and `max-age` are present, `s-maxage` takes precedence in shared caches.
18
30
  S_MAXAGE = "s-maxage"
19
31
 
32
+ # The `static` directive is a custom directive often used to indicate that the resource is immutable or rarely changes, allowing longer caching periods.
20
33
  STATIC = "static"
34
+
35
+ # The `dynamic` directive is a custom directive used to indicate that the resource is generated dynamically and may change frequently, requiring shorter caching periods.
21
36
  DYNAMIC = "dynamic"
37
+
38
+ # The `streaming` directive is a custom directive used to indicate that the resource is intended for progressive or chunked delivery, such as live video streams.
22
39
  STREAMING = "streaming"
23
40
 
41
+ # The `must-revalidate` directive indicates that once a response becomes stale, caches must not use it to satisfy subsequent requests without revalidating it with the origin server.
24
42
  MUST_REVALIDATE = "must-revalidate"
43
+
44
+ # The `proxy-revalidate` directive is similar to `must-revalidate` but applies only to shared caches.
25
45
  PROXY_REVALIDATE = "proxy-revalidate"
26
46
 
47
+ # Initializes the cache control header with the given value. The value is expected to be a comma-separated string of cache directives.
48
+ #
49
+ # @parameter value [String | Nil] the raw Cache-Control header value.
27
50
  def initialize(value = nil)
28
51
  super(value&.downcase)
29
52
  end
30
53
 
54
+ # Adds a directive to the Cache-Control header. The value will be normalized to lowercase before being added.
55
+ #
56
+ # @parameter value [String] the directive to add.
31
57
  def << value
32
58
  super(value.downcase)
33
59
  end
34
60
 
61
+ # @returns [Boolean] whether the `static` directive is present.
35
62
  def static?
36
63
  self.include?(STATIC)
37
64
  end
38
65
 
66
+ # @returns [Boolean] whether the `dynamic` directive is present.
39
67
  def dynamic?
40
68
  self.include?(DYNAMIC)
41
69
  end
42
70
 
71
+ # @returns [Boolean] whether the `streaming` directive is present.
43
72
  def streaming?
44
73
  self.include?(STREAMING)
45
74
  end
46
75
 
76
+ # @returns [Boolean] whether the `private` directive is present.
47
77
  def private?
48
78
  self.include?(PRIVATE)
49
79
  end
50
80
 
81
+ # @returns [Boolean] whether the `public` directive is present.
51
82
  def public?
52
83
  self.include?(PUBLIC)
53
84
  end
54
85
 
86
+ # @returns [Boolean] whether the `no-cache` directive is present.
55
87
  def no_cache?
56
88
  self.include?(NO_CACHE)
57
89
  end
58
90
 
91
+ # @returns [Boolean] whether the `no-store` directive is present.
59
92
  def no_store?
60
93
  self.include?(NO_STORE)
61
94
  end
62
95
 
63
- # Indicates that a response must not be used once it is stale.
64
- # See https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate
96
+ # @returns [Boolean] whether the `must-revalidate` directive is present.
65
97
  def must_revalidate?
66
98
  self.include?(MUST_REVALIDATE)
67
99
  end
68
100
 
69
- # Like must-revalidate, but for shared caches only.
70
- # See https://www.rfc-editor.org/rfc/rfc9111.html#name-proxy-revalidate
101
+ # @returns [Boolean] whether the `proxy-revalidate` directive is present.
71
102
  def proxy_revalidate?
72
103
  self.include?(PROXY_REVALIDATE)
73
104
  end
74
105
 
75
- # The maximum time, in seconds, a response should be considered fresh.
76
- # See https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2
106
+ # @returns [Integer | Nil] the value of the `max-age` directive in seconds, or `nil` if the directive is not present or invalid.
77
107
  def max_age
78
108
  find_integer_value(MAX_AGE)
79
109
  end
80
110
 
81
- # Like max-age, but for shared caches only, which should use it before
82
- # max-age when present.
83
- # See https://www.rfc-editor.org/rfc/rfc9111.html#name-s-maxage
111
+ # @returns [Integer | Nil] the value of the `s-maxage` directive in seconds, or `nil` if the directive is not present or invalid.
84
112
  def s_maxage
85
113
  find_integer_value(S_MAXAGE)
86
114
  end
87
115
 
88
116
  private
89
117
 
118
+ # Finds and parses an integer value from a directive.
119
+ #
120
+ # @parameter value_name [String] the directive name to search for (e.g., "max-age").
121
+ # @returns [Integer | Nil] the parsed integer value, or `nil` if not found or invalid.
90
122
  def find_integer_value(value_name)
91
- if value = self.find{|value| value.start_with?(value_name)}
123
+ if value = self.find { |value| value.start_with?(value_name) }
92
124
  _, age = value.split("=", 2)
93
125
 
94
126
  if age =~ /\A[0-9]+\z/
@@ -9,31 +9,48 @@ require_relative "split"
9
9
  module Protocol
10
10
  module HTTP
11
11
  module Header
12
+ # Represents the `connection` HTTP header, which controls options for the current connection.
13
+ #
14
+ # The `connection` header is used to specify control options such as whether the connection should be kept alive, closed, or upgraded to a different protocol.
12
15
  class Connection < Split
16
+ # The `keep-alive` directive indicates that the connection should remain open for future requests or responses, avoiding the overhead of opening a new connection.
13
17
  KEEP_ALIVE = "keep-alive"
18
+
19
+ # The `close` directive indicates that the connection should be closed after the current request and response are complete.
14
20
  CLOSE = "close"
21
+
22
+ # The `upgrade` directive indicates that the connection should be upgraded to a different protocol, as specified in the `Upgrade` header.
15
23
  UPGRADE = "upgrade"
16
24
 
25
+ # Initializes the connection header with the given value. The value is expected to be a comma-separated string of directives.
26
+ #
27
+ # @parameter value [String | Nil] the raw `connection` header value.
17
28
  def initialize(value = nil)
18
29
  super(value&.downcase)
19
30
  end
20
31
 
32
+ # Adds a directive to the `connection` header. The value will be normalized to lowercase before being added.
33
+ #
34
+ # @parameter value [String] the directive to add.
21
35
  def << value
22
36
  super(value.downcase)
23
37
  end
24
38
 
39
+ # @returns [Boolean] whether the `keep-alive` directive is present and the connection is not marked for closure with the `close` directive.
25
40
  def keep_alive?
26
41
  self.include?(KEEP_ALIVE) && !close?
27
42
  end
28
43
 
44
+ # @returns [Boolean] whether the `close` directive is present, indicating that the connection should be closed after the current request and response.
29
45
  def close?
30
46
  self.include?(CLOSE)
31
47
  end
32
48
 
49
+ # @returns [Boolean] whether the `upgrade` directive is present, indicating that the connection should be upgraded to a different protocol.
33
50
  def upgrade?
34
51
  self.include?(UPGRADE)
35
52
  end
36
53
  end
37
54
  end
38
55
  end
39
- end
56
+ end
@@ -9,8 +9,13 @@ require_relative "../cookie"
9
9
  module Protocol
10
10
  module HTTP
11
11
  module Header
12
- # The Cookie HTTP request header contains stored HTTP cookies previously sent by the server with the Set-Cookie header.
12
+ # The `cookie` header contains stored HTTP cookies previously sent by the server with the `set-cookie` header.
13
+ #
14
+ # It is used by clients to send key-value pairs representing stored cookies back to the server.
13
15
  class Cookie < Multiple
16
+ # Parses the `cookie` header into a hash of cookie names and their corresponding cookie objects.
17
+ #
18
+ # @returns [Hash(String, HTTP::Cookie)] a hash where keys are cookie names and values are {HTTP::Cookie} objects.
14
19
  def to_h
15
20
  cookies = self.collect do |string|
16
21
  HTTP::Cookie.parse(string)
@@ -20,9 +25,11 @@ module Protocol
20
25
  end
21
26
  end
22
27
 
23
- # The Set-Cookie HTTP response header sends cookies from the server to the user agent.
28
+ # The `set-cookie` header sends cookies from the server to the user agent.
29
+ #
30
+ # It is used to store cookies on the client side, which are then sent back to the server in subsequent requests using the `cookie` header.
24
31
  class SetCookie < Cookie
25
32
  end
26
33
  end
27
34
  end
28
- end
35
+ end
@@ -8,11 +8,20 @@ require "time"
8
8
  module Protocol
9
9
  module HTTP
10
10
  module Header
11
+ # The `date` header represents the date and time at which the message was originated.
12
+ #
13
+ # This header is typically included in HTTP responses and follows the format defined in RFC 9110.
11
14
  class Date < String
15
+ # Replaces the current value of the `date` header with the specified value.
16
+ #
17
+ # @parameter value [String] the new value for the `date` header.
12
18
  def << value
13
19
  replace(value)
14
20
  end
15
21
 
22
+ # Converts the `date` header value to a `Time` object.
23
+ #
24
+ # @returns [Time] the parsed time object corresponding to the `date` header value.
16
25
  def to_time
17
26
  ::Time.parse(self)
18
27
  end
@@ -6,11 +6,22 @@
6
6
  module Protocol
7
7
  module HTTP
8
8
  module Header
9
+ # The `etag` header represents the entity tag for a resource.
10
+ #
11
+ # The `etag` header provides a unique identifier for a specific version of a resource, typically used for cache validation or conditional requests. It can be either a strong or weak validator as defined in RFC 9110.
9
12
  class ETag < String
13
+ # Replaces the current value of the `etag` header with the specified value.
14
+ #
15
+ # @parameter value [String] the new value for the `etag` header.
10
16
  def << value
11
17
  replace(value)
12
18
  end
13
19
 
20
+ # Checks whether the `etag` is a weak validator.
21
+ #
22
+ # Weak validators indicate semantically equivalent content but may not be byte-for-byte identical.
23
+ #
24
+ # @returns [Boolean] whether the `etag` is weak.
14
25
  def weak?
15
26
  self.start_with?("W/")
16
27
  end
@@ -9,32 +9,63 @@ require_relative "split"
9
9
  module Protocol
10
10
  module HTTP
11
11
  module Header
12
+ # The `etags` header represents a list of entity tags (ETags) for resources.
13
+ #
14
+ # The `etags` header is used for conditional requests to compare the current version of a resource with previously stored versions. It supports both strong and weak validators, as well as the wildcard character (`*`) to indicate a match for any resource.
12
15
  class ETags < Split
16
+ # Checks if the `etags` header contains the wildcard (`*`) character.
17
+ #
18
+ # The wildcard character matches any resource version, regardless of its actual value.
19
+ #
20
+ # @returns [Boolean] whether the wildcard is present.
13
21
  def wildcard?
14
22
  self.include?("*")
15
23
  end
16
24
 
17
- # This implementation is not strictly correct according to the RFC-specified format.
25
+ # Checks if the specified ETag matches the `etags` header.
26
+ #
27
+ # This method returns `true` if the wildcard is present or if the exact ETag is found in the list. Note that this implementation is not strictly compliant with the RFC-specified format.
28
+ #
29
+ # @parameter etag [String] the ETag to compare against the `etags` header.
30
+ # @returns [Boolean] whether the specified ETag matches.
18
31
  def match?(etag)
19
32
  wildcard? || self.include?(etag)
20
33
  end
21
34
 
22
- # Useful with If-Match
35
+ # Checks for a strong match with the specified ETag, useful with the `if-match` header.
36
+ #
37
+ # A strong match requires that the ETag in the header list matches the specified ETag and that neither is a weak validator.
38
+ #
39
+ # @parameter etag [String] the ETag to compare against the `etags` header.
40
+ # @returns [Boolean] whether a strong match is found.
23
41
  def strong_match?(etag)
24
42
  wildcard? || (!weak_tag?(etag) && self.include?(etag))
25
43
  end
26
44
 
27
- # Useful with If-None-Match
45
+ # Checks for a weak match with the specified ETag, useful with the `if-none-match` header.
46
+ #
47
+ # A weak match allows for semantically equivalent content, including weak validators and their strong counterparts.
48
+ #
49
+ # @parameter etag [String] the ETag to compare against the `etags` header.
50
+ # @returns [Boolean] whether a weak match is found.
28
51
  def weak_match?(etag)
29
52
  wildcard? || self.include?(etag) || self.include?(opposite_tag(etag))
30
53
  end
31
54
 
32
55
  private
33
-
56
+
57
+ # Converts a weak tag to its strong counterpart or vice versa.
58
+ #
59
+ # @parameter etag [String] the ETag to convert.
60
+ # @returns [String] the opposite form of the provided ETag.
34
61
  def opposite_tag(etag)
35
62
  weak_tag?(etag) ? etag[2..-1] : "W/#{etag}"
36
63
  end
37
64
 
65
+ # Checks if the given ETag is a weak validator.
66
+ #
67
+ # @parameter tag [String] the ETag to check.
68
+ # @returns [Boolean] whether the tag is weak.
38
69
  def weak_tag?(tag)
39
70
  tag&.start_with? "W/"
40
71
  end
@@ -6,14 +6,22 @@
6
6
  module Protocol
7
7
  module HTTP
8
8
  module Header
9
- # Header value which is split by newline charaters (e.g. cookies).
9
+ # Represents headers that can contain multiple distinct values separated by newline characters.
10
+ #
11
+ # This isn't a specific header but is used as a base for headers that store multiple values, such as cookies. The values are split and stored as an array internally, and serialized back to a newline-separated string when needed.
10
12
  class Multiple < Array
13
+ # Initializes the multiple header with the given value. As the header key-value pair can only contain one value, the value given here is added to the internal array, and subsequent values can be added using the `<<` operator.
14
+ #
15
+ # @parameter value [String] the raw header value.
11
16
  def initialize(value)
12
17
  super()
13
18
 
14
19
  self << value
15
20
  end
16
21
 
22
+ # Serializes the stored values into a newline-separated string.
23
+ #
24
+ # @returns [String] the serialized representation of the header values.
17
25
  def to_s
18
26
  join("\n")
19
27
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "split"
7
+
8
+ module Protocol
9
+ module HTTP
10
+ module Header
11
+ # Represents the `priority` header, used to indicate the relative importance of an HTTP request.
12
+ #
13
+ # The `priority` header allows clients to express their preference for how resources should be prioritized by the server. It can include directives like `urgency` to specify the importance of a request, and `progressive` to indicate whether a response can be delivered incrementally.
14
+ class Priority < Split
15
+ # Urgency levels as defined in RFC 9218:
16
+ #
17
+ # These levels indicate the relative importance of a request, helping servers and intermediaries allocate resources efficiently. Properly setting urgency can significantly improve user-perceived performance by prioritizing critical content and deferring less important tasks.
18
+ module Urgency
19
+ # `background` priority indicates a request that is not time-sensitive and can be processed with minimal impact to other tasks. It is ideal for requests like analytics or logging, which do not directly impact the user's current experience.
20
+ BACKGROUND = "background"
21
+
22
+ # `low` priority indicates a request that is important but not critical. It is suitable for content like non-blocking images, videos, or scripts that enhance the experience but do not affect core functionality.
23
+ LOW = "low"
24
+
25
+ # `normal` priority (default) indicates the standard priority for most requests. It is appropriate for content like text, CSS, or essential images that are necessary for the primary user experience but do not require urgent delivery.
26
+ NORMAL = "normal"
27
+
28
+ # `high` priority indicates the highest priority, used for requests that are essential and time-critical to the user experience. Examples include content above-the-fold on a webpage, critical API calls, or resources required for rendering.
29
+ HIGH = "high"
30
+ end
31
+
32
+ # The `progressive` flag indicates that the response can be delivered incrementally (progressively) as data becomes available. This is particularly useful for large resources like images or video streams, where partial delivery improves the user experience by allowing content to render or play before the full response is received.
33
+ PROGRESSIVE = "progressive"
34
+
35
+ # Initialize the priority header with the given value.
36
+ #
37
+ # @parameter value [String | Nil] the value of the priority header, if any.
38
+ def initialize(value = nil)
39
+ super(value&.downcase)
40
+ end
41
+
42
+ # Add a value to the priority header.
43
+ def << value
44
+ super(value.downcase)
45
+ end
46
+
47
+ # Returns the urgency level if specified.
48
+ #
49
+ # @returns [String | Nil] the urgency level if specified, or `nil`.
50
+ def urgency
51
+ if value = self.find{|value| value.start_with?("urgency=")}
52
+ _, level = value.split("=", 2)
53
+
54
+ return level
55
+ end
56
+ end
57
+
58
+ # @returns [Boolean] whether the request should be delivered progressively.
59
+ def progressive?
60
+ self.include?(PROGRESSIVE)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -6,22 +6,36 @@
6
6
  module Protocol
7
7
  module HTTP
8
8
  module Header
9
- # Header value which is split by commas.
9
+ # Represents headers that can contain multiple distinct values separated by commas.
10
+ #
11
+ # This isn't a specific header class is a utility for handling headers with comma-separated values, such as `accept`, `cache-control`, and other similar headers. The values are split and stored as an array internally, and serialized back to a comma-separated string when needed.
10
12
  class Split < Array
13
+ # Regular expression used to split values on commas, with optional surrounding whitespace.
11
14
  COMMA = /\s*,\s*/
12
15
 
13
- def initialize(value)
16
+ # Initializes a `Split` header with the given value. If the value is provided, it is split into distinct entries and stored as an array.
17
+ #
18
+ # @parameter value [String | Nil] the raw header value containing multiple entries separated by commas, or `nil` for an empty header.
19
+ def initialize(value = nil)
14
20
  if value
15
21
  super(value.split(COMMA))
16
22
  else
17
- super([])
23
+ super()
18
24
  end
19
25
  end
20
26
 
27
+ # Adds one or more comma-separated values to the header.
28
+ #
29
+ # The input string is split into distinct entries and appended to the array.
30
+ #
31
+ # @parameter value [String] the value or values to add, separated by commas.
21
32
  def << value
22
33
  self.push(*value.split(COMMA))
23
34
  end
24
35
 
36
+ # Serializes the stored values into a comma-separated string.
37
+ #
38
+ # @returns [String] the serialized representation of the header values.
25
39
  def to_s
26
40
  join(",")
27
41
  end
@@ -8,15 +8,24 @@ require_relative "split"
8
8
  module Protocol
9
9
  module HTTP
10
10
  module Header
11
+ # Represents the `vary` header, which specifies the request headers a server considers when determining the response.
12
+ #
13
+ # The `vary` header is used in HTTP responses to indicate which request headers affect the selected response. It allows caches to differentiate stored responses based on specific request headers.
11
14
  class Vary < Split
15
+ # Initializes a `Vary` header with the given value. The value is split into distinct entries and converted to lowercase for normalization.
16
+ #
17
+ # @parameter value [String] the raw header value containing request header names separated by commas.
12
18
  def initialize(value)
13
19
  super(value.downcase)
14
20
  end
15
21
 
22
+ # Adds one or more comma-separated values to the `vary` header. The values are converted to lowercase for normalization.
23
+ #
24
+ # @parameter value [String] the value or values to add, separated by commas.
16
25
  def << value
17
26
  super(value.downcase)
18
27
  end
19
28
  end
20
29
  end
21
30
  end
22
- end
31
+ end