35185b1d

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.
- **`key=` is a decoder setting.** A form has no null, so whether an empty value
  is the empty string or is absence is a real choice, and it is
  `FormDecodeSettings`:

  ```haskell
  data EmptyValue = EmptyValueIsValue | EmptyValueIsAbsent
  ```

  The decoder consults it **at optional keys only**. A required key has no
  absence for an empty value to mean, so it decodes the empty string either way.
  The default is `EmptyValueIsValue`, which is lossless and agrees with
  `lookupMaybe`. `EmptyValueIsAbsent` is for the shell case, where
  `--data-urlencode "x=$UNSET"` sends `x=`.

  That required/optional distinction exists nowhere but inside the interpreter,
  which is why the setting lives there rather than in a pre-pass over the
  `Form` — a pre-pass would strip a required field's legitimate empty string
  along with the optional field's unset one.

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.

Beyond that mirror:

- Two assertions on a key that two fields share, which is the only shape that
  tells a values-concatenating union from a left-biased one. Without them,
  replacing `unionForm` with `<>` passed the entire suite.

- Four assertions on `formDecodeSettingEmptyValue`, using `ListsExample` because
  it has a required and an optional field in one type. The load-bearing one is
  that under `EmptyValueIsAbsent` a form with *both* fields present and empty
  still decodes the required field as `"" :| []` while the optional one becomes
  `Nothing`. I checked that this fails against a pre-pass implementation, which
  loses the required key with `Left "Expected a nonempty list, but got an empty
  list."`.
- 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,483 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), which can
now use `fromFormViaCodecWith` with `EmptyValueIsAbsent` instead of its
hand-written instance.

## Known limitations

Documented in the module header rather than worked around:

- A nested object or array is JSON-encoded into the text slot of its key. It
  round-trips here, but no other form parser will understand it.
- An optional field holding an empty list decodes as absent, not as the empty
  list: a form cannot carry a key with zero values, so `Just []` and `Nothing`
  encode identically. Same in `autodocodec-servant-multipart`. Use a required
  field or a non-empty list to tell them apart.
- Under `EmptyValueIsAbsent`, an optional field that is genuinely the empty
  string is inexpressible. There is a test stating that loss rather than
  skipping the case.

## Where the coverage actually is

Worth knowing when reading the spec: for the seven types that derive via
`Autodocodec`, "matches the encoding" and "matches the decoding" compare
`toForm`/`fromForm` against the very functions those instances delegate to, so
they are tautologies. I confirmed this by making the encoder mangle every
required key — `Example`'s "matches the encoding" failed, every other type's
comparisons passed, and every round trip failed.

The structure is inherited from `MultipartSpec` and kept for maintainability,
but it means the real coverage is the round trips plus `Example`'s hand-written
`ToForm`/`FromForm`. Those are therefore built without the interpreter's own
`singletonForm`/`unionForm` helpers, so the comparison is two independent
spellings rather than one shared implementation.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 0s 1s 1s 1s 0s
Eval 7s 13s 13s 21s 5s
Build 17s 0s 0s 17s 0s
Suite 0s 15s 20s 21s 5s

Timeline

0s10s20s