From b794f99a316cb127189f94697fba5aa39b13adc3 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 18:24:06 +0530 Subject: [PATCH 1/4] docs: cover the remaining public names with docstrings ClientPlan, ServerPlan, NormalizedAPI, DocumentVersion, location, oas_family, obj, and parse were the only public names without docstrings; the Documenter reference page checks public coverage. --- src/loading.jl | 23 +++++++++++++++++++++++ src/normalize.jl | 7 +++++++ src/planning.jl | 10 ++++++++++ src/read.jl | 6 ++++++ src/schemas.jl | 7 +++++++ 5 files changed, 53 insertions(+) diff --git a/src/loading.jl b/src/loading.jl index ea07c7f..9ff89b9 100644 --- a/src/loading.jl +++ b/src/loading.jl @@ -1,3 +1,11 @@ +""" + DocumentVersion(value::AbstractString) + +The parsed `openapi` version declaration of a source document. Accepts 3.0.x, +3.1.x, and 3.2.x values, with an optional prerelease suffix, and rejects +everything else. Carries `raw`, `major`, `minor`, `patch`, and `prerelease` +fields; [`OpenAPI.oas_family`](@ref) names the minor line it belongs to. +""" struct DocumentVersion raw::String major::Int @@ -22,6 +30,13 @@ function DocumentVersion(value::AbstractString) ) end +""" + oas_family(version::DocumentVersion) -> Symbol + +The OAS minor line a document belongs to: `:oas30`, `:oas31`, or `:oas32`. +Behavior that differs between specification lines — structural schema +selection, normalization rules — follows this family, never the patch version. +""" oas_family(version::DocumentVersion) = Symbol("oas3", version.minor) """An immutable, parsed OpenAPI source resource.""" @@ -41,6 +56,14 @@ Base.getindex(document::SourceDocument, key) = document.resource.contents[key] Base.haskey(document::SourceDocument, key) = haskey(document.resource.contents, key) Base.keys(document::SourceDocument) = keys(document.resource.contents) +""" + location(document::SourceDocument, pointer = Resources.JSONPointer()) -> SourceLocation + +The source location of the value at `pointer` inside a loaded document. When +no position was recorded for the exact node, the nearest recorded ancestor's +position is reported. Diagnostics use these locations to point back into the +original JSON or YAML text. +""" function location( document::SourceDocument, pointer::Resources.JSONPointer = Resources.JSONPointer(), diff --git a/src/normalize.jl b/src/normalize.jl index 3a17341..771a4e0 100644 --- a/src/normalize.jl +++ b/src/normalize.jl @@ -158,6 +158,13 @@ struct NormalizedOperation provenance::Provenance end +""" +The immutable, version-neutral result of [`OpenAPI.normalize`](@ref): resolved +references, a frozen resource registry, and normalized servers, security +schemes, schemas, and operations, together with the diagnostics produced while +normalizing. [`OpenAPI.plan`](@ref) and [`OpenAPI.serverplan`](@ref) consume +this value. +""" struct NormalizedAPI source::SourceDocument registry::Resources.FrozenRegistry diff --git a/src/planning.jl b/src/planning.jl index 2e3ff39..1f2c2e5 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -62,6 +62,11 @@ struct OperationPlan return_type::String end +""" +The result of [`OpenAPI.plan`](@ref): the deterministic Julia model and +operation plans for one generated client module, plus the diagnostics produced +while planning. Pass it to [`OpenAPI.client`](@ref) to emit source. +""" struct ClientPlan api::NormalizedAPI module_name::String @@ -74,6 +79,11 @@ struct ClientPlan datetime::Symbol end +""" +The result of [`OpenAPI.serverplan`](@ref): the deterministic Julia model and +operation plans for one generated server-stub module, plus the diagnostics +produced while planning. Pass it to [`OpenAPI.server`](@ref) to emit source. +""" struct ServerPlan api::NormalizedAPI module_name::String diff --git a/src/read.jl b/src/read.jl index a9d6077..82e9871 100644 --- a/src/read.jl +++ b/src/read.jl @@ -17,6 +17,12 @@ function read(source::AbstractString; kwargs...) end end +""" + OpenAPI.parse(source; options...) -> AbstractDict + +Behaves exactly like [`OpenAPI.read`](@ref); kept for callers that expect a +`parse` name in the namespace. +""" function parse(source::AbstractString; kwargs...) try return load(source; kwargs...).resource.contents diff --git a/src/schemas.jl b/src/schemas.jl index 4fda421..c2d42e6 100644 --- a/src/schemas.jl +++ b/src/schemas.jl @@ -2,6 +2,13 @@ # Named struct types are registered once under #/components/schemas and # referenced by $ref everywhere they appear. +""" + obj(pairs::Pair...) -> JSON.Object{String,Any} + +An ordered JSON object from key-value pairs; keys convert to `String`. A small +helper for assembling OpenAPI document fragments by hand alongside +[`OpenAPI.document`](@ref). +""" function obj(pairs::Pair...) o = JSON.Object{String,Any}() for (k, v) in pairs From 033772bd5b0aba37e4b22e70b7907b3ab45e4c53 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 18:32:01 +0530 Subject: [PATCH 2/4] docs: add the Documenter manual Home, migration (mirrored from MIGRATION.md at build time so the two cannot drift), nine manual pages reorganized from the README, and an API reference covering every public name (checkdocs = :public). The pipeline, server, and authoring pages run their examples at build time against inline documents, so the site fails to build if the documented behavior drifts from the code. --- docs/.gitignore | 3 + docs/Project.toml | 8 +++ docs/make.jl | 47 +++++++++++++++ docs/src/artifacts.md | 43 ++++++++++++++ docs/src/boundary.md | 55 +++++++++++++++++ docs/src/clients.md | 117 ++++++++++++++++++++++++++++++++++++ docs/src/documents.md | 49 +++++++++++++++ docs/src/index.md | 119 +++++++++++++++++++++++++++++++++++++ docs/src/models.md | 42 +++++++++++++ docs/src/pipeline.md | 134 +++++++++++++++++++++++++++++++++++++++++ docs/src/reference.md | 87 +++++++++++++++++++++++++++ docs/src/security.md | 48 +++++++++++++++ docs/src/servers.md | 135 ++++++++++++++++++++++++++++++++++++++++++ docs/src/streaming.md | 71 ++++++++++++++++++++++ 14 files changed, 958 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/artifacts.md create mode 100644 docs/src/boundary.md create mode 100644 docs/src/clients.md create mode 100644 docs/src/documents.md create mode 100644 docs/src/index.md create mode 100644 docs/src/models.md create mode 100644 docs/src/pipeline.md create mode 100644 docs/src/reference.md create mode 100644 docs/src/security.md create mode 100644 docs/src/servers.md create mode 100644 docs/src/streaming.md diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..cf5d073 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +build/ +Manifest.toml +src/migration.md diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..9ede498 --- /dev/null +++ b/docs/Project.toml @@ -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" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..3975806 --- /dev/null +++ b/docs/make.jl @@ -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, +) diff --git a/docs/src/artifacts.md b/docs/src/artifacts.md new file mode 100644 index 0000000..97dd24f --- /dev/null +++ b/docs/src/artifacts.md @@ -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. diff --git a/docs/src/boundary.md b/docs/src/boundary.md new file mode 100644 index 0000000..572787c --- /dev/null +++ b/docs/src/boundary.md @@ -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. diff --git a/docs/src/clients.md b/docs/src/clients.md new file mode 100644 index 0000000..586aad1 --- /dev/null +++ b/docs/src/clients.md @@ -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. diff --git a/docs/src/documents.md b/docs/src/documents.md new file mode 100644 index 0000000..d10bae7 --- /dev/null +++ b/docs/src/documents.md @@ -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. diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..5d634f7 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,119 @@ +# OpenAPI.jl + +OpenAPI.jl reads [OpenAPI](https://www.openapis.org/) descriptions and +generates single-file, typed Julia HTTP clients and server stubs. It also +provides a smaller API for creating an OpenAPI document from declared Julia +endpoints. + +Three pieces: + +1. **Client generation** — [`OpenAPI.client`](@ref) turns an OpenAPI 3.0, 3.1, + or 3.2 document (built in-process, read from JSON or YAML, or fetched from a + running app) into a deterministic single-file Julia client. Generated + modules use HTTP.jl for transport and JSON.jl plus OpenAPI's provisional + schema engine for typed, validated request and response handling. +2. **Server generation** — [`OpenAPI.server`](@ref) turns the same documents + into a deterministic single-file server-stub module: typed request decoding, + response validation and encoding, and a `register!(router, impl)` entry + point that mounts handler functions you implement onto a framework router. +3. **Document generation** — describe endpoints as [`OpenAPI.Operation`](@ref)s + and get a valid OpenAPI 3.2.0 document. Framework packages can add router + adapters through the [`OpenAPI.operations`](@ref) and + [`OpenAPI.register!`](@ref) extension seams. + +The pipeline supports OpenAPI 3.0.x, 3.1.x, and 3.2.x. It parses JSON and +YAML, resolves references, validates the document, normalizes version +differences, plans Julia types, and emits deterministic source code. +OpenAPI.jl supports Julia 1.10 LTS and later Julia 1.x releases. + +OpenAPI.jl does not export names. Use its API through the `OpenAPI` namespace. + +!!! note "Upgrading from 0.2.x" + OpenAPI.jl 0.2.x was the runtime library consumed by code that the Java + [openapi-generator](https://openapi-generator.tech/) `julia-client` / + `julia-server` targets produced. 1.0 replaces that model with the native + generator described here, and the 0.2.x runtime API is removed — a + breaking change. See [Migrating from OpenAPI.jl 0.2.x to 1.0](migration.md). + +## Installation + +```julia-repl +pkg> add OpenAPI +``` + +A generated module additionally imports `HTTP` and `JSON` (plus the standard +libraries `Base64`, `Dates`, and `UUIDs`). Add those two packages to the +environment that will include the generated file. + +## Generate a client + +```julia +using OpenAPI, HTTP + +OpenAPI.client( + "https://example.com/openapi.yaml"; + name = "ExampleClient", + path = "ExampleClient.jl", +) +``` + +```julia +include("ExampleClient.jl") + +client = ExampleClient.Client("https://api.example.com") + +# Each operationId becomes a Julia function. Path parameters are positional, +# other parameters are keywords. +result = ExampleClient.get_widget("widget-123"; verbose = true, client) +``` + +[Generating clients](clients.md) covers the full calling convention, error +handling, and content negotiation. + +## Generate a server + +```julia +using OpenAPI, HTTP + +OpenAPI.server( + "https://example.com/openapi.yaml"; + framework = :HTTP, + name = "ExampleServer", + path = "ExampleServer.jl", +) +``` + +The generated module header lists every handler signature to implement; +`ExampleServer.register!(router, Handlers)` mounts them on an `HTTP.Router`. +[Generating servers](servers.md) covers the handler contract, middleware, and +request decoding behavior. + +## How the documentation is organized + +- [Migrating from 0.2.x](migration.md) — what changed relative to the + openapi-generator lane, and how to move over. +- [Generating clients](clients.md), [Streaming and codecs](streaming.md), and + [Security](security.md) — generating a client and everything the generated + client can do. +- [Generating servers](servers.md) — server stubs, the handler contract, and + `register!`. +- [Documents from Julia code](documents.md) — the authoring API. +- [Pipeline and diagnostics](pipeline.md) — the staged public pipeline, + diagnostics, resource limits, and reference resolution. +- [Generated models](models.md) — how JSON Schemas become Julia types and what + validation guarantees hold. +- [Generated modules and the runtime contract](artifacts.md) — why generated + files are versioned build products and when to regenerate. +- [Support boundary](boundary.md) — what is supported, deferred, and rejected, + and what strict mode means. +- [API reference](reference.md) — docstrings for every public name. + +## Specification sources + +OpenAPI behavior follows the normative +[OpenAPI 3.0.4](https://spec.openapis.org/oas/v3.0.4.html), +[OpenAPI 3.1.1](https://spec.openapis.org/oas/v3.1.1.html), and +[OpenAPI 3.2.0](https://spec.openapis.org/oas/v3.2.0.html) specifications. +The files in the repository's `schemas/` directory are official structural +schemas published by the OpenAPI Initiative. The normative text remains +authoritative when a published schema differs from it. diff --git a/docs/src/models.md b/docs/src/models.md new file mode 100644 index 0000000..ce5a03d --- /dev/null +++ b/docs/src/models.md @@ -0,0 +1,42 @@ +# Generated models + +Generated structs are a typed view over the document's JSON Schemas. Runtime +schema validation remains authoritative. This design protects correctness when +a Julia field type cannot express every schema rule. + +Implemented model behavior includes: + +- objects, arrays, tuples, dictionaries, primitives, enums, and nullable types; +- required, optional, and explicit-null values; +- `allOf`, `oneOf`, `anyOf`, and discriminators; +- recursive models and recursive aliases; +- `additionalProperties`, `patternProperties`, `propertyNames`, and closed + objects; +- JSON Schema assertions such as `const`, `not`, conditions, dependent rules, + bounds, formats, and unevaluated constraints through runtime validation; +- `readOnly` and `writeOnly` request and response projections; +- Julia `Date`, `Time`, `DateTime`, `UUID`, and base64 byte values; +- `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); +- deterministic names with protection against Julia keywords, Base/Core names, + and generated runtime names. + +An unusual schema can plan to `Any` when no useful Julia type exists. It is +still validated at request and response boundaries. Custom JSON Schema dialects +and custom vocabularies can therefore retain correct validation while using a +less precise Julia type. + +## Disabling boundary validation + +Set `validate_requests=false` or `validate_responses=false` on a generated +`Client` only when the application accepts that loss of boundary validation. +For example, a response schema with `additionalProperties: false` rejects a new +server field. This is contract-correct but can make a client less tolerant of +an API that changes outside its published contract. With response validation +disabled, that policy also reaches nested generated models. Unknown response +properties are ignored, and an explicit null on an optional response property +decodes to `nothing` even when the document marks that property non-nullable. +Missing optional properties still decode to `ABSENT`. Values that cannot fit +the generated Julia type can still raise `DecodeError`. diff --git a/docs/src/pipeline.md b/docs/src/pipeline.md new file mode 100644 index 0000000..6413238 --- /dev/null +++ b/docs/src/pipeline.md @@ -0,0 +1,134 @@ +# Pipeline and diagnostics + +The public stages are separate so applications can inspect or cache them. + +- [`OpenAPI.load`](@ref) parses one root document and validates it against the + official schema for its OAS minor line. It returns an immutable + [`OpenAPI.SourceDocument`](@ref) with source identity, format, version, and + source locations. +- [`OpenAPI.check`](@ref) returns structured [`OpenAPI.Diagnostic`](@ref) + values instead of throwing for document validation errors. +- [`OpenAPI.normalize`](@ref) resolves references and creates an immutable, + version-neutral [`OpenAPI.NormalizedAPI`](@ref). +- [`OpenAPI.plan`](@ref) creates deterministic Julia model and operation plans + (a [`OpenAPI.ClientPlan`](@ref); [`OpenAPI.serverplan`](@ref) is the server + sibling). +- [`OpenAPI.client`](@ref) / [`OpenAPI.server`](@ref) emit source and + optionally write it to a file. + +Every stage accepts the output of any earlier stage — or the original source — +so the short form `OpenAPI.client("openapi.yaml"; ...)` runs the whole +pipeline. + +## The stages, end to end + +```@example pipeline +using OpenAPI + +document = """ +openapi: 3.1.0 +info: {title: Widgets, version: 1.0.0} +paths: + /widgets/{id}: + get: + operationId: getWidget + parameters: + - {name: id, in: path, required: true, schema: {type: integer, format: int64}} + responses: + "200": + description: one widget + content: + application/json: + schema: + \$ref: "#/components/schemas/Widget" +components: + schemas: + Widget: + type: object + required: [id, name] + properties: + id: {type: integer, format: int64} + name: {type: string} + tags: {type: array, items: {type: string}} +""" + +source = OpenAPI.load(document) +(source.version.raw, source.format) +``` + +```@example pipeline +api = OpenAPI.normalize(source) +(api.title, [operation.id for operation in api.operations]) +``` + +```@example pipeline +plan = OpenAPI.plan(api; name = "WidgetsClient") +[(model.name, model.kind) for model in plan.models] +``` + +```@example pipeline +source_code = OpenAPI.client(plan) +println(join(Iterators.take(eachsplit(source_code, '\n'), 4), '\n')) +println("⋮ (", count('\n', source_code), " lines)") +``` + +## Diagnostics + +Errors use stable diagnostic codes and resource plus JSON Pointer locations +([`OpenAPI.location`](@ref) recovers the position in the original text). JSON +and YAML mappings reject duplicate keys. Parsers reject alias cycles, +non-finite numbers, excessive nesting, and documents that exceed configured +limits. + +[`OpenAPI.check`](@ref) collects structural diagnostics without throwing: + +```@example pipeline +diagnostics = OpenAPI.check("openapi: 3.1.0\ninfo: {title: Broken, version: 1.0.0}") +foreach(println, diagnostics) +``` + +## Resource limits + +The main limits are: + +```julia +OpenAPI.normalize( + source; + base_uri = nothing, # identity for inline JSON or YAML + max_bytes = 16 * 1024 * 1024, + max_nodes = 1_000_000, + max_depth = 512, + max_resources = 256, + max_diagnostics = 1_000, +) +``` + +## References + +OpenAPI.jl resolves reusable OpenAPI objects and JSON Schema references. It +uses the isolated [`OpenAPI.SchemaEngine`](@ref) module for resource identity, +URI resolution, JSON Pointer, anchors, schema dialects, schema compilation, +and runtime validation. + +The schema engine is provisional. It is kept under `src/schema_engine` with no +OpenAPI-specific behavior so it can move to JSONSchema.jl after the API and +implementation have hardened against real OpenAPI documents. + +The default retriever has conservative access rules: + +- A local root can read relative files under the root file's directory. +- Extra local roots require `file_roots=[...]`. +- A URL root can read same-origin HTTP or HTTPS references. +- Cross-origin references require `allow_remote_refs=true`. +- HTTP redirects are not followed. +- Unsupported URI schemes are rejected. + +Pass an `OpenAPI.SchemaEngine.Resources.AbstractRetriever` with `retriever=...` +when an application needs another retrieval policy or an in-memory resource +store. Resource size and count limits still apply. + +Non-schema reference cycles are rejected. Recursive JSON Schemas are retained +and compiled normally. OpenAPI 3.0 Reference Object siblings are ignored. +OpenAPI 3.1 and 3.2 `summary` and `description` siblings are applied. Path Item +Reference Object siblings have undefined specification behavior. Strict mode +rejects them. Permissive mode warns and lets local fields override the target. diff --git a/docs/src/reference.md b/docs/src/reference.md new file mode 100644 index 0000000..af49311 --- /dev/null +++ b/docs/src/reference.md @@ -0,0 +1,87 @@ +# API reference + +OpenAPI.jl does not export names. Every public name below is used through the +`OpenAPI` namespace. Generated modules have their own surface (`Client`, +operation functions, model types, `register!`); that surface is documented by +the generated module itself and in the manual pages. + +```@docs +OpenAPI.OpenAPI +``` + +## Reading and validating documents + +```@docs +OpenAPI.load +OpenAPI.check +OpenAPI.read +OpenAPI.parse +OpenAPI.validate +OpenAPI.SourceDocument +OpenAPI.DocumentVersion +OpenAPI.oas_family +``` + +## Source locations and diagnostics + +```@docs +OpenAPI.location +OpenAPI.SourceLocation +OpenAPI.SourcePosition +OpenAPI.Diagnostic +OpenAPI.OpenAPIError +``` + +## Normalization + +```@docs +OpenAPI.normalize +OpenAPI.NormalizedAPI +``` + +## Planning and code generation + +```@docs +OpenAPI.plan +OpenAPI.ClientPlan +OpenAPI.client +OpenAPI.serverplan +OpenAPI.ServerPlan +OpenAPI.server +OpenAPI.server_source +OpenAPI.server_module_source +``` + +## Document authoring + +```@docs +OpenAPI.document +OpenAPI.Operation +OpenAPI.Param +OpenAPI.SchemaRegistry +OpenAPI.schemaof +OpenAPI.obj +``` + +## Extension seams + +```@docs +OpenAPI.register! +OpenAPI.operations +``` + +## Schema engine + +```@docs +OpenAPI.SchemaEngine +OpenAPI.Resources +``` + +## Generated-code contract + +```@docs +OpenAPI.Runtime +OpenAPI.Runtime.CONTRACT_VERSION +OpenAPI.Runtime.require_contract +OpenAPI.Runtime.Spec +``` diff --git a/docs/src/security.md b/docs/src/security.md new file mode 100644 index 0000000..77ee11c --- /dev/null +++ b/docs/src/security.md @@ -0,0 +1,48 @@ +# Security + +Generated clients implement OpenAPI security requirement alternatives and +combinations. Supported credentials include: + +- API keys in headers, query parameters, or cookies; +- HTTP Basic and Bearer authentication; +- other HTTP authentication values; +- OAuth 2.0 and OpenID Connect bearer tokens with documented scope checks; +- mutual TLS through HTTP request options. + +```julia +ExampleClient.credential!( + client, + "bearerAuth", + ExampleClient.BearerCredential("token"; scopes = ["widgets:read"]), +) +``` + +`authorization!(client, token)` is a convenience for every bearer-compatible +scheme in a document. The generated client does not acquire or refresh OAuth or +OpenID Connect tokens. The caller owns that lifecycle. + +By default, a secured operation fails before network access when no documented +credential alternative can be satisfied. Set `require_credentials=false` only +when an external HTTP layer supplies authentication. + +## Externally declared schemes + +A document loaded with `strict=false` may name security schemes in operation +`security` that its own `components` never declare — a common production shape +where authentication lives in a gateway and the schemes are declared by an +umbrella document. Strict mode rejects such documents (the Security Requirement +name rule is normative); permissive mode warns and treats the scheme as +externally declared. Generation then excludes it from operation security +descriptors — the runtime cannot construct credentials for a scheme it cannot +see — and the caller supplies gateway authentication explicitly, for example +through `request_headers`: + +```julia +ExampleClient.get_widget("widget-123"; + client, + request_headers = ["Authorization" => "Bearer $(gateway_token)"], +) +``` + +Generated **servers** never authenticate or authorize requests; see +[Generating servers](servers.md). diff --git a/docs/src/servers.md b/docs/src/servers.md new file mode 100644 index 0000000..4800597 --- /dev/null +++ b/docs/src/servers.md @@ -0,0 +1,135 @@ +# Generating servers + +The same document generates a server-stub module. The document stays the +source of truth: generate the client and the server from one specification and +implement one handler function per operation. + +```julia +using OpenAPI, HTTP + +OpenAPI.server( + "https://example.com/openapi.yaml"; + framework = :HTTP, + name = "ExampleServer", + path = "ExampleServer.jl", +) +``` + +`framework = :HTTP` (the default, available when HTTP.jl is loaded) targets +`HTTP.Router`. Server framework packages add their own emitters through the +[`OpenAPI.server_source`](@ref) extension seam. An extension must assemble its +generated module through [`OpenAPI.server_module_source`](@ref); this keeps the +runtime data, pasted server code, and generated-code contract guard together. +[`OpenAPI.serverplan`](@ref) is the staged sibling of [`OpenAPI.plan`](@ref) +and rejects documents whose requests cannot be decoded faithfully (for example +`multipart/mixed` request bodies, or two exploded object query parameters +whose wire names cannot be told apart). + +## The generated header + +The generated module header lists every handler signature the implementation +must define. For a small document: + +```@example servergen +using OpenAPI, HTTP + +document = """ +openapi: 3.1.0 +info: {title: Widgets, version: 1.0.0} +paths: + /widgets/{id}: + get: + operationId: getWidget + parameters: + - {name: id, in: path, required: true, schema: {type: integer, format: int64}} + - {name: verbose, in: query, schema: {type: boolean}} + responses: + "200": + description: one widget + content: + application/json: + schema: + type: object + required: [id, name] + properties: + id: {type: integer, format: int64} + name: {type: string} + delete: + operationId: deleteWidget + parameters: + - {name: id, in: path, required: true, schema: {type: integer, format: int64}} + responses: + "204": {description: deleted} +""" + +source_code = OpenAPI.server(document; framework = :HTTP, name = "WidgetsServer") +header = Iterators.takewhile(!startswith("module"), eachsplit(source_code, '\n')) +println(join(header, '\n')) +``` + +## Implementing handlers + +Handler functions receive the raw request first, then typed path parameters in +template order, then a required body; optional parameters arrive as keyword +arguments only when the request supplied them. + +```julia +include("ExampleServer.jl") + +module Handlers + +using HTTP + +# GET /widgets/{id} -> get_widget(request, id::Int64; verbose = ABSENT) +function get_widget(request, id; verbose = false) + return lookup_widget(id; verbose) # encoded, validated, 200 +end + +# This operation documents 204, so nothing becomes an empty 204 response. +delete_widget(request, id) = nothing + +# Return an HTTP.Response directly for custom behavior. +create_widget(request, body) = HTTP.Response(409, "already exists") + +end + +router = HTTP.Router() +ExampleServer.register!(router, Handlers; path_prefix = "/v1") +server = HTTP.serve!(router, "127.0.0.1", 8080) +``` + +`register!(router, impl; path_prefix, middleware)` mounts every documented +operation and fails eagerly, listing the expected signatures, when `impl` is +missing any handler. `middleware` wraps each operation handler +(`middleware(handler) -> handler`). `register` is kept as an alias, and the +handler contract — implementation module second, typed positional parameters, +typed-value-or-`HTTP.Response` returns — matches the shape OpenAPI.jl 0.2.x +users generated with `-g julia-server`. + +## Request decoding and response encoding + +Request decoding mirrors client encoding: parameter styles (`simple`, `label`, +`matrix`, `form`, `spaceDelimited`, `pipeDelimited`, `deepObject`), header and +cookie parameters, JSON, `application/x-www-form-urlencoded`, and +`multipart/form-data` request bodies, with request-direction schema validation +before handlers run. Decoding failures produce structured JSON `400` (or `415` +for undocumented media types) responses without invoking the handler. Response +values are validated against the output-direction schema and encoded from the +first documented success response. Returning `nothing` follows that response: +it emits an empty body when the response has no content, or JSON `null` when +the selected JSON schema accepts null. A full `HTTP.Response` bypasses +generated status, header, and body validation. The handler owns that +validation. + +An operation that documents no success response at all (only error entries) +produces a `missing_success_response` planning warning; its handler's +`nothing` return is answered with an empty `200`, which the OpenAPI +specification permits — response documentation is explicitly non-exhaustive. + +## Security and unsupported encodings + +Generated server stubs do not authenticate or authorize requests. Apply a +`middleware` that enforces the operation's security policy before it calls the +handler. `serverplan` rejects request-body Encoding Objects that the generated +HTTP server cannot recover faithfully. This includes nested encodings, custom +multipart part headers, and explicit body encoding style modifiers. diff --git a/docs/src/streaming.md b/docs/src/streaming.md new file mode 100644 index 0000000..09e3cb6 --- /dev/null +++ b/docs/src/streaming.md @@ -0,0 +1,71 @@ +# Streaming and codecs + +## Streaming responses + +Pass `stream_to = Channel(n)` to any operation to consume the response body +incrementally, e.g. long-running watch endpoints or large exports: + +```julia +events = Channel{Any}(16) +ExampleClient.watch_pods(; client, stream_to = events) # returns at the response head +for event in events + # each item is decoded to the documented response type +end +``` + +The call returns as soon as the response head arrives (the channel itself, or +an `ApiResponse` whose body is the channel with `with_http_info = true`), and a +background task decodes items onto the channel. `application/json` bodies split +into consecutive JSON documents, each decoded against the documented response +schema — the convention used by Kubernetes-style watch endpoints. JSON Lines +and NDJSON bodies decode each line to the documented array's element type, and +JSON text sequences split on RFC 7464 record separators. `text/*` yields lines +and any other media type yields raw byte chunks. The channel closes when the +response ends, closes with the error when decoding or validation fails, and +closing it from the consumer side aborts the transfer. Error statuses still +throw `ApiError` with the fully buffered error body. + +## Custom codecs + +For a custom media type, register an encoder or decoder on the client: + +```julia +ExampleClient.codec!( + client, + "application/cbor"; + encode = (value, media_type) -> encode_cbor(value), + decode = (bytes, media_type) -> decode_cbor(bytes), +) +``` + +XML metadata is retained in the schema but does not generate an XML codec. +Register a custom codec for XML or another non-built-in representation. + +## Codecs on streaming calls + +A registered response decoder also applies to streaming calls. The runtime +calls it once for each framed item and puts its return value directly on the +channel. This is an escape hatch for deployed APIs whose streaming wire format +does not match the response schema. Register the full parameterized media type +and select it with `accept` to limit the override to those calls: + +```julia +ExampleClient.codec!( + client, + "application/json;stream=watch"; + decode = (bytes, media_type) -> JSON.parse(String(bytes)), +) +events = Channel{Any}(16) +ExampleClient.watch_pods(; + client, + accept = "application/json;stream=watch", + stream_to = events, +) +``` + +This decoder does not replace the decoder for plain `application/json`. +Codecs are matched against the received Content-Type first, and streaming +calls fall back to the media type the call requested via `accept`: deployed +servers such as the Kubernetes apiserver reply with the bare +`application/json` even when the request selected the parameterized variant, +so passing `accept` is what scopes the override to the watch calls. From aa00f93f4ca7bdf58199eb75322cfb73a6837653 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 18:32:14 +0530 Subject: [PATCH 3/4] ci: build and deploy the documentation Same julia-docdeploy flow the 0.2 lane uses; deploydocs targets devbranch main, deploys versioned docs on tags, and pushes PR previews. --- .github/workflows/CI.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5bc40a2..019bde0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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 From 2e53528bbf2652949acd0a3602df7a4a7895da50 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 18:32:52 +0530 Subject: [PATCH 4/4] docs(readme): make the README a landing page for the manual The full content now lives in the Documenter manual, in one place; the README keeps the overview, quick starts, and migration pointer, and gains docs and CI badges. --- README.md | 472 ++++-------------------------------------------------- 1 file changed, 27 insertions(+), 445 deletions(-) diff --git a/README.md b/README.md index 3166f92..b5b1b92 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,29 @@ # OpenAPI.jl +[![Stable docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliacomputing.github.io/OpenAPI.jl/stable/) +[![Dev docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliacomputing.github.io/OpenAPI.jl/dev/) +[![CI](https://github.com/JuliaComputing/OpenAPI.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/JuliaComputing/OpenAPI.jl/actions/workflows/CI.yml) + OpenAPI.jl reads OpenAPI descriptions and generates single-file, typed Julia -HTTP clients. It also provides a smaller API for creating an OpenAPI document -from declared Julia endpoints. +HTTP clients and server stubs. It also provides a smaller API for creating an +OpenAPI document from declared Julia endpoints. -The client pipeline supports OpenAPI 3.0.x, 3.1.x, and 3.2.x. It parses JSON +The pipeline supports OpenAPI 3.0.x, 3.1.x, and 3.2.x. It parses JSON and YAML, resolves references, validates the document, normalizes version differences, plans Julia types, and emits deterministic source code. OpenAPI.jl supports Julia 1.10 LTS and later Julia 1.x releases. -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. - OpenAPI.jl does not export names. Use its API through the `OpenAPI` namespace. Upgrading from 0.2.x — the runtime library used by openapi-generator's `julia-client`/`julia-server` targets — is a breaking change; see -[MIGRATION.md](MIGRATION.md). +[MIGRATION.md](MIGRATION.md). 0.2.x maintenance continues on the +[`release-0.2`](https://github.com/JuliaComputing/OpenAPI.jl/tree/release-0.2) +branch. + +**[The documentation](https://juliacomputing.github.io/OpenAPI.jl/stable/)** +covers the full calling conventions, streaming, security, the staged pipeline, +generated model behavior, the generated-code contract, and the API reference. ## Generate a client @@ -29,17 +33,6 @@ need `HTTP` during generation. ```julia using OpenAPI, HTTP -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 short form runs the same pipeline: - -```julia -using OpenAPI, HTTP - OpenAPI.client( "https://example.com/openapi.yaml"; name = "ExampleClient", @@ -47,17 +40,13 @@ OpenAPI.client( ) ``` -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. +The generated file imports `OpenAPI`, `HTTP`, and `JSON`; add those three +dependencies to the environment that includes it. ```julia include("ExampleClient.jl") -client = ExampleClient.Client( - "https://api.example.com"; - headers = ["User-Agent" => "my-app/1.0"], -) +client = ExampleClient.Client("https://api.example.com") # Each operationId becomes a Julia function. Path parameters are positional. # Other parameters are keywords. A required request body is the last positional @@ -65,26 +54,11 @@ client = ExampleClient.Client( 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, -) -``` - -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. - ## Generate a server -The same document generates a server-stub module. The document stays the -source of truth: generate the client and the server from one specification and -implement one handler function per operation. +The same document generates a server-stub module: implement one handler +function per operation (the generated module header lists every expected +signature) and mount them on a router. ```julia using OpenAPI, HTTP @@ -97,40 +71,13 @@ OpenAPI.server( ) ``` -`framework = :HTTP` (the default, available when HTTP.jl is loaded) targets -`HTTP.Router`. Server framework packages add their own emitters through the -`OpenAPI.server_source` extension seam — loading Servo.jl enables -`framework = :Servo`. An extension must assemble its generated module through -`OpenAPI.server_module_source`; this keeps the runtime data, pasted server -code, and generated-code contract guard together. `OpenAPI.serverplan` is the -staged sibling of `OpenAPI.plan` and rejects documents whose requests cannot -be decoded -faithfully (for example `multipart/mixed` request bodies, or two exploded -object query parameters whose wire names cannot be told apart). - -The generated module header lists every handler signature the implementation -must define. Handler functions receive the raw request first, then typed path -parameters in template order, then a required body; optional parameters arrive -as keyword arguments only when the request supplied them. - ```julia include("ExampleServer.jl") module Handlers - using HTTP - -# GET /widgets/{id} -> get_widget(request, id::Int64; verbose = ABSENT) -function get_widget(request, id; verbose = false) - return lookup_widget(id; verbose) # encoded, validated, 200 -end - -# This operation documents 204, so nothing becomes an empty 204 response. -delete_widget(request, id) = nothing - -# Return an HTTP.Response directly for custom behavior. -create_widget(request, body) = HTTP.Response(409, "already exists") - +get_widget(request, id; verbose = false) = lookup_widget(id; verbose) +delete_widget(request, id) = nothing # documented 204 -> empty 204 end router = HTTP.Router() @@ -138,394 +85,29 @@ ExampleServer.register!(router, Handlers; path_prefix = "/v1") server = HTTP.serve!(router, "127.0.0.1", 8080) ``` -`register!(router, impl; path_prefix, middleware)` mounts every documented -operation and fails eagerly, listing the expected signatures, when `impl` is -missing any handler. `middleware` wraps each operation handler -(`middleware(handler) -> handler`). `register` is kept as an alias, and the -handler contract — implementation module second, typed positional parameters, -typed-value-or-`HTTP.Response` returns — matches the shape OpenAPI.jl 0.2.x -users generated with `-g julia-server`. - -Request decoding mirrors client encoding: parameter styles (`simple`, `label`, -`matrix`, `form`, `spaceDelimited`, `pipeDelimited`, `deepObject`), header and -cookie parameters, JSON, `application/x-www-form-urlencoded`, and -`multipart/form-data` request bodies, with request-direction schema validation -before handlers run. Decoding failures produce structured JSON `400` (or `415` -for undocumented media types) responses without invoking the handler. Response -values are validated against the output-direction schema and encoded from the -first documented success response. Returning `nothing` follows that response: -it emits an empty body when the response has no content, or JSON `null` when -the selected JSON schema accepts null. A full `HTTP.Response` bypasses generated -status, header, and body validation. The handler owns that validation. - -Generated server stubs do not authenticate or authorize requests. Apply a -`middleware` that enforces the operation's security policy before it calls the -handler. `serverplan` rejects request-body Encoding Objects that the generated -HTTP server cannot recover faithfully. This includes nested encodings, custom -multipart part headers, and explicit body encoding style modifiers. - -## Pipeline and diagnostics - -The public stages are separate so applications can inspect or cache them. - -- `OpenAPI.load(source)` parses one root document and validates it against the - official schema for its OAS minor line. It returns an immutable - `SourceDocument` with source identity, format, version, and source locations. -- `OpenAPI.check(source)` returns structured `Diagnostic` values instead of - throwing for document validation errors. -- `OpenAPI.normalize(source)` resolves references and creates an immutable, - version-neutral `NormalizedAPI`. -- `OpenAPI.plan(source; name="ApiClient")` creates deterministic Julia model - and operation plans. -- `OpenAPI.client(source; ...)` emits source and optionally writes it to a - file. - -Errors use stable diagnostic codes and resource plus JSON Pointer locations. -JSON and YAML mappings reject duplicate keys. Parsers reject alias cycles, -non-finite numbers, excessive nesting, and documents that exceed configured -limits. - -The main limits are: - -```julia -OpenAPI.normalize( - source; - base_uri = nothing, # identity for inline JSON or YAML - max_bytes = 16 * 1024 * 1024, - max_nodes = 1_000_000, - max_depth = 512, - max_resources = 256, - max_diagnostics = 1_000, -) -``` - -## References - -OpenAPI.jl resolves reusable OpenAPI objects and JSON Schema references. It -uses the isolated `OpenAPI.SchemaEngine` module for resource identity, URI -resolution, JSON Pointer, anchors, schema dialects, schema compilation, and -runtime validation. - -The schema engine is provisional. It is kept under `src/schema_engine` with no -OpenAPI-specific behavior so it can move to JSONSchema.jl after the API and -implementation have hardened against real OpenAPI documents. - -The default retriever has conservative access rules: - -- A local root can read relative files under the root file's directory. -- Extra local roots require `file_roots=[...]`. -- A URL root can read same-origin HTTP or HTTPS references. -- Cross-origin references require `allow_remote_refs=true`. -- HTTP redirects are not followed. -- Unsupported URI schemes are rejected. - -Pass an `OpenAPI.SchemaEngine.Resources.AbstractRetriever` with `retriever=...` -when an application needs another retrieval policy or an in-memory resource -store. Resource size and count limits still apply. - -Non-schema reference cycles are rejected. Recursive JSON Schemas are retained -and compiled normally. OpenAPI 3.0 Reference Object siblings are ignored. -OpenAPI 3.1 and 3.2 `summary` and `description` siblings are applied. Path Item -Reference Object siblings have undefined specification behavior. Strict mode -rejects them. Permissive mode warns and lets local fields override the target. - -## Generated model behavior - -Generated structs are a typed view over the document's JSON Schemas. Runtime -schema validation remains authoritative. This design protects correctness when -a Julia field type cannot express every schema rule. - -Implemented model behavior includes: - -- objects, arrays, tuples, dictionaries, primitives, enums, and nullable types; -- required, optional, and explicit-null values; -- `allOf`, `oneOf`, `anyOf`, and discriminators; -- recursive models and recursive aliases; -- `additionalProperties`, `patternProperties`, `propertyNames`, and closed - objects; -- JSON Schema assertions such as `const`, `not`, conditions, dependent rules, - bounds, formats, and unevaluated constraints through runtime validation; -- `readOnly` and `writeOnly` request and response projections; -- Julia `Date`, `Time`, `DateTime`, `UUID`, and base64 byte values; -- `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); -- deterministic names with protection against Julia keywords, Base/Core names, - and generated runtime names. - -An unusual schema can plan to `Any` when no useful Julia type exists. It is -still validated at request and response boundaries. Custom JSON Schema dialects -and custom vocabularies can therefore retain correct validation while using a -less precise Julia type. - -Set `validate_requests=false` or `validate_responses=false` on a generated -`Client` only when the application accepts that loss of boundary validation. -For example, a response schema with `additionalProperties: false` rejects a new -server field. This is contract-correct but can make a client less tolerant of -an API that changes outside its published contract. With response validation -disabled, that policy also reaches nested generated models. Unknown response -properties are ignored, and an explicit null on an optional response property -decodes to `nothing` even when the document marks that property non-nullable. -Missing optional properties still decode to `ABSENT`. Values that cannot fit -the generated Julia type can still raise `DecodeError`. - -## Generated modules are baked artifacts - -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 - `Runtime.require_contract(N, version)` at load time, where `N` is the - generated-code contract version (`OpenAPI.Runtime.CONTRACT_VERSION`) 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. Regenerate when the guard reports a -contract mismatch, or when you want a fix that changes generated source. Rerun -`OpenAPI.client` or `OpenAPI.server` against your document and commit the new -file. - -## 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. - -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. - -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`. - -## Streaming responses - -Pass `stream_to = Channel(n)` to any operation to consume the response body -incrementally, e.g. long-running watch endpoints or large exports: - -```julia -events = Channel{Any}(16) -ExampleClient.watch_pods(; client, stream_to = events) # returns at the response head -for event in events - # each item is decoded to the documented response type -end -``` - -The call returns as soon as the response head arrives (the channel itself, or -an `ApiResponse` whose body is the channel with `with_http_info = true`), and a -background task decodes items onto the channel. `application/json` bodies split -into consecutive JSON documents, each decoded against the documented response -schema — the convention used by Kubernetes-style watch endpoints. JSON Lines -and NDJSON bodies decode each line to the documented array's element type, and -JSON text sequences split on RFC 7464 record separators. `text/*` yields lines -and any other media type yields raw byte chunks. The channel closes when the -response ends, closes with the error when decoding or validation fails, and -closing it from the consumer side aborts the transfer. Error statuses still -throw `ApiError` with the fully buffered error body. - -A registered response decoder also applies to streaming calls. The runtime -calls it once for each framed item and puts its return value directly on the -channel. This is an escape hatch for deployed APIs whose streaming wire format -does not match the response schema. Register the full parameterized media type -and select it with `accept` to limit the override to those calls: - -```julia -ExampleClient.codec!( - client, - "application/json;stream=watch"; - decode = (bytes, media_type) -> JSON.parse(String(bytes)), -) -events = Channel{Any}(16) -ExampleClient.watch_pods(; - client, - accept = "application/json;stream=watch", - stream_to = events, -) -``` - -This decoder does not replace the decoder for plain `application/json`. -Codecs are matched against the received Content-Type first, and streaming -calls fall back to the media type the call requested via `accept`: deployed -servers such as the Kubernetes apiserver reply with the bare -`application/json` even when the request selected the parameterized variant, -so passing `accept` is what scopes the override to the watch calls. - -For a custom media type, register an encoder or decoder: - -```julia -ExampleClient.codec!( - client, - "application/cbor"; - encode = (value, media_type) -> encode_cbor(value), - decode = (bytes, media_type) -> decode_cbor(bytes), -) -``` - -XML metadata is retained in the schema but does not generate an XML codec. -Register a custom codec for XML or another non-built-in representation. - -## Security - -Generated clients implement OpenAPI security requirement alternatives and -combinations. Supported credentials include: - -- API keys in headers, query parameters, or cookies; -- HTTP Basic and Bearer authentication; -- other HTTP authentication values; -- OAuth 2.0 and OpenID Connect bearer tokens with documented scope checks; -- mutual TLS through HTTP request options. - -```julia -ExampleClient.credential!( - client, - "bearerAuth", - ExampleClient.BearerCredential("token"; scopes = ["widgets:read"]), -) -``` - -`authorization!(client, token)` is a convenience for every bearer-compatible -scheme in a document. The generated client does not acquire or refresh OAuth or -OpenID Connect tokens. The caller owns that lifecycle. - -By default, a secured operation fails before network access when no documented -credential alternative can be satisfied. Set `require_credentials=false` only -when an external HTTP layer supplies authentication. - -## 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 | -| 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=true` is the default. Use `strict=false` only for documented ecosystem -compatibility cases. Permissive mode can retain ambiguous path templates and a -non-object `deepObject` parameter 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. +Requests are decoded and validated before handlers run; return values are +validated and encoded from the documented responses. Generated stubs do not +authenticate requests — apply a `middleware` for that. ## Create a document from Julia declarations -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. - ```julia 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), - ], + params = [OpenAPI.Param("id", :path, Int)], responsetype = Widget, ), ] -document = OpenAPI.document( - operations; - title = "Widgets", - version = "1.0.0", -) - +document = OpenAPI.document(operations; title = "Widgets", version = "1.0.0") write("openapi.json", JSON.json(document; pretty = 2)) ``` -OpenAPI.jl does not depend on a server framework. Framework packages can add -optional `operations` and `register!` methods. Servo.jl provides its OpenAPI -adapter from a downstream package extension. - -## 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. - -Large descriptions also produce large generated modules because the client -keeps the schema data needed for runtime validation. The pinned Stripe and -GitHub cases are scaling gates for this design. Generation is practical, but -loading either client can take tens of seconds. Applications should generate -and precompile these clients during a build step, not at service startup. - ## Specification sources OpenAPI behavior follows the normative