philiprehberger-header_kit 0.5.0 → 0.7.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 +11 -0
- data/README.md +31 -0
- data/lib/philiprehberger/header_kit/accept_encoding.rb +31 -1
- data/lib/philiprehberger/header_kit/range.rb +86 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +26 -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: 8a240cb6bc501c7737d126492c7ef3d97eeb12dfde706a9223eb4e4021f0ded1
|
|
4
|
+
data.tar.gz: 1eda330a4c801573f6a96fa909562859e8636d5f015905a97cc298afad22cc8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa7df933af28801ee3819f973a69b8a4bc95fc6bb1129bc15821ca9454df21836e57874ecab24a7ff44eabd4618d3439dfb5e1fd9c8a83b1a459af80c0bfd0c8
|
|
7
|
+
data.tar.gz: 1583acd2a93ff399eaa4a559e86551b43788363305eb9f43066fe97eef353d012b575034416c2b09d5e950943e20d307d033c7595d9e7601868e43ff3c31ceb5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-05-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `HeaderKit.parse_range(header)` — parses an RFC 7233 Range header into `{ unit:, ranges: [{ first:, last: }, ...] }`. Supports byte ranges, suffix ranges (`-N`), and open-ended ranges (`N-`). Returns `nil` for invalid input.
|
|
14
|
+
- `HeaderKit.build_range(unit, ranges)` — builds a Range header value from an array of `{ first:, last: }` hashes, `[first, last]` arrays, or Ruby `Range` objects (inclusive/exclusive both supported)
|
|
15
|
+
|
|
16
|
+
## [0.6.0] - 2026-04-23
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `HeaderKit.build_accept_encoding(encodings)` for constructing Accept-Encoding headers from an array of `{encoding:, quality:}` hashes, mirroring the existing `build_accept` builder
|
|
20
|
+
|
|
10
21
|
## [0.5.0] - 2026-04-17
|
|
11
22
|
|
|
12
23
|
### 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
|
|
@@ -207,6 +218,23 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
207
218
|
# => {date: 2026-04-04 12:00:00 UTC}
|
|
208
219
|
```
|
|
209
220
|
|
|
221
|
+
### Parse and Build Range
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
Philiprehberger::HeaderKit.parse_range("bytes=0-499")
|
|
225
|
+
# => { unit: "bytes", ranges: [{ first: 0, last: 499 }] }
|
|
226
|
+
|
|
227
|
+
Philiprehberger::HeaderKit.parse_range("bytes=0-99, 200-, -50")
|
|
228
|
+
# => { unit: "bytes", ranges: [
|
|
229
|
+
# { first: 0, last: 99 },
|
|
230
|
+
# { first: 200, last: nil },
|
|
231
|
+
# { first: nil, last: 50 }
|
|
232
|
+
# ] }
|
|
233
|
+
|
|
234
|
+
Philiprehberger::HeaderKit.build_range("bytes", [(0..99), { first: 200, last: nil }])
|
|
235
|
+
# => "bytes=0-99, 200-"
|
|
236
|
+
```
|
|
237
|
+
|
|
210
238
|
## API
|
|
211
239
|
|
|
212
240
|
| Method | Description |
|
|
@@ -216,6 +244,7 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
216
244
|
| `HeaderKit.parse_accept_language(header)` | Parse Accept-Language into sorted entries |
|
|
217
245
|
| `HeaderKit.negotiate_language(header, available)` | Language negotiation, returns best match or nil |
|
|
218
246
|
| `HeaderKit.parse_accept_encoding(header)` | Parse Accept-Encoding into sorted entries |
|
|
247
|
+
| `HeaderKit.build_accept_encoding(encodings)` | Build Accept-Encoding header string from encoding array |
|
|
219
248
|
| `HeaderKit.parse_authorization(header)` | Parse Authorization header (Bearer, Basic, Digest) |
|
|
220
249
|
| `HeaderKit.parse_cache_control(header)` | Parse Cache-Control into directive hash |
|
|
221
250
|
| `HeaderKit.build_cache_control(directives)` | Build Cache-Control string from hash |
|
|
@@ -231,6 +260,8 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
231
260
|
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
232
261
|
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
233
262
|
| `HeaderKit.parse_retry_after(header)` | Parse Retry-After header (seconds or HTTP date) |
|
|
263
|
+
| `HeaderKit.parse_range(header)` | Parse Range header into `{ unit:, ranges: }` (returns nil for invalid input) |
|
|
264
|
+
| `HeaderKit.build_range(unit, ranges)` | Build a Range header from hashes, arrays, or Ruby Ranges |
|
|
234
265
|
| `HeaderKit.etag_match?(header_value, resource_etag)` | Check If-None-Match / If-Match against a resource ETag (list, `W/` weak prefix, `*` wildcard) |
|
|
235
266
|
|
|
236
267
|
## Development
|
|
@@ -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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
# Parses and builds the HTTP Range request header (RFC 7233 §3.1).
|
|
6
|
+
#
|
|
7
|
+
# Format: `<unit>=<start>-<end>[, <start>-<end>]*`
|
|
8
|
+
# Supports byte ranges, suffix ranges (`-N`), and open-ended ranges (`N-`).
|
|
9
|
+
module Range
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Parse a Range header value.
|
|
13
|
+
#
|
|
14
|
+
# @param header [String, nil] the raw header value
|
|
15
|
+
# @return [Hash{Symbol => Object}, nil] hash with :unit (String) and :ranges
|
|
16
|
+
# (Array of Hash with :first/:last keys, where either may be nil for
|
|
17
|
+
# open-ended), or nil for nil/blank/invalid input
|
|
18
|
+
# @example
|
|
19
|
+
# parse('bytes=0-499') # => { unit: 'bytes', ranges: [{ first: 0, last: 499 }] }
|
|
20
|
+
# parse('bytes=500-') # => { unit: 'bytes', ranges: [{ first: 500, last: nil }] }
|
|
21
|
+
# parse('bytes=-500') # => { unit: 'bytes', ranges: [{ first: nil, last: 500 }] }
|
|
22
|
+
# parse('bytes=0-99, 200-') # => { unit: 'bytes', ranges: [{ first: 0, last: 99 }, { first: 200, last: nil }] }
|
|
23
|
+
def parse(header)
|
|
24
|
+
return nil if header.nil?
|
|
25
|
+
|
|
26
|
+
header = header.to_s.strip
|
|
27
|
+
return nil if header.empty?
|
|
28
|
+
|
|
29
|
+
unit, _, spec = header.partition('=')
|
|
30
|
+
unit = unit.strip
|
|
31
|
+
spec = spec.strip
|
|
32
|
+
return nil if unit.empty? || spec.empty?
|
|
33
|
+
|
|
34
|
+
ranges = spec.split(',').map { |part| parse_range(part.strip) }
|
|
35
|
+
return nil if ranges.any?(&:nil?)
|
|
36
|
+
|
|
37
|
+
{ unit: unit, ranges: ranges }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build a Range header value.
|
|
41
|
+
#
|
|
42
|
+
# @param unit [String] the range unit (e.g. 'bytes')
|
|
43
|
+
# @param ranges [Array<Hash, Array, Range>] each as `{ first:, last: }`,
|
|
44
|
+
# a `[first, last]` array, or a Ruby `Range` (inclusive)
|
|
45
|
+
# @return [String] formatted Range header
|
|
46
|
+
# @example
|
|
47
|
+
# build('bytes', [{ first: 0, last: 499 }]) # => "bytes=0-499"
|
|
48
|
+
# build('bytes', [[0, 499], [500, nil]]) # => "bytes=0-499, 500-"
|
|
49
|
+
# build('bytes', [(0..99), { first: nil, last: 50 }]) # => "bytes=0-99, -50"
|
|
50
|
+
def build(unit, ranges)
|
|
51
|
+
list = ranges.is_a?(Array) ? ranges : [ranges]
|
|
52
|
+
formatted = list.map { |r| format_range(r) }
|
|
53
|
+
"#{unit}=#{formatted.join(', ')}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_range(raw)
|
|
57
|
+
return nil unless raw.match?(/\A-?\d*-\d*\z/)
|
|
58
|
+
return nil if raw == '-'
|
|
59
|
+
|
|
60
|
+
first_str, _, last_str = raw.partition('-')
|
|
61
|
+
first = first_str.empty? ? nil : Integer(first_str, 10)
|
|
62
|
+
last = last_str.empty? ? nil : Integer(last_str, 10)
|
|
63
|
+
return nil if first.nil? && last.nil?
|
|
64
|
+
return nil if first && last && first > last
|
|
65
|
+
|
|
66
|
+
{ first: first, last: last }
|
|
67
|
+
rescue ArgumentError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
private_class_method :parse_range
|
|
71
|
+
|
|
72
|
+
def format_range(spec)
|
|
73
|
+
first, last =
|
|
74
|
+
case spec
|
|
75
|
+
when ::Range then [spec.first, spec.exclude_end? ? spec.last - 1 : spec.last]
|
|
76
|
+
when Array then [spec[0], spec[1]]
|
|
77
|
+
when Hash then [spec[:first] || spec['first'], spec[:last] || spec['last']]
|
|
78
|
+
else raise ArgumentError, "Unsupported range entry: #{spec.inspect}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
"#{first}-#{last}"
|
|
82
|
+
end
|
|
83
|
+
private_class_method :format_range
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -15,6 +15,7 @@ require_relative 'header_kit/negotiation'
|
|
|
15
15
|
require_relative 'header_kit/cors'
|
|
16
16
|
require_relative 'header_kit/security'
|
|
17
17
|
require_relative 'header_kit/forwarded'
|
|
18
|
+
require_relative 'header_kit/range'
|
|
18
19
|
require_relative 'header_kit/retry_after'
|
|
19
20
|
|
|
20
21
|
module Philiprehberger
|
|
@@ -45,6 +46,14 @@ module Philiprehberger
|
|
|
45
46
|
AcceptEncoding.parse(header)
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
# Build an Accept-Encoding header string from an array of encoding hashes.
|
|
50
|
+
#
|
|
51
|
+
# @param encodings [Array<Hash>] each with :encoding and optional :quality
|
|
52
|
+
# @return [String] formatted Accept-Encoding header value
|
|
53
|
+
def self.build_accept_encoding(encodings)
|
|
54
|
+
AcceptEncoding.build(encodings)
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
# Parse an Accept-Language header into structured entries sorted by quality.
|
|
49
58
|
#
|
|
50
59
|
# @param header [String] the Accept-Language header value
|
|
@@ -190,6 +199,23 @@ module Philiprehberger
|
|
|
190
199
|
Etag.match?(header_value, resource_etag)
|
|
191
200
|
end
|
|
192
201
|
|
|
202
|
+
# Parse a Range request header (RFC 7233 §3.1).
|
|
203
|
+
#
|
|
204
|
+
# @param header [String] the Range header value
|
|
205
|
+
# @return [Hash{Symbol => Object}, nil] hash with :unit and :ranges, or nil for invalid input
|
|
206
|
+
def self.parse_range(header)
|
|
207
|
+
Range.parse(header)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Build a Range request header value.
|
|
211
|
+
#
|
|
212
|
+
# @param unit [String] the range unit (e.g. 'bytes')
|
|
213
|
+
# @param ranges [Array<Hash, Array, ::Range>] each as `{ first:, last: }`, `[first, last]`, or `::Range`
|
|
214
|
+
# @return [String] formatted Range header
|
|
215
|
+
def self.build_range(unit, ranges)
|
|
216
|
+
Range.build(unit, ranges)
|
|
217
|
+
end
|
|
218
|
+
|
|
193
219
|
# Parse a Retry-After header.
|
|
194
220
|
#
|
|
195
221
|
# 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.7.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-
|
|
11
|
+
date: 2026-05-08 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.
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- lib/philiprehberger/header_kit/forwarded.rb
|
|
37
37
|
- lib/philiprehberger/header_kit/link.rb
|
|
38
38
|
- lib/philiprehberger/header_kit/negotiation.rb
|
|
39
|
+
- lib/philiprehberger/header_kit/range.rb
|
|
39
40
|
- lib/philiprehberger/header_kit/retry_after.rb
|
|
40
41
|
- lib/philiprehberger/header_kit/security.rb
|
|
41
42
|
- lib/philiprehberger/header_kit/version.rb
|