Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ jobs:
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
docs:
name: Documentation
runs-on: ubuntu-latest
permissions:
contents: write
statuses: write
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-docdeploy@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
corpus:
name: OpenAPI corpus
runs-on: ubuntu-latest
Expand Down
472 changes: 27 additions & 445 deletions README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
Manifest.toml
src/migration.md
8 changes: 8 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d"

[compat]
Documenter = "1.3"
47 changes: 47 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Build locally with:
# julia --project=docs -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate()'
# julia --project=docs docs/make.jl

using Documenter
using OpenAPI

# MIGRATION.md at the repository root is the single source for migration
# guidance; mirror it into the built site so the two cannot drift.
cp(
normpath(@__DIR__, "..", "MIGRATION.md"),
joinpath(@__DIR__, "src", "migration.md");
force = true,
)

makedocs(
sitename = "OpenAPI.jl",
modules = [OpenAPI],
checkdocs = :public,
format = Documenter.HTML(
prettyurls = get(ENV, "CI", nothing) == "true",
canonical = "https://juliacomputing.github.io/OpenAPI.jl",
edit_link = "main",
),
pages = [
"Home" => "index.md",
"Migrating from 0.2.x" => "migration.md",
"Manual" => [
"Generating clients" => "clients.md",
"Streaming and codecs" => "streaming.md",
"Security" => "security.md",
"Generating servers" => "servers.md",
"Documents from Julia code" => "documents.md",
"Pipeline and diagnostics" => "pipeline.md",
"Generated models" => "models.md",
"Generated modules and the runtime contract" => "artifacts.md",
"Support boundary" => "boundary.md",
],
"API reference" => "reference.md",
],
)

deploydocs(
repo = "github.com/JuliaComputing/OpenAPI.jl.git",
devbranch = "main",
push_preview = true,
)
43 changes: 43 additions & 0 deletions docs/src/artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated modules and the runtime contract

A generated module targets an OpenAPI.jl generated-code contract version. It
also records the exact OpenAPI.jl version that produced it. The module imports
internal `OpenAPI.Runtime` machinery and bakes runtime data shapes — operation
tables, `Runtime.Spec` keywords, schema descriptors, and dialect references —
directly into its source. The result is one generated source artifact, but it
still needs a compatible OpenAPI.jl runtime. Treat it as a build product, not
as version-independent user code.

Every generated module therefore records and checks its provenance:

- the first line stamps the OpenAPI.jl version that produced the file, and
- before it imports private runtime names, the module calls
[`OpenAPI.Runtime.require_contract`](@ref)`(N, version)` at load time, where
`N` is the generated-code contract version
([`OpenAPI.Runtime.CONTRACT_VERSION`](@ref)) current at generation time.

A release that changes any part of the generated-code contract bumps
`CONTRACT_VERSION`, so a previously generated module fails at load time with
an error naming the release that generated it and asking for regeneration —
instead of failing mysteriously, or worse silently, inside the runtime.
Releases with the same contract version remain load-compatible, so compatible
runtime fixes do not require regeneration.

## When to regenerate

Regenerate when the guard reports a contract mismatch, or when you want a fix
that changes generated source. Rerun [`OpenAPI.client`](@ref) or
[`OpenAPI.server`](@ref) against your document and commit the new file.

Because generation is deterministic, regenerating from an unchanged document
with the same OpenAPI.jl version reproduces the same file, so a generated
module diffs cleanly in version control.

## Large documents

Large descriptions produce large generated modules because the client keeps
the schema data needed for runtime validation. Generation is practical even
for the biggest public API descriptions (the package's corpus tests pin
Stripe and GitHub), but loading such a client can take tens of seconds.
Applications should generate and precompile these clients during a build step,
not at service startup.
55 changes: 55 additions & 0 deletions docs/src/boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Support boundary

The loader and normalizer preserve more OpenAPI information than an outgoing
client needs. The following boundaries are intentional and explicit:

| Feature | Status |
| --- | --- |
| OAS 3.0.x, 3.1.x, and 3.2.x document loading | Supported |
| JSON and YAML, with duplicate-key rejection | Supported |
| Local, same-origin, opt-in remote, anchor, and recursive references | Supported |
| Standard operations, OAS 3.2 `QUERY`, and `additionalOperations` | Supported |
| Callback and webhook operations | Normalized and validated; no outgoing client functions are emitted |
| Link Objects | Preserved; no automatic follow-up operation is emitted |
| XML Object mapping | Preserved as schema metadata; use a custom media codec |
| Swagger/OAS 2.0 input | **Not supported. Convert to OpenAPI 3.x first (see [the migration guide](migration.md)).** |
| OAS 3.2 `querystring` parameters | **Deferred. Client planning fails with `unsupported_querystring_generation`.** |
| OAS 3.2 streaming `itemSchema`, `itemEncoding`, and `prefixEncoding` | **Deferred. Client planning fails with `unsupported_streaming_generation`.** |

The two deferred features fail during planning. They never produce a client
that silently sends the wrong wire format. Runtime response streaming with
`stream_to` is independent of the deferred OAS 3.2 `itemSchema` generation: it
streams response bodies that are described by normal schemas.

## Strict and permissive mode

`strict=true` is the default. Use `strict=false` only for documented ecosystem
compatibility cases. Permissive mode can retain ambiguous path templates, a
non-object `deepObject` parameter, and operation security naming schemes the
document never declares (see [Security](security.md)), each with warnings. For
OAS 3.0 documents, it also supports the common non-standard `nullable: true`
plus `$ref` or `allOf` idiom. Strict mode follows the normative rule that
`nullable` only takes effect when the same Schema Object defines `type`.
Permissive mode does not suppress unsafe or unsupported behavior.

## Validation evidence

The test suite includes structural schemas published by the OpenAPI Initiative,
adversarial JSON and YAML parsing, external and cyclic references, OAS 3.0/3.1/
3.2 semantics, JSON Schema edge cases, all parameter locations and styles,
security alternatives, server selection, media negotiation, nested multipart
encoding, error responses, and a live local HTTP integration server.

An optional pinned corpus test generates and compiles clients from public
Petstore, Discord, Stripe, and GitHub descriptions. Run it with:

```sh
OPENAPI_CORPUS_TESTS=small julia --project=. -e 'using Pkg; Pkg.test()'
OPENAPI_CORPUS_TESTS=all julia --project=. -e 'using Pkg; Pkg.test()'
OPENAPI_CORPUS_TESTS=all OPENAPI_CORPUS_CASE=GitHub julia --project=. -e 'using Pkg; Pkg.test()'
```

The large Stripe and GitHub descriptions require permissive mode for known
description-level compatibility warnings. Corpus success proves that a client
is generated and compiled. It does not prove that every operation was exercised
against each live service.
117 changes: 117 additions & 0 deletions docs/src/clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Generating clients

[`OpenAPI.client`](@ref) reads an OpenAPI 3.0, 3.1, or 3.2 document and emits
one deterministic Julia module. Load `HTTP` before reading a URL; local files
and inline JSON or YAML do not need `HTTP` during generation.

```julia
using OpenAPI, HTTP

OpenAPI.client(
"https://example.com/openapi.yaml";
name = "ExampleClient",
path = "ExampleClient.jl",
)
```

The long form runs the same pipeline in stages, which lets an application
inspect or cache the intermediate values (see
[Pipeline and diagnostics](pipeline.md)):

```julia
source = OpenAPI.load("https://example.com/openapi.yaml")
api = OpenAPI.normalize(source)
plan = OpenAPI.plan(api; name = "ExampleClient")
OpenAPI.client(plan; path = "ExampleClient.jl")
```

The generated file imports `OpenAPI`, `HTTP`, and `JSON`. It also imports the
Julia standard libraries `Base64`, `Dates`, and `UUIDs`. Add the three package
dependencies to the environment that will include the generated file.

## Calling operations

```julia
include("ExampleClient.jl")

client = ExampleClient.Client(
"https://api.example.com";
headers = ["User-Agent" => "my-app/1.0"],
)

# Each operationId becomes a Julia function. Path parameters are positional.
# Other parameters are keywords. A required request body is the last positional
# argument. Pass `client=client` to avoid shared global configuration.
result = ExampleClient.get_widget("widget-123"; verbose = true, client)
```

Optional model fields use `ExampleClient.Absent`, not `nothing`. This keeps a
missing value distinct from an explicit JSON `null`.

```julia
model = ExampleClient.WidgetInput(
name = "example",
description = ExampleClient.ABSENT,
)
```

## Responses and errors

Pass `with_http_info=true` to receive an `ApiResponse` with the status, raw
headers, decoded documented headers, and typed body. A non-2xx response throws
`ApiError`. The error keeps the raw body even when documented error decoding
fails.

Responses are decoded by status alone when a server omits its Content-Type
header, or misreports it while only one media type is documented for that
status; `UnexpectedContentType` is thrown only when several documented media
types make the choice ambiguous. A `2XX` status the document does not describe
never fails the call: an empty body returns `nothing` and a payload returns
raw bytes. Undocumented error statuses still throw `ApiError`.

## Request options and content negotiation

Use `content_type=...` and `accept=...` on an operation when the document
offers more than one representation. Use `request_headers` for one call and
`Client(headers=...)` for all calls. `request_options` passes options to the
HTTP transport. Streaming calls default to HTTP/1.1 because consumer-driven
stream cancellation closes one request connection. Set `protocol=:auto` or
`:h2` in `request_options` when the caller accepts HTTP/2 stream lifecycle
semantics. Buffered calls keep HTTP.jl's automatic protocol selection.

## HTTP behavior

Generated clients support:

- path, query, header, and cookie parameters;
- `simple`, `label`, `matrix`, `form`, `spaceDelimited`, `pipeDelimited`, and
`deepObject` serialization where the specification permits each style;
- `allowReserved`, `allowEmptyValue`, explode defaults, and parameter `content`;
- JSON and structured-suffix JSON media types;
- text and binary bodies;
- `application/x-www-form-urlencoded` bodies;
- multipart bodies, per-property encodings, documented part headers, uploads,
and one required level of nested named OAS 3.2 encoding;
- JSON Lines, NDJSON, JSON text sequences, and GeoJSON text sequences when the
body is described by a normal schema;
- exact, wildcard, and structured-suffix media negotiation;
- exact response codes, `1XX` through `5XX` ranges, and `default` responses;
- documented response headers, including repeated headers and `Set-Cookie`;
- operation, path, and root servers, relative server URLs, named servers, and
validated server variables;
- request and response validation with input/output JSON Schema semantics.

## Date and time mapping

`format: date-time` maps to `Dates.DateTime` by default, decoding RFC 3339
offsets by normalizing to UTC. Generate with `datetime = :zoned` to map to
`TimeZones.ZonedDateTime` instead, preserving offsets end to end; the
generated module then depends on TimeZones.jl.

## Source privacy

Generated schema graphs use content-derived resource identifiers. Local paths,
source URL userinfo, and source URL query strings are not embedded in
generated files. A relative Server Object still depends on the public scheme,
host, and path of the source URL because that location is part of the OpenAPI
resolution rule.
49 changes: 49 additions & 0 deletions docs/src/documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Documents from Julia code

The authoring API is intentionally smaller than the ingestion and client
pipeline. It maps common Julia endpoint declarations to an OpenAPI 3.2.0
document: describe each endpoint as an [`OpenAPI.Operation`](@ref) and pass
the collection to [`OpenAPI.document`](@ref).

```@example authoring
using OpenAPI, JSON

struct Widget
id::Int
tags::Vector{String}
end

operations = [
OpenAPI.Operation(
id = "get_widget",
method = :GET,
path = "/v1/widgets/{id}",
params = [
OpenAPI.Param("id", :path, Int),
OpenAPI.Param("verbose", :query, Bool; required = false),
],
responsetype = Widget,
),
]

document = OpenAPI.document(
operations;
title = "Widgets",
version = "1.0.0",
)

println(JSON.json(document; pretty = 2))
```

Named struct types encountered in parameter, body, and response types are
collected under `components/schemas` and referenced by `$ref`;
[`OpenAPI.schemaof`](@ref) documents the exact Julia-type-to-schema mapping.

The result is a plain JSON object, so the same document can be served by an
application, written to a file, or fed straight back into the generation
pipeline ([`OpenAPI.client`](@ref) accepts in-memory documents).

OpenAPI.jl does not depend on a server framework. Framework packages can add
optional [`OpenAPI.operations`](@ref) and [`OpenAPI.register!`](@ref) methods
to expose their routes as `Operation`s and serve the generated document.
Servo.jl provides its OpenAPI adapter from a downstream package extension.
Loading
Loading