The generator applies a well defined set of rules to convert OpenAPI documents to Rust code.
Code generation follows the following principles:
- Every valid OpenAPI document will be tranformed into valid Rust code. This means that the goal is to support every construct allowed by the spec.
- Generated code should be ergonomic. This may be in conflict with the first rule, however the generator may apply simplifications where it deems necessary
- Generated code must be usable without modification.
- Generated code should be stable with regards to small changes in the spec document. This means that local changes to the spec should only result in local changes to the generated code. Violation to this rule causes code relying on generated code to break.
- The generator aims to support all major OpenAPI versions (at the time of writing this would be 2.0, 3.0 and 3.1)
Code will be generated in a module whose name is configurable. Only generated code will live inside this module. This helps prevent interference with surrounding user Rust code.
Where possible, the generator will use Rust's built-in types. The types chosen depend
on the the type and the format set in the schema definition (see also the respective section in the OpenAPI 3.0 spec)
When new types are generated, their names are derived from the names in the schemas object
(#/components/schemas/). So a an object type in #/components/schemas/Foo will be mapped as struct Foo.
TODO: Mapping inline schemata
string types are mapped to Rust's String type.
TODO: we will map function parameters to &str in the future.
TODO: Map string types with enum to actual Rust enums
TODO: Other string formats from https://spec.openapis.org/oas/v3.0.4.html#x4-4-1-data-type-format
type |
format |
Rust type(s) |
|---|---|---|
number |
int32 |
i32 |
number |
int64 |
i64 |
number |
float |
f32 |
number |
double |
f64 |
number |
any other | f64 |
Booleans always map to bool.
In general, arrays are mapped to Vec<T>, where T is the type mapped from the schema in the items
property.
Schemas with type object are mapped to generated Rust structs.
TODO: Support allOf / anyOf
TODO: Support oneOf polymorphism by generating Rust enums
TODO: Support additionalProperties via HashMap<String,V>
TODO: Support mapping null, even though this does not make much sense. For Rust, the unit type
() seems appropriate.
The generator will produce a struct called Client, for which it produces an impl block. The block will contains methods for each operation (so each HTTP verb like get, put etc. will produce its own method). The method names derive from the path and the HTTP verb, so the endpoint for GET /foo/bar will become pub fn foo_bar_get(&self).
An OpenAPI operation consits of a number of key components that each influence the way a method is generated.
Every method has Result<T,E> as it's return type. The actual types used for T and E depend on the responses object.
TODO: Async methods
Parameters are mapped directly to operation method parameters. Rust method parameter types derive by the usual rules for type mapping (see above). Since parameters that affect an operation can be declared at the path level or the operation level, both parameter lists are considered for generation.
To generate a list of parameters to include into the operation method from the operation's declared parameters, the following steps are performed:
- Since operation-level parameters can shadow path-level parameters if they have the same values for
inandname, the shadowed parameters are removed from the path-level parameter list. - the list of operation-level parameters is appended to the list of remaining path-level parameters
- Each parameter is converted into a Rust parameter in sequence, applying the rules for type mapping defined above. The name of the generated method parameters are taken by converting the names in the operation parameters.
TODO: instead of using String, use &str for parameters.
If an operation has a request body (defined by its requestBody field), a parameter named body (subject to de-duplication) is appended to the operation method's parameter list.
The type of the parameter is determined using the rules in the section about mapping content.
Broadly speaking, we distinguish between known responses (that is, responses explicitely declared in the OpenAPI file) and unknown responses (responses that the server yields, but are not defined in the API). Unknown responses are generally treated as an error, regardless of their response code (when an endpoint declared to respond with 200 actuall returns 201 instead, this is an error in terms of the spec).
In terms of the responses declared in the OpenAPI file we devide those into two categories: Success responses (1xx, 2xx, 3xx) and Non-Success responses (4xx-5xx, or any other non-2xx code).
The responses of an OpenAPI operation may link to a named response in the #/components/responses path. The names there may be used to name generated types.
Each Client method returns a Result<T,E>, where T represents the success response category (1xx-3xx), and E non-success responses (4xx-5xx HTTP statuses as well as other errors alike).
TODO: what to do with 'default' responses?
We treat all 1xx, 2xx and 3xx response codes as 'success' responses - because in Result<T,E>, T is the type used in Result::Ok(T), which respresents success. In contrast to this, HTTP only calls the 2xx response code range 'success' - the 1xx and 3xx ranges are called 'informational' and 'redirection'. However, all three ranges are treated as 'success' when it comes to the generated result types. The 'Default' range is both 'success' and non-successful (it may be returned in either case).
If there is no informational, success or redirection response defined (no 1xx, 2xx or 3xx codes present), T maps to the Rust unit type ().
If there is exactly one such response defined, T maps to the type that is mapped for the media type(s) in content (see section below)
If there are multiple success responses defined, T maps to an enum that is generated for this purpose. The enum carries the name {operationFragment}Success (GET /foo/bar will become FooBarSuccess). For each defined response, a variant for this response will be generated.
In OAS responses can bei either keyed to explicit status codes like 200 or 302, or be tied to entire ranges, like 2XX or 3XX, or the 'Default' range.
- For explicit status codes, the variants in the enum are called by the respective name of the HTTP code. The variants are crated as tuple variants of
T, so 200 becomesOk200(T), 201 becomesCreated201(T), etc. For non-standard codes in the OAS-supported 100..599 range, which don't have a name, the enum variant name is formed using the patternStatus{code} - so a code 288 is represented by theStatus288(T)variant. - Status code ranges like '2XX' are created as
Status{range}. The variant is generated as tuple(u16,T), where theu16value stores the status code. So '2XX' becomesStatus2XX(u16,T). - Declared 'Default' responses are generated as the
Default(u16,T)variant. Like ranges,u16contains the status code.
Non-Success responses as well as other potential errors that can happen when calling a HTTP endpoint must be represented by the error type E in a method's Result<T,E> return type.
Therefore, E must be able to represent:
- Defined error responses, distinguished by their HTTP status
- Non-HTTP errors such as network or general I/O errors
- Undefined responses, success or error alike
For this reason, E will always be a generated Rust enum.
The enum is generated as follows:
- The name is composed of {operationFragment}
Error. So forPUT /pet, the generated enum will be calledPutPetError. - The variants are defined like this:
- Declared error codes, such as HTTP 400, are called after their {statusFragment}, so HTTP 400 becomes
NotFound400. For each declared HTTP error (4xx or 5xx ranges), such a variant is generated. The variants are generated as tuple variants, whose single member type is the type yielded by mapping the media type of that response (see section below). - For undeclared HTTP responses, a variant called
UnknownResponseis generated. The variant is generated as a tuple variant whoose type ishttp::Responsefrom thehttpcrate. - For all other errors, a tuple variant
OtherErroris generated. It's contained type isBox<dyn Error>.
- Declared error codes, such as HTTP 400, are called after their {statusFragment}, so HTTP 400 becomes
A Media type content Map is present in these places in OAS:
- Parameter Object
- Request Body Object
- Response Body Object
- Header Object
For each of these, OAS defines a 'content' attribute, validated against a Map[String,Media Type Object].
For example, a response or a request body defined in OpenAPI may define schemata for one or more media types. Consider the following fragment:
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
application/xml:
schema:
$ref: "#/components/schemas/Pet"Note that the example above is taken from the petstore example for OpenAPI which defines the same schema for the XML and JSON media types, but this may not be the case for other OpenAPI for other OpenAPI definitions.
The example shown above will generate the following enum:
enum PetPutOk200Content {
ApplicationJson(Pet),
ApplicationXml(Pet),
}The type mapped to a response follows the following rules:
- If a request body or response declaration does not have a
contentfield or the content map is empty, the mapped type is the unit type() - Otherwise, the mapped type for the request or response is the type mapped via the
contentattribute.
- If the
contentmap is empty, the mapped type is the unit type() - If the
contentmap contains only one media type, the type representing the declaration is the type mapped for this media type (see below). - Otherwise, an enum is generated to distinguish between media types. The enum is generated according to the following rules:
- The name of the enum depends on where the
contentattribute appears (request body, response, ...). In general, the location ofcontentproduces a {prefix}. The pattern for the name is {prefix}Content. - For each media type, an enum variant is introduced. Media types are translated into Rust enum variant names by using the type and subtype and uppercasing each of their first characters. Then both are joined into a string (ignoring the '/' separator). Media type wildcard characters (
*) present in the type or subtype are replaced by the stringAny. So a media typeapplication/jsonbecomes an enum variant calledApplicationJson. A media type wildcardtext/*will becomeTextAny. Non-alphabetic characters are removed. - The generated enum variants are generated as tuple variants with a single field. The field's type is the field mapped for this media type (see below)
- The name of the enum depends on where the
A single media type will be mapped to a type according to the following rules:
- If the media type contains a
schemafield, the type mapped for this media type is the type mapped for this schema - Otherwise, a type implementing
std::io::Readwill be mapped. The idea is that, because the spec does not sufficiently specify what the content is, the client falls back to reading the content's binary representation
If an enum is generated for the content propery, its name is created using the pattern {prefix}Content. {prefix} depends on the OAS object in which the content attribute is embedded:
- Request Body Object: {prefix} is the {operationFragment}. For
PUT /pet, {operationFragment} isPetPut. So forPUT /pet, the enum will be calledPetPutContent. - Response Object:
- If the response is inlined, {prefix} is {operationFragment}{statusFragment}. For the HTTP 200 example above, the {statusFragment} is
Ok200. So the resulting name of the enum isPetPutOk200Content. - If the response is declared in
#/components/responses, {prefix} is the local name of the Response Body Object
- If the response is inlined, {prefix} is {operationFragment}{statusFragment}. For the HTTP 200 example above, the {statusFragment} is
- Parameter Object:
- If the parameter is inlined (in an operation object, it only occurs there), {prefix} is {operationFragment}{paramName}. {paramName} is taken from the parameter objects's
nameproperty. For example the type name for theX-APIKeyheader onPUT /petwill bePetPutX_APIKeyContent(not that X-APIKey translates to X_APIKey) - If the parameter is declared in
#/components/parameters, {prefix} is the local name of the Parameter Object.
- If the parameter is inlined (in an operation object, it only occurs there), {prefix} is {operationFragment}{paramName}. {paramName} is taken from the parameter objects's
- TODO: Header Object
TODO: Be more concrete in which type is mapped if there is no schema. In this case we may also think about providing more information, like HTTP headers, to provide additional information to API consumers. An alternative to providing a std::io::Read could be the underlying client's entire response object.