Add autodocodec-http-api-data for form-urlencoded data Derives `Web.FormUrlEncoded.ToForm` and `FromForm` from a `HasObjectCodec` instance, so a type with a codec can be posted as `application/x-www-form-urlencoded` without a second, hand-written description of its fields. ## Why The motivating case is an API endpoint that a shell should be able to hit with curl alone: ``` curl --netrc --silent --show-error --fail-with-body \ --data-urlencode "clone-url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" \ --data-urlencode "message=$COMMIT_MESSAGE" \ https://api.nix-ci.com/suite ``` The point of the form is that `--data-urlencode` escapes each value, so a commit message containing a quote or a newline needs no JSON encoder in the shell. The type already had a `HasObjectCodec` instance naming all its fields, but with no autodocodec support the `FromForm` instance had to be written by hand, so the field names were spelled twice and a test had to assert the two lists agreed. That test is exactly the check this package makes unnecessary. `http-api-data`'s own generic derivation is not a substitute: it derives names from Haskell record fields via a label modifier, so it would be a third independent spelling of the same names rather than a shared one. ## What `autodocodec-servant-multipart`'s interpreter retargeted at `Web.FormUrlEncoded.Form`, which is likewise a flat map from text keys to text values. A `Form`'s `HashMap Text [Text]` models repeated keys natively, so `ArrayOfCodec` fits it better than it fits multipart's flat `[Input]`. Three places where copying multipart verbatim would have been wrong: - **`unionForm`, not `<>`.** `Form` newtype-derives `Semigroup` from `HashMap`, whose union is left-biased, so `<>` would silently drop the second form's values under a shared key — where multipart's `mappendMultipartData` concatenates. - **Booleans encode lower case.** Multipart's encoder emits Haskell spelling; `toUrlPiece @Bool` is `T.toLower . show`, so `"True"` would round-trip through our own decoder but read as unconventional to every other form consumer. The decoder still accepts either spelling. - **A field is absent exactly when its key is absent**, which is lossless and matches `http-api-data`'s own `lookupMaybe`. `dropEmptyFormValues :: Form -> Form` is exported separately for callers who need a shell's unset variable (`--data-urlencode "x=$UNSET"` sends `x=`) to read as absence, at the cost of making a genuinely empty value inexpressible. That is a caller's choice rather than a policy threaded through the interpreter. Nested values keep multipart's behaviour — JSON-encoded into the text slot — documented in the module header as something no other form parser will understand. ## Tests `autodocodec-api-usage/test/Autodocodec/FormUrlEncodedSpec.hs` mirrors `MultipartSpec`: "matches the encoding", "matches the decoding" and a round trip, over `Example`, `Via`, `LegacyValue`, `LegacyObject`, `These`, `Expression`, `ListsExample` and `Overlap`. Hand-written `ToForm`/`FromForm Example` in `Usage.hs` give the first two something independent to compare against, as the multipart pair do. Two additions beyond that mirror: - `dropEmptyFormValues` asserted on both sides of the policy: an empty value stays a value without it, and reads as an absent field with it. - A round trip through `urlEncodeFormStable`/`urlDecodeForm`, so the actual percent-escaped wire bytes are covered. That escaping is the reason the feature exists. **No `xdescribe` was needed.** `MultipartSpec` skips `Example` with "does not hold."; here the whole corpus round-trips, `Example` included. I ran 480,000 examples per property rather than trusting the default 100 before concluding that. Grouping repeated keys is what makes the difference. ## Checks - `nix flake check` passes. The new package builds under `-Werror` against nixpkgs 26.05, 25.11 and 25.05 plus horizon-advance, so against four `http-api-data` versions. - Full `autodocodec-api-usage` suite: 77,482 examples, 0 failures. - `pre-commit run -a` clean. `stack build --pedantic` cannot pass in this tree, and not because of this change: `autodocodec-swagger2` has an unused `aeson` dependency that `-Wunused-packages` rejects under `-Werror`. I confirmed that on a clean tree before working around it. It does not affect `nix flake check`, whose override does not enable that warning, but it does mean the cheapest feedback loop is unusable until a separate one-line fix to `autodocodec-swagger2/package.yaml`. ## Not in this PR `cabal.project` needs no change (it globs `*/*.cabal`); `stack.yaml` does, since it lists packages explicitly. Replacing the first hand-written caller lives in the nix-ci tree, on branch `openapi-spec` (NorfairKing/nix-ci#463).