/openapi-lint

Validate an OpenAPI schema against some rules

Primary LanguageRustApache License 2.0Apache-2.0

openapi-lint

This is a simple crate to validate OpenAPI v3.0.3 content. It flags constructs that we've determined are not "ergonomic" or "well-designed". In particular we try to avoid constructs that lead to structures that SDK generators would have a hard time turning into easy-to-use native constructs.

Rules

Type mismatch

A schema that describes a type may include subschemas where one, all, or any of the subschemas might match ( for the oneOf, allOf, and anyOf fields respectively). For example, the following Rust code produces such a schema with mixed types:

#[derive(JsonSchema)]
pub enum E {
    ThingA(String),
    ThingB,
}

A JSON object that used this enum for the type of a field could look like this:

{
    "field": { "ThingA": "some value" }
}

or this:

{
    "field": "ThingB"
}

So field may be either a string or an object. This complicates the description of these types and is harder to represent in SDKs (in particular those without Rust's ability for enums to have associated values). To avoid this, we can simply use serde's facility for annotating enums. In particular, we prefer "adjacently tagged" enums:

#[derive(JsonSchema)]
#[serde(tag = "type", content = "value")]
pub enum E {
    ThingA(String),
    ThingB,
}

This produces JSON like this:

{
    "field1": { "type": "ThingA", "value": "some value" },
    "field2": { "type": "ThingB" }
}

Paths

Paths (routes) with compound-words as components should use kebab case.

This /service-processors/{sp_id}/serial-console
Not this /service_processors/{sp_id}/serial_console

Naming

In general, we use the typical Rust naming conventions.

  • All type names should be PascalCase.
  • All operation_ids should be snake_case.
  • All operation properties should be snake_case.
  • All struct (and struct enum variant) members should be snake_case.
  • All enum variants should be snake_case. (Note that depending on the serde tagging scheme used, variant names may appear in OpenAPI as either struct property names (external tagging) or as constant values (internal or adjacent tagging). The choice of snake_case makes naming uniform regardless of the tagging scheme.)

Type names are already PascalCase by normal Rust conventions. If you need (really?) to have a type with a non-PascalCase name, you can renamed it like this:

#[derive(JsonSchema)]
#[allow(non_camel_case_types)]
#[serde(rename = "IllumosButUpperCase")]
struct illumosIsAlwaysLowerCaseIGuess {
    // ...
}

Operation IDs come from the function name. If you obey the normal Rust convention, your functions are already snake_case. There isn't currently a facility to change the operation name; file an issue in (dropshot)[https://github.com/oxidecomputer/dropshot] if this is required.

Rust enums typically name variants with PascalCase. Typically you'll rename them all to snake_case:

#[derive(JsonSchema)]
#[serde(rename_all = "snake_case")]
enum Things {
    ThingA,
    ThingB,
}

Sometimes you might prefer SCREAMING_SNAKE_CASE e.g. for things that are more typically abbreviated:

#[derive(JsonSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum Things {
    ThingA,
    ThingB,
}

UUIDs

It's tempting to name fields that are UUIDs with an _uuid suffix, but this is redundant. For simplicity and consistency we use the _id suffix instead.

Trivial Null Response

(Drophot)[https://github.com/oxidecomputer/dropshot] makes it easy (too easy!) to accidentally return a null response when you intend to return an empty response.

Consider this handler:

#[endpoint {
    method = POST,
    path = "/device/confirm",
}]
pub async fn device_auth_confirm(
    rqctx: Arc<RequestContext<Arc<ServerContext>>>,
) -> Result<HttpResponseOk<()>, HttpError> {
    // ...
}

The corresponding OpenAPI responses will be:

{
  "200": {
    "description": "successful operation",
    "content": {
      "application/json": {
        "schema": {
          "title": "Null",
          "type": "string",
          "enum": [
            null
          ]
        }
      }
    }
  }
}

Instead, use HttpResponseUpdatedNoContent:

#[endpoint {
    method = POST,
    path = "/device/confirm",
}]
pub async fn device_auth_confirm(
    rqctx: Arc<RequestContext<Arc<ServerContext>>>,
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
    // ...
}

External Rules

These rules only apply to APIs that are "external".

Rust Documentation

Both dropshot and schemars use rustdoc comments as the basis for documentation fields (specifically title and description). As such, it's easy to accidentally allow internally-relevant documentation leak out as externally-visible in the OpenAPI document. It's not possible to simply infer this from text alone, but we do look for shibboleths such as a Rust path delimeter (::) and bracketed expressions with no subsequent parentheses ([title](http://link.dest) being reasonable).