philiprehberger-header_kit 0.6.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 +6 -0
- data/README.md +19 -0
- data/lib/philiprehberger/header_kit/range.rb +86 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +18 -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,12 @@ 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
|
+
|
|
10
16
|
## [0.6.0] - 2026-04-23
|
|
11
17
|
|
|
12
18
|
### Added
|
data/README.md
CHANGED
|
@@ -218,6 +218,23 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
218
218
|
# => {date: 2026-04-04 12:00:00 UTC}
|
|
219
219
|
```
|
|
220
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
|
+
|
|
221
238
|
## API
|
|
222
239
|
|
|
223
240
|
| Method | Description |
|
|
@@ -243,6 +260,8 @@ Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
|
243
260
|
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
244
261
|
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
245
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 |
|
|
246
265
|
| `HeaderKit.etag_match?(header_value, resource_etag)` | Check If-None-Match / If-Match against a resource ETag (list, `W/` weak prefix, `*` wildcard) |
|
|
247
266
|
|
|
248
267
|
## Development
|
|
@@ -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
|
|
@@ -198,6 +199,23 @@ module Philiprehberger
|
|
|
198
199
|
Etag.match?(header_value, resource_etag)
|
|
199
200
|
end
|
|
200
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
|
+
|
|
201
219
|
# Parse a Retry-After header.
|
|
202
220
|
#
|
|
203
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
|