Add Form.encode, form-escape, and Form.parse-pairs#8
Conversation
There was a problem hiding this comment.
Build & Tests
CI: pass (macOS + Ubuntu both green)
Local build: fails with pre-existing TM/tm_zone const-qualifier issue on ARM Linux (from the time dependency) — unrelated to this PR.
Lint: carp-fmt -c and angler both pass clean.
Findings
1. form-escape implementation is correct ✓
http.carp:587-592 — splits by space, URI-escapes each part (which encodes =, &, +, and all other reserved chars), then joins with +. This is the correct inverse of form-unescape (which splits by +, joins with space, then URI-unescapes). Verified against URI.escape in the uri module — it encodes everything except a-z A-Z 0-9 - _ . ~, so reserved form characters (= & +) are all percent-encoded.
Edge cases verified:
- Double spaces:
"a b"→ split["a","","b"]→ join"a++b"→ roundtrips correctly viaform-unescape - Empty string:
""→ split[""]→ escape[""]→ join""→ ok - Key/value with
=: encoded as%3dbyURI.escape→parsesplits correctly
2. encode accumulator pattern is correct ✓
http.carp:597-603 — kv-reduce with String.empty? check avoids leading &. Since Map iteration order is unspecified, multi-pair output order is non-deterministic, but this is fine for form encoding.
3. parse-pairs mirrors parse correctly ✓
http.carp:609-621 — same split-by &, index-of \=, form-unescape pattern as parse, but collects into (Array (Pair String String)) instead of Map. Preserves order and duplicates as documented.
4. Test coverage is solid
13 tests cover: single/multi-pair encoding, space→+, &→%26, literal +→%2b, space in keys, encode/parse roundtrip, parse-pairs count/correctness/order/duplicates/+ decoding/key-without-value/percent decoding. All encode tests use single-pair maps, avoiding Map-order flakiness.
5. Minor gap: no multi-pair roundtrip test (non-blocking)
All encode tests use single-pair maps. A multi-pair encode→parse roundtrip test would add confidence without Map-order flakiness (since Map equality is structural). Not blocking — single-pair roundtrip is sufficient to prove the escaping logic is correct, and the accumulator &-separator logic is straightforward.
6. Pre-existing test failure noted
The PR description correctly identifies the pre-existing form-data on empty body returns empty map failure as unrelated. Confirmed — it's a parse("") edge case that predates this PR.
Verdict: merge
CI green on both platforms, code correctly mirrors existing patterns, tests are thorough, lint clean. The implementation is a clean complement to the existing parse/form-unescape API.
hellerve
left a comment
There was a problem hiding this comment.
Rebase and adjust the comment style.
65462a6 to
3033bea
Compare
|
Addressed hellerve's review:
|
There was a problem hiding this comment.
Build & Tests
Build: pass (carp --check http.carp).
Tests: CI green on ubuntu + macos. Local ARM compilation hits a pre-existing clang crash unrelated to this PR.
Lint: carp-fmt -c and angler pass.
Prior feedback
hellerve requested rebase and comment style adjustment. Both are addressed:
- Rebase: clean — branch is exactly 1 commit on top of current
origin/master. No divergence. - Banner separators removed: zero decorative comment lines exist in
test/http.carp. No; ---banners, no section labels, no redundant inline comments restating test descriptions. - Test style matches the existing file: bare
assert-equal/assert-truecalls with short description strings, consistent indentation, no decorative comments between them.
Findings
Read full http.carp (new functions) and test/http.carp (new tests) on the checked-out branch.
-
form-escape(http.carp:587-592): splits on space, URI-escapes each segment, joins with+.URI.escapeencodes everything except[a-zA-Z0-9_.\-~], so=,&,+,%are all percent-encoded. Empty string input produces""(correct). Correct inverse ofform-unescape. -
Form.encode(http.carp:597-603):Map.kv-reducewith empty-string check avoids leading&. No trailing&. Empty map returns"". Correct. -
Form.parse-pairs(http.carp:609-621): mirrorsForm.parseexactly but collects into(Array (Pair String String)). Handles key-without-value (no=) by setting value to"". Usesform-unescapefor+-as-space and percent-decoding. Correct. -
Test coverage (13 tests): single/multi-pair encoding, space→
+,&→%26, literal+→%2b, space in key, encode/parse roundtrip, parse-pairs count/correctness/order/duplicates/+decoding/key-without-value/percent decoding. Multi-pair encode tests wisely use single-pair maps to avoid hash-map ordering flakiness. -
Minor gap (non-blocking): no test for
=in a value (would produce%3d).URI.escapehandles it correctly, just not explicitly tested. Also no empty-map encode test. Both low risk. -
Pre-existing issue (not from this PR):
Form.parse ""produces a map with one entry ("": ""), but the existing test at line 304 assertsMap.length == 0. This is a pre-existing bug in the test or implementation.
No bugs found in the new code.
Verdict: merge
Rebase clean, style feedback addressed, CI green, implementation correct, tests thorough.
Summary
The
Formmodule currently only hasparse(decode) but no way to produce URL-encoded form bodies. This adds the encoding half of the API:Form.encode— encodes a(Map String String)as a URL-encoded form body (key=val&key2=val2). Spaces become+, other special characters are percent-encoded. This is the inverse ofForm.parse.Form.parse-pairs— likeForm.parsebut returns(Array (Pair String String))instead of aMap, preserving insertion order and allowing duplicate keys.form-escape— private helper, the symmetric inverse of the existingform-unescape. Splits by space, URI-escapes each part, joins with+.Testing
13 new tests covering:
+encoding in both keys and values&and+percent-encodingencode/parseroundtripparse-pairsorder preservation, duplicate keys,+decoding, percent decoding, key-without-valueNote: the test suite has a pre-existing C compilation issue from the
timedependency (tm_zoneconst qualifier mismatch under-Werror). Tests were verified by relaxing that specific warning flag. The one pre-existing test failure (form-data on empty body returns empty map) is unrelated to this change.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.