#6

feat: multipart/form-data file uploads

OpenCyrusNuevoDiawants to merge intofrom

Summary

Operations whose request body is multipart/form-data (file upload endpoints — audio transcription, image processing, document import) didn't work: providing any body flag threw "Body field flags are only supported for JSON request bodies", and providing none silently sent {} with a boundary-less Content-Type: multipart/form-data header.

Now body properties declared as type: string, format: binary become file flags that take a filesystem path. For a spec like:

requestBody:
  content:
    multipart/form-data:
      schema:
        type: object
        properties:
          file: { type: string, format: binary }
          model_id: { type: string }
        required: [file, model_id]

the generated command is:

mycli transcriptions create --file ./audio.mp3 --model_id scribe_v1
  • Paths are Ajv-validated as strings, checked for existence (File not found: ./audio.mp3 (--file)), then assembled into a FormData body via Bun.file() so the Request constructor derives the boundary
  • --curl emits a runnable command: -F 'file=@/path' for files, --form-string 'name=value' for scalars, with the Content-Type header suppressed so curl generates its own boundary
  • Help renders file flags as --file <path>
  • PreparedRequest gains a serializable bodyParts field (for files, the value is the source path)
  • A required multipart body with no flags errors instead of silently sending {}; nested fields under multipart error clearly; JSON stays preferred when an operation offers both content types
  • File uploads require Bun (Node throws a clear error)

Scope decisions: path strings only (no Blob-in-flags for the programmatic client), multipart only (no form-urlencoded), no arrays of files.

Also includes a prefactoring commit deleting the dead loadBody/BodyInput module and the unused bodyFlags field.

Test plan

  • bun test — 292 pass (13 new: FormData round-trip, curl shape, file-not-found, missing required field, empty-body error, Ajv scalar validation, nested-field rejection, content-type preference, execute round-trip, render output)
  • bun run typecheck clean; biome check src clean (biome ci fails only on generated dist/ output, pre-existing)
  • End-to-end: a compiled standalone binary from a spec with a multipart speech-to-text endpoint produces curl -sS -X POST -F 'file=@.../sample.mp3' --form-string 'model_id=scribe_v1' 'https://api.example.com/v1/speech-to-text'