Elixir
The Elixir SDK is an idiomatic Hex package emitted from the same GlottoIR as every other target.
It is functional and module-first: the public package facade builds the client with
Petstore.new/2, and each resource gets its own module whose functions take that struct as their
first argument — Petstore.Pets.list_pets(client). It is built on Req.
Quickstart
def deps do
[
{:petstore, "~> 1.0"}
]
end
client =
Petstore.new("https://api.petstore.example",
token: System.get_env("PETSTORE_TOKEN")
)
# calls return {:ok, _} / {:error, %Petstore.ApiError{}} tuples
{:ok, pet} = Petstore.Pets.create_pet(client, %{name: "Rex"})
# paginated functions return a lazy Stream
Petstore.Pets.list_pets(client)
|> Enum.each(&IO.puts(&1.name))
Typed models
Each GlottoIR model becomes a struct module with from_map/to_map; functions decode and return
the typed response and accept a typed request body. Discriminated unions resolve to the right
variant.
Typed errors
Functions return a %Glotto.ApiError{} struct carrying the parsed error body, with
Glotto.ApiError.kind/1 mapping the status to a documented atom so callers case on :not_found
rather than matching status codes. See Errors.
Pagination
Paginated list functions return a lazy Stream that walks every page as you enumerate, advancing
the cursor for you. Manual-page companions expose typed items, the full response, and explicit next-page navigation. See Pagination.
Retries & backoff
Transient failures (5xx, 429, transport errors) retry with exponential backoff and jitter,
configurable per client and per call. A trailing keyword list accepts request headers, timeout, retry count, idempotency and applicable extra query/body values; explicit values override configured method/resource defaults. Terminating the request-owning Task cancels its request and retry wait. See Retries & timeouts.
SSE streaming
Server-sent-event endpoints return a Stream of typed events decoded from the text/event-stream
framing. See Streaming and Authentication.
Unknown response fields
Your API can add a response field without it being a breaking change — but a generated struct has only the keys it declares, and its from_map/1 factory reads only those. The generated
models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable
through an accessor, and written back out when the model is re-serialized.
{:ok, pet} = Glotto.Pets.create_pet(client, body)
# A field your API started returning after this SDK was generated.
species = Glotto.Pet.extra_fields(pet)["species"]
# Re-encoding preserves it — a read-modify-write never silently drops it.
json = pet |> Glotto.Pet.to_map() |> Jason.encode!()
extra_fields/1 returns a plain map, so nested objects and lists survive intact, and retention is
recursive.
The struct key defaults to an empty map, so %Glotto.Pet{id: "p1"} still builds.
The retained fields are read-only by design. To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than writing to the retained bag.
Missing and null fields
By default an optional field that decodes from JSON null is nil, the same as an omitted key,
and to_map/1 drops it — so a read-modify-write sends a decoded null back as an absent key. Set
targets.elixir.model_presence: explicit to keep missing,
null and a value apart: a missing field holds :glotto_missing and nil means JSON null. Code
that treated nil as absence must change; the generated README shows the idioms.
File uploads
multipart/form-data operations build the multipart body from their fields for you; an
application/octet-stream operation takes a positional body (a binary) sent raw with the right
content-type.
Optional request bodies
When your spec marks a request body optional (required: false), it is the :body key of the
trailing options list every operation already takes, rather than a positional argument:
Glotto.Api.typed(client, body: %Glotto.Search{...}). Leave the key out to send no body
and no content-type, or pass a value to send it. A falsy value is still a value: false, 0 and
"" are sent as written, and so is nil as JSON null. (For an object-model or inline-map body
whose schema does not admit null, nil has no meaning of its own and reads as left out. A schema admits null when it, or the schema it names, is marked nullable or is a union with a null member.) An optional multipart/form-data body takes a keyword list of its parts,
body: [file: bytes, text: "..."]: the parts you pass are the parts sent, and passing none sends no
body. The same holds on paginated operations (which re-send the body unchanged on every later page) and on
streaming operations with a JSON body. A streaming operation whose body is octet-stream or multipart sends the body
or parts you pass exactly as the buffered call sends them; what leaving such an optional body out sends is not yet
specified (tracked in #5747). An operation whose config sets
auto_populate body_fields is the one exception
to “no body”: it sends just those constants (with their content-type) when you leave the body out.
A required body stays a positional argument.
Binary downloads
Declared binary responses return an owned Glotto.BinaryDownload with status, headers, media type, content length, filename and request ID. Consume its chunks/1 stream, call read_all/2 with a required max_bytes bound, or pipe/2 to a binary IO device or callback. Limits count delivered bytes, including decoded gzip bytes. Close an abandoned response explicitly; terminating its original request Task also closes it. The request timeout covers the first byte or EOF, and no retry occurs after delivery. See File transfers.