philiprehberger-header_kit 0.4.0 → 0.5.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 +5 -0
- data/README.md +17 -0
- data/lib/philiprehberger/header_kit/etag.rb +95 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +14 -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: d06951b5686f64b328c573fc16904e84eddfc61424ab8491fc2da817c7241fca
|
|
4
|
+
data.tar.gz: 9bdc24a4fc3dabc8d34ccdc105564e5669e9024ada60869c09317fdc11f1c1d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82e06b0e3e146393f17782091fd76c5813be7c46df09c6366d9e892d40a4d05e652655b0017c2cd8242a8812961b428f853cb771e74721e08c46b778911dd124
|
|
7
|
+
data.tar.gz: f9970a177aa61fa120ea97eceeeda50f19d9517c143b2d91c4bce924a26466c13de2ca88f2a5e00375cadebae0cf2f804436ace824ac0fa3a53d00dbf8f4efec
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `HeaderKit.etag_match?(header_value, resource_etag)` for If-None-Match / If-Match evaluation with list, weak-prefix, and `*` wildcard support
|
|
14
|
+
|
|
10
15
|
## [0.4.0] - 2026-04-04
|
|
11
16
|
|
|
12
17
|
### Added
|
data/README.md
CHANGED
|
@@ -181,6 +181,22 @@ Philiprehberger::HeaderKit.parse_via('1.1 vegur, 1.0 fred')
|
|
|
181
181
|
# => [{protocol: "1.1", host: "vegur"}, {protocol: "1.0", host: "fred"}]
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Conditional-request matching
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'abc')
|
|
188
|
+
# => true
|
|
189
|
+
|
|
190
|
+
Philiprehberger::HeaderKit.etag_match?('W/"abc", W/"xyz"', 'xyz')
|
|
191
|
+
# => true
|
|
192
|
+
|
|
193
|
+
Philiprehberger::HeaderKit.etag_match?('*', 'anything')
|
|
194
|
+
# => true
|
|
195
|
+
|
|
196
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'nope')
|
|
197
|
+
# => false
|
|
198
|
+
```
|
|
199
|
+
|
|
184
200
|
### Parse Retry-After
|
|
185
201
|
|
|
186
202
|
```ruby
|
|
@@ -215,6 +231,7 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
215
231
|
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
216
232
|
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
217
233
|
| `HeaderKit.parse_retry_after(header)` | Parse Retry-After header (seconds or HTTP date) |
|
|
234
|
+
| `HeaderKit.etag_match?(header_value, resource_etag)` | Check If-None-Match / If-Match against a resource ETag (list, `W/` weak prefix, `*` wildcard) |
|
|
218
235
|
|
|
219
236
|
## Development
|
|
220
237
|
|
|
@@ -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'
|
|
@@ -176,6 +177,19 @@ module Philiprehberger
|
|
|
176
177
|
Forwarded.parse_via(header)
|
|
177
178
|
end
|
|
178
179
|
|
|
180
|
+
# Check whether an If-None-Match / If-Match header matches a resource ETag.
|
|
181
|
+
#
|
|
182
|
+
# Accepts a single ETag, comma-separated list, weak-prefixed values
|
|
183
|
+
# (e.g. `W/"abc"`), or the `*` wildcard. Weak and strong ETags collapse
|
|
184
|
+
# to equality on the inner token.
|
|
185
|
+
#
|
|
186
|
+
# @param header_value [String, nil] the raw header value
|
|
187
|
+
# @param resource_etag [String, nil] the resource ETag (unquoted, no `W/` prefix)
|
|
188
|
+
# @return [Boolean] true if any value in the header matches the resource
|
|
189
|
+
def self.etag_match?(header_value, resource_etag)
|
|
190
|
+
Etag.match?(header_value, resource_etag)
|
|
191
|
+
end
|
|
192
|
+
|
|
179
193
|
# Parse a Retry-After header.
|
|
180
194
|
#
|
|
181
195
|
# 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.5.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-18 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
|