Skip to content

Add Form.encode, form-escape, and Form.parse-pairs#8

Merged
hellerve merged 1 commit into
masterfrom
claude/form-encode
Jun 26, 2026
Merged

Add Form.encode, form-escape, and Form.parse-pairs#8
hellerve merged 1 commit into
masterfrom
claude/form-encode

Conversation

@carpentry-agent

Copy link
Copy Markdown

Summary

The Form module currently only has parse (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 of Form.parse.
  • Form.parse-pairs — like Form.parse but returns (Array (Pair String String)) instead of a Map, preserving insertion order and allowing duplicate keys.
  • form-escape — private helper, the symmetric inverse of the existing form-unescape. Splits by space, URI-escapes each part, joins with +.

Testing

13 new tests covering:

  • Single and multi-pair encoding
  • Space → + encoding in both keys and values
  • & and + percent-encoding
  • encode/parse roundtrip
  • parse-pairs order preservation, duplicate keys, + decoding, percent decoding, key-without-value

Note: the test suite has a pre-existing C compilation issue from the time dependency (tm_zone const 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.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via form-unescape
  • Empty string: "" → split [""] → escape [""] → join "" → ok
  • Key/value with =: encoded as %3d by URI.escapeparse splits correctly

2. encode accumulator pattern is correct ✓

http.carp:597-603kv-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 hellerve left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase and adjust the comment style.

@hellerve hellerve marked this pull request as ready for review June 20, 2026 19:26
@carpentry-agent carpentry-agent Bot force-pushed the claude/form-encode branch from 65462a6 to 3033bea Compare June 26, 2026 05:11
@carpentry-agent

Copy link
Copy Markdown
Author

Addressed hellerve's review:

  • Rebased onto current master (was behind by two commits: a75885b and 9a5b282). Clean rebase, no conflicts.
  • Removed banner separators (; ---...) that were added to the test file — these conflict with the project's decluttered comment style established in a75885b.
  • Removed redundant inline comments (; roundtrip: ..., ; preserves duplicate keys, etc.) that just restated the test description strings.
  • Doc comments in http.carp (encode, parse-pairs, form-escape) already match the existing style — no changes needed there.

carp-fmt -c and angler both pass clean.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Rebase: clean — branch is exactly 1 commit on top of current origin/master. No divergence.
  2. Banner separators removed: zero decorative comment lines exist in test/http.carp. No ; --- banners, no section labels, no redundant inline comments restating test descriptions.
  3. Test style matches the existing file: bare assert-equal/assert-true calls 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.

  1. form-escape (http.carp:587-592): splits on space, URI-escapes each segment, joins with +. URI.escape encodes everything except [a-zA-Z0-9_.\-~], so =, &, +, % are all percent-encoded. Empty string input produces "" (correct). Correct inverse of form-unescape.

  2. Form.encode (http.carp:597-603): Map.kv-reduce with empty-string check avoids leading &. No trailing &. Empty map returns "". Correct.

  3. Form.parse-pairs (http.carp:609-621): mirrors Form.parse exactly but collects into (Array (Pair String String)). Handles key-without-value (no =) by setting value to "". Uses form-unescape for +-as-space and percent-decoding. Correct.

  4. 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.

  5. Minor gap (non-blocking): no test for = in a value (would produce %3d). URI.escape handles it correctly, just not explicitly tested. Also no empty-map encode test. Both low risk.

  6. Pre-existing issue (not from this PR): Form.parse "" produces a map with one entry ("": ""), but the existing test at line 304 asserts Map.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.

@hellerve hellerve merged commit c77d972 into master Jun 26, 2026
2 checks passed
@hellerve hellerve deleted the claude/form-encode branch June 26, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant