Add a Cache-Control directive parser#15
Conversation
Parse Cache-Control header values (RFC 7234 §5.2) into a map of lower-cased directive names to values, with convenience accessors. The library already parses Accept, MediaType, multipart and Set-Cookie headers but could not read Cache-Control, so callers had to hand-parse it. CacheControl.parse splits on commas while respecting quoted-strings (so a quoted field-name list like no-cache="Set-Cookie, X" stays one directive), lower-cases directive names, unquotes values, and maps valueless directives to the empty string. Accessors: has?, directive, max-age, s-maxage, no-cache?, no-store?, must-revalidate?, public?, private?. Adds 11 tests.
There was a problem hiding this comment.
Build & Tests
Checked out claude/cache-control-parser and ran carp -x test/http.carp: 131 passed / 1 failed locally. The single failure is the pre-existing form-data on empty body returns empty map bug in the Form module (untouched by this PR, masked in CI via continue-on-error), exactly as the PR states. All 11 new CacheControl tests pass. CI green on ubuntu + macos.
Findings
Minor (non-blocking): a valueless or empty numeric directive reports Just 0 instead of Nothing. In int-directive (http.carp:1023), a present-but-valueless directive maps to "", and Int.from-string "" returns Just 0 (its C strtol wrapper treats the empty string as a fully-consumed parse). So:
(CacheControl.max-age &(CacheControl.parse "max-age"))→Just 0(CacheControl.max-age &(CacheControl.parse "max-age="))→Just 0
The docstring says "when present and numeric", so an empty value arguably should be Nothing. Impact is low — this only happens on malformed input (RFC 7234 requires max-age=<seconds>), and 0 reads as "must revalidate", a safe interpretation. If you want to tighten it, a one-liner in int-directive does it:
(Maybe.Just v) (if (String.empty? &v) (Maybe.Nothing) (Int.from-string &v))
Things I checked that are correct:
- Non-numeric (
max-age=abc) and trailing-junk (max-age=3600x) values correctly returnNothing—strtol's whole-string check catches these. - Quoted-comma handling works (
no-cache="Set-Cookie, X"stays one directive), including with an escaped inner quote, and=inside a quoted value (foo="a=b"→a=b). - Case-insensitivity, whitespace around
=, outer whitespace, empty/all-comma headers, and duplicate directives (last wins) all behave sensibly. - No out-of-bounds risk: every
String.char-atis under a< lenguard and everybyte-sliceuses bounded indices — no segfault surface.
Optional polish: the README "## Types" table and usage sections aren't updated, but the immediately-preceding sibling parser (Accept, #13) also isn't in the README, so this is consistent with recent practice — noting only for completeness, not as a gap.
Verdict: merge
Core parsing of well-formed Cache-Control headers is correct and thoroughly tested, and it fits the existing header-parser conventions. The Just 0-on-empty edge is a minor, safe quirk on malformed input worth a one-line guard if you care, but it doesn't block.
What
Adds a
CacheControlmodule that parsesCache-Controlheader values(RFC 7234 §5.2) into a
(Map String String)of lower-cased directive names tovalues, plus convenience accessors:
CacheControl.parse s— splits on commas while respecting quoted-strings(so a quoted field-name list like
no-cache="Set-Cookie, X"stays a singledirective), lower-cases directive names, strips surrounding quotes from
values, and maps valueless directives (e.g.
no-cache) to"". An emptyheader yields an empty map.
has?,directive(→Maybe String)max-age,s-maxage(→Maybe Int)no-cache?,no-store?,must-revalidate?,public?,private?Why
The library already parses
Accept,MediaType,multipart/form-data, andSet-Cookie, but had no way to readCache-Control— a core header for HTTPcaching semantics — so callers had to hand-parse it. This is the natural
additive companion to the existing
Accept/MediaTypeparsers, and reusestheir conventions (comma/
;-splitting that honours quotes,ascii-to-lowerkeys, quoted-value stripping).
Tests
11 new assertions:
max-age/s-maxageasInt, valueless-directivedetection, case-insensitive directive names,
max-age=0,max-ageabsent →Nothing, unquoting, and the quoted-comma case (asserting it stays onedirective). The parser mirrors
Accept.parse's empty-segment skipping, so itis unaffected by the pre-existing
Form.parse ""test failure. Suite: 131passed / 1 failed locally (the 1 failure is that unrelated pre-existing
form-data on empty bodybug).carp-fmt --checkandanglerclean onhttp.carpandtest/http.carp.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.