philiprehberger-header_kit 0.4.0 → 0.6.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
- data/CHANGELOG.md +10 -0
- data/README.md +29 -0
- data/lib/philiprehberger/header_kit/accept_encoding.rb +31 -1
- data/lib/philiprehberger/header_kit/etag.rb +95 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +22 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 255a0f1f4321e63ae82d6acaf028cddb9a041d4f1594b16dcbb8f2a51f54a286
|
|
4
|
+
data.tar.gz: 1a0ea312d1937e9e17c7cabe31f7822345d0de02f4627d09c597c5a71a5b7715
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee48374d55f01e11ae5b499c31556037801082a6631ed60306d544109897715bf5510eff7403675e2e94795257903efe289469bd790181cc2c32fef70e65a975
|
|
7
|
+
data.tar.gz: 40cf00b3c4d3d9f88d50ab21f08fc2dfb2d75b9ee13e879f985d59b28a14e81c600376d13d8f13f9bafb4edd3ff9350d6c4f6d5766abf20e9dd94bd2fd9e706c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `HeaderKit.build_accept_encoding(encodings)` for constructing Accept-Encoding headers from an array of `{encoding:, quality:}` hashes, mirroring the existing `build_accept` builder
|
|
14
|
+
|
|
15
|
+
## [0.5.0] - 2026-04-17
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `HeaderKit.etag_match?(header_value, resource_etag)` for If-None-Match / If-Match evaluation with list, weak-prefix, and `*` wildcard support
|
|
19
|
+
|
|
10
20
|
## [0.4.0] - 2026-04-04
|
|
11
21
|
|
|
12
22
|
### Added
|
data/README.md
CHANGED
|
@@ -66,6 +66,17 @@ Philiprehberger::HeaderKit.parse_accept_encoding("gzip, deflate;q=0.5, br;q=0.8"
|
|
|
66
66
|
# => [{encoding: "gzip", quality: 1.0}, {encoding: "br", quality: 0.8}, {encoding: "deflate", quality: 0.5}]
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Build Accept-Encoding
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Philiprehberger::HeaderKit.build_accept_encoding([
|
|
73
|
+
{encoding: "gzip"},
|
|
74
|
+
{encoding: "br", quality: 0.9},
|
|
75
|
+
{encoding: "deflate", quality: 0.5}
|
|
76
|
+
])
|
|
77
|
+
# => "gzip, br;q=0.9, deflate;q=0.5"
|
|
78
|
+
```
|
|
79
|
+
|
|
69
80
|
### Parse Authorization
|
|
70
81
|
|
|
71
82
|
```ruby
|
|
@@ -181,6 +192,22 @@ Philiprehberger::HeaderKit.parse_via('1.1 vegur, 1.0 fred')
|
|
|
181
192
|
# => [{protocol: "1.1", host: "vegur"}, {protocol: "1.0", host: "fred"}]
|
|
182
193
|
```
|
|
183
194
|
|
|
195
|
+
### Conditional-request matching
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'abc')
|
|
199
|
+
# => true
|
|
200
|
+
|
|
201
|
+
Philiprehberger::HeaderKit.etag_match?('W/"abc", W/"xyz"', 'xyz')
|
|
202
|
+
# => true
|
|
203
|
+
|
|
204
|
+
Philiprehberger::HeaderKit.etag_match?('*', 'anything')
|
|
205
|
+
# => true
|
|
206
|
+
|
|
207
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'nope')
|
|
208
|
+
# => false
|
|
209
|
+
```
|
|
210
|
+
|
|
184
211
|
### Parse Retry-After
|
|
185
212
|
|
|
186
213
|
```ruby
|
|
@@ -200,6 +227,7 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
200
227
|
| `HeaderKit.parse_accept_language(header)` | Parse Accept-Language into sorted entries |
|
|
201
228
|
| `HeaderKit.negotiate_language(header, available)` | Language negotiation, returns best match or nil |
|
|
202
229
|
| `HeaderKit.parse_accept_encoding(header)` | Parse Accept-Encoding into sorted entries |
|
|
230
|
+
| `HeaderKit.build_accept_encoding(encodings)` | Build Accept-Encoding header string from encoding array |
|
|
203
231
|
| `HeaderKit.parse_authorization(header)` | Parse Authorization header (Bearer, Basic, Digest) |
|
|
204
232
|
| `HeaderKit.parse_cache_control(header)` | Parse Cache-Control into directive hash |
|
|
205
233
|
| `HeaderKit.build_cache_control(directives)` | Build Cache-Control string from hash |
|
|
@@ -215,6 +243,7 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
215
243
|
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
216
244
|
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
217
245
|
| `HeaderKit.parse_retry_after(header)` | Parse Retry-After header (seconds or HTTP date) |
|
|
246
|
+
| `HeaderKit.etag_match?(header_value, resource_etag)` | Check If-None-Match / If-Match against a resource ETag (list, `W/` weak prefix, `*` wildcard) |
|
|
218
247
|
|
|
219
248
|
## Development
|
|
220
249
|
|
|
@@ -50,7 +50,37 @@ module Philiprehberger
|
|
|
50
50
|
value.to_f.clamp(0.0, 1.0)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
# Build an Accept-Encoding header string from an array of encoding hashes.
|
|
54
|
+
#
|
|
55
|
+
# @param encodings [Array<Hash>] each with :encoding and optional :quality
|
|
56
|
+
# @return [String] formatted Accept-Encoding header value
|
|
57
|
+
def self.build(encodings)
|
|
58
|
+
return '' if encodings.nil? || encodings.empty?
|
|
59
|
+
|
|
60
|
+
parts = encodings.map do |entry|
|
|
61
|
+
encoding = entry[:encoding]
|
|
62
|
+
quality = entry[:quality]
|
|
63
|
+
|
|
64
|
+
if quality && quality < 1.0
|
|
65
|
+
"#{encoding};q=#{format_quality(quality)}"
|
|
66
|
+
else
|
|
67
|
+
encoding
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
parts.join(', ')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Format a quality value, removing trailing zeros.
|
|
75
|
+
#
|
|
76
|
+
# @param quality [Float] the quality value
|
|
77
|
+
# @return [String] formatted quality string
|
|
78
|
+
def self.format_quality(quality)
|
|
79
|
+
formatted = format('%.3f', quality)
|
|
80
|
+
formatted.sub(/0+\z/, '').sub(/\.\z/, '.0')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method :parse_entry, :parse_quality, :format_quality
|
|
54
84
|
end
|
|
55
85
|
end
|
|
56
86
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
# Evaluates If-None-Match / If-Match style header values against a
|
|
6
|
+
# resource ETag.
|
|
7
|
+
#
|
|
8
|
+
# Accepts single ETags, comma-separated lists, weak-prefixed values
|
|
9
|
+
# (e.g. `W/"abc"`), and the `*` wildcard. Strong and weak semantics
|
|
10
|
+
# collapse to equality on the inner token — callers that need strict
|
|
11
|
+
# strong/weak differentiation should implement that separately.
|
|
12
|
+
module Etag
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Check whether a header value matches a resource ETag.
|
|
16
|
+
#
|
|
17
|
+
# @param header_value [String, nil] the raw If-None-Match / If-Match header
|
|
18
|
+
# @param resource_etag [String, nil] the resource ETag (unquoted, no `W/` prefix)
|
|
19
|
+
# @return [Boolean] true if any value in the header matches the resource
|
|
20
|
+
def match?(header_value, resource_etag)
|
|
21
|
+
return false if header_value.nil?
|
|
22
|
+
|
|
23
|
+
stripped = header_value.strip
|
|
24
|
+
return false if stripped.empty?
|
|
25
|
+
|
|
26
|
+
tokens = split_values(stripped)
|
|
27
|
+
return false if tokens.empty?
|
|
28
|
+
|
|
29
|
+
tokens.any? { |token| token_matches?(token, resource_etag) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Split a header value into individual ETag tokens.
|
|
33
|
+
#
|
|
34
|
+
# Quoted sections (including escaped characters) are preserved so commas
|
|
35
|
+
# inside a quoted ETag do not split the value.
|
|
36
|
+
#
|
|
37
|
+
# @param value [String] the header value to split
|
|
38
|
+
# @return [Array<String>] trimmed, non-empty tokens
|
|
39
|
+
def split_values(value)
|
|
40
|
+
tokens = []
|
|
41
|
+
buffer = +''
|
|
42
|
+
in_quotes = false
|
|
43
|
+
escaped = false
|
|
44
|
+
|
|
45
|
+
value.each_char do |char|
|
|
46
|
+
if escaped
|
|
47
|
+
buffer << char
|
|
48
|
+
escaped = false
|
|
49
|
+
elsif in_quotes && char == '\\'
|
|
50
|
+
buffer << char
|
|
51
|
+
escaped = true
|
|
52
|
+
elsif char == '"'
|
|
53
|
+
buffer << char
|
|
54
|
+
in_quotes = !in_quotes
|
|
55
|
+
elsif char == ',' && !in_quotes
|
|
56
|
+
tokens << buffer.strip unless buffer.strip.empty?
|
|
57
|
+
buffer = +''
|
|
58
|
+
else
|
|
59
|
+
buffer << char
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
tokens << buffer.strip unless buffer.strip.empty?
|
|
64
|
+
tokens
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine whether a single token matches the resource ETag.
|
|
68
|
+
#
|
|
69
|
+
# @param token [String] a single header token
|
|
70
|
+
# @param resource_etag [String, nil] the resource ETag
|
|
71
|
+
# @return [Boolean] true if the token matches
|
|
72
|
+
def token_matches?(token, resource_etag)
|
|
73
|
+
return !resource_etag.nil? if token == '*'
|
|
74
|
+
return false if resource_etag.nil?
|
|
75
|
+
|
|
76
|
+
unwrap(token) == resource_etag
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Strip weak prefix and surrounding quotes from an ETag token.
|
|
80
|
+
#
|
|
81
|
+
# Backslash-escaped characters inside the quoted value are unescaped.
|
|
82
|
+
#
|
|
83
|
+
# @param token [String] a single ETag token
|
|
84
|
+
# @return [String] the inner ETag value
|
|
85
|
+
def unwrap(token)
|
|
86
|
+
value = token.sub(%r{\AW/}i, '')
|
|
87
|
+
|
|
88
|
+
return value unless value.start_with?('"') && value.end_with?('"') && value.length >= 2
|
|
89
|
+
|
|
90
|
+
inner = value[1..-2]
|
|
91
|
+
inner.gsub(/\\(.)/, '\1')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative 'header_kit/authorization'
|
|
|
9
9
|
require_relative 'header_kit/cache_control'
|
|
10
10
|
require_relative 'header_kit/content_type'
|
|
11
11
|
require_relative 'header_kit/cookie'
|
|
12
|
+
require_relative 'header_kit/etag'
|
|
12
13
|
require_relative 'header_kit/link'
|
|
13
14
|
require_relative 'header_kit/negotiation'
|
|
14
15
|
require_relative 'header_kit/cors'
|
|
@@ -44,6 +45,14 @@ module Philiprehberger
|
|
|
44
45
|
AcceptEncoding.parse(header)
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# Build an Accept-Encoding header string from an array of encoding hashes.
|
|
49
|
+
#
|
|
50
|
+
# @param encodings [Array<Hash>] each with :encoding and optional :quality
|
|
51
|
+
# @return [String] formatted Accept-Encoding header value
|
|
52
|
+
def self.build_accept_encoding(encodings)
|
|
53
|
+
AcceptEncoding.build(encodings)
|
|
54
|
+
end
|
|
55
|
+
|
|
47
56
|
# Parse an Accept-Language header into structured entries sorted by quality.
|
|
48
57
|
#
|
|
49
58
|
# @param header [String] the Accept-Language header value
|
|
@@ -176,6 +185,19 @@ module Philiprehberger
|
|
|
176
185
|
Forwarded.parse_via(header)
|
|
177
186
|
end
|
|
178
187
|
|
|
188
|
+
# Check whether an If-None-Match / If-Match header matches a resource ETag.
|
|
189
|
+
#
|
|
190
|
+
# Accepts a single ETag, comma-separated list, weak-prefixed values
|
|
191
|
+
# (e.g. `W/"abc"`), or the `*` wildcard. Weak and strong ETags collapse
|
|
192
|
+
# to equality on the inner token.
|
|
193
|
+
#
|
|
194
|
+
# @param header_value [String, nil] the raw header value
|
|
195
|
+
# @param resource_etag [String, nil] the resource ETag (unquoted, no `W/` prefix)
|
|
196
|
+
# @return [Boolean] true if any value in the header matches the resource
|
|
197
|
+
def self.etag_match?(header_value, resource_etag)
|
|
198
|
+
Etag.match?(header_value, resource_etag)
|
|
199
|
+
end
|
|
200
|
+
|
|
179
201
|
# Parse a Retry-After header.
|
|
180
202
|
#
|
|
181
203
|
# Returns a hash with :seconds (Integer) for numeric values,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-header_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Parse and build Accept, Accept-Language, Accept-Encoding, Authorization,
|
|
14
14
|
Cache-Control, Content-Type, Cookie, Link, CORS, Forwarded, and Via HTTP headers.
|
|
@@ -32,6 +32,7 @@ files:
|
|
|
32
32
|
- lib/philiprehberger/header_kit/content_type.rb
|
|
33
33
|
- lib/philiprehberger/header_kit/cookie.rb
|
|
34
34
|
- lib/philiprehberger/header_kit/cors.rb
|
|
35
|
+
- lib/philiprehberger/header_kit/etag.rb
|
|
35
36
|
- lib/philiprehberger/header_kit/forwarded.rb
|
|
36
37
|
- lib/philiprehberger/header_kit/link.rb
|
|
37
38
|
- lib/philiprehberger/header_kit/negotiation.rb
|