feat: support JSON-native union inputs (#3048)
* feat: support JSON-native union inputs
Add support for union input types such as `str | float` and
`str | float | None`. Unions are restricted to JSON-native members
(str, int, float, bool, dict/Any, list[T], None) so request
validation happens at the HTTP edge against the OpenAPI schema.
Unions involving Path, File, Secret, custom coders, and BaseModel
are rejected at build/schema-generation time, and output unions
remain unsupported.
- pkg/schema: add a recursive InputType model and resolver, emit
OpenAPI anyOf for union inputs, and keep multi-variant nullable
unions required when no default is supplied
- pkg/predict: parse numeric CLI values for unions that accept a
number (schemaAcceptsNumber), and fall back to a string member
when a numeric parse fails for number-first unions like
`float | str` (schemaAcceptsString)
- python/cog/_adt: add deterministic union normalisation with strict
per-member compatibility (bool not int/float, scalars not dict/Any,
list unions validate elements) and anyOf json_type emission
- tests: Go unit tests, Python regression tests, and end-to-end
txtar integration tests for HTTP, CLI, and list unions
- docs: document union inputs and nullable semantics
* fix: correct CLI numeric parsing for integer-only and float unions
Address two related bugs in CLI `-i` parsing of union inputs:
- `int | float` resolves to the integer member first, so a fractional
value like `1.5` failed ParseInt and errored instead of falling back
to the float member.
- `str | int` resolves to the string member, then the schemaAcceptsNumber
branch parsed `1.5` as a float even though the union only accepts an
integer, sending an invalid float.
Add a schemaAcceptsFloat helper that matches number members but not
integer-only members, and gate float parsing behind it in both the
integer branch (with a float fallback) and the schemaAcceptsNumber
branch. Add unit tests for `int | float` and `str | int` unions.
* docs: clarify optional vs multi-variant union required behaviour
The previous table conflated plain single-type optionals with
multi-variant nullable unions. A plain Optional[T] / T | None is never
placed in required, while a multi-variant union like A | B | None stays
required unless a default is supplied. Split these into separate rows
and add a runtime caveat: an optional needs a Python-level default
(via = Input(...) or default=None) so an omitted value resolves to
None; a bare Optional[T] annotation raises TypeError when omitted.