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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 255a0f1f4321e63ae82d6acaf028cddb9a041d4f1594b16dcbb8f2a51f54a286
4
- data.tar.gz: 1a0ea312d1937e9e17c7cabe31f7822345d0de02f4627d09c597c5a71a5b7715
3
+ metadata.gz: 8a240cb6bc501c7737d126492c7ef3d97eeb12dfde706a9223eb4e4021f0ded1
4
+ data.tar.gz: 1eda330a4c801573f6a96fa909562859e8636d5f015905a97cc298afad22cc8b
5
5
  SHA512:
6
- metadata.gz: ee48374d55f01e11ae5b499c31556037801082a6631ed60306d544109897715bf5510eff7403675e2e94795257903efe289469bd790181cc2c32fef70e65a975
7
- data.tar.gz: 40cf00b3c4d3d9f88d50ab21f08fc2dfb2d75b9ee13e879f985d59b28a14e81c600376d13d8f13f9bafb4edd3ff9350d6c4f6d5766abf20e9dd94bd2fd9e706c
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HeaderKit
5
- VERSION = '0.6.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  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.6.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-04-24 00:00:00.000000000 Z
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