Geração de Documentação OpenAPI

OpenAPI é uma especificação de código aberto para descrever o design de interfaces de API RESTful. Ele define estruturas de requisição e resposta da API, parâmetros, tipos de retorno, códigos de erro e outros detalhes em formato JSON ou YAML, tornando a comunicação entre cliente e servidor mais explícita e padronizada.

OpenAPI era originalmente a versão de código aberto da especificação Swagger e agora se tornou um projeto independente apoiado por muitas grandes empresas e desenvolvedores. Usar a especificação OpenAPI ajuda as equipes de desenvolvimento a colaborar melhor, reduzir custos de comunicação e melhorar a eficiência de desenvolvimento. Além disso, o OpenAPI fornece aos desenvolvedores ferramentas para gerar automaticamente documentação de API, dados simulados e casos de teste, facilitando o trabalho de desenvolvimento e teste.

O Salvo fornece integração OpenAPI (modificada a partir do utoipa). O Salvo extrai elegantemente informações relevantes de tipos de dados OpenAPI automaticamente do Handler com base em suas próprias características. O Salvo também integra várias interfaces OpenAPI de código aberto populares, como SwaggerUI, Scalar, RapiDoc e ReDoc.

Como os nomes de tipos Rust podem ser longos e nem sempre adequados para uso no OpenAPI, o salvo-oapi fornece o tipo Namer, que permite personalizar regras para alterar nomes de tipos no OpenAPI conforme necessário.

Código de Exemplo

main.rs
Cargo.toml
use salvo::oapi::extract::*;
use salvo::prelude::*;

#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
    format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path("hello").get(hello));

    let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);

    let router = router
        .unshift(doc.into_router("/api-doc/openapi.json"))
        .unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui"));

    let acceptor = TcpListener::new("0.0.0.0:8698").bind().await;
    Server::new(acceptor).serve(router).await;
}

Digite http://localhost:8698/swagger-ui em seu navegador para ver a página do Swagger UI.

A integração OpenAPI no Salvo é bastante elegante. Para o exemplo acima, em comparação com um projeto Salvo normal, apenas fizemos as seguintes etapas:

  • Habilitar o recurso oapi em Cargo.toml: salvo = { workspace = true, features = ["oapi"] };

  • Substituir #[handler] por #[endpoint];

  • Usar name: QueryParam<String, false> para obter o valor da string de consulta. Quando você acessa http://localhost/hello?name=chris, a string de consulta name será analisada. O false em QueryParam<String, false> significa que este parâmetro é opcional. Se você acessar http://localhost/hello, não retornará um erro. Por outro lado, se for QueryParam<String, true>, significa que este parâmetro deve ser fornecido, caso contrário, um erro será retornado.

  • Criar OpenAPI e o Router correspondente. O merge_router em OpenApi::new("test api", "0.0.1").merge_router(&router) significa que este OpenAPI obtém as informações de documento necessárias analisando uma determinada rota e suas sub-rotas. Alguns Handlers de rotas podem não fornecer informações para gerar documentos, e essas rotas serão ignoradas, como Handlers definidos usando a macro #[handler] em vez da macro #[endpoint]. Ou seja, em projetos reais, por razões como progresso de desenvolvimento, você pode optar por não gerar documentos OpenAPI ou gerar documentos OpenAPI parcialmente. Posteriormente, você pode aumentar gradualmente o número de interfaces OpenAPI geradas, e tudo o que você precisa fazer é alterar #[handler] para #[endpoint] e modificar a assinatura da função.

Extratores de Dados

Você pode importar extratores de dados comuns predefinidos através de use salvo::oapi::extract::*;. O extrator fornecerá algumas informações necessárias ao Salvo para que o Salvo possa gerar documentos OpenAPI.

  • QueryParam<T, const REQUIRED: bool>: Um extrator para extrair dados de strings de consulta. QueryParam<T, false> significa que este parâmetro não é obrigatório e pode ser omitido. QueryParam<T, true> significa que este parâmetro é obrigatório e não pode ser omitido. Se não for fornecido, um erro será retornado;

  • HeaderParam<T, const REQUIRED: bool>: Um extrator para extrair dados do cabeçalho da requisição. HeaderParam<T, false> significa que este parâmetro não é obrigatório e pode ser omitido. HeaderParam<T, true> significa que este parâmetro é obrigatório e não pode ser omitido. Se não for fornecido, um erro será retornado;

  • CookieParam<T, const REQUIRED: bool>: Um extrator para extrair dados do cookie da requisição. CookieParam<T, false> significa que este parâmetro não é obrigatório e pode ser omitido. CookieParam<T, true> significa que este parâmetro é obrigatório e não pode ser omitido. Se não for fornecido, um erro será retornado;

  • PathParam<T>: Um extrator para extrair parâmetros de caminho da URL da requisição. Se este parâmetro não existir, a correspondência de rota não será bem-sucedida, portanto, não há caso em que possa ser omitido;

  • FormBody<T>: Extrai informações do formulário enviado pela requisição;

  • JsonBody<T>: Extrai informações do payload em formato JSON enviado pela requisição;

#[endpoint]

Ao gerar documentos OpenAPI, você precisa usar a macro #[endpoint] em vez da macro regular #[handler]. Na verdade, é uma versão aprimorada da macro #[handler].

  • Ela pode obter as informações necessárias para gerar OpenAPI através da assinatura da função;

  • Para informações que não são convenientes de fornecer através da assinatura, você pode fornecê-las diretamente adicionando atributos na macro #[endpoint]. As informações fornecidas dessa forma serão mescladas com as informações obtidas através da assinatura da função. Se houver conflito, elas substituirão as informações fornecidas pela assinatura da função.

Você pode usar o atributo #[deprecated] embutido do Rust para marcar um Handler como obsoleto. Embora o atributo #[deprecated] suporte a adição de informações como motivo da depreciação e versão, o OpenAPI não o suporta, portanto, essas informações serão ignoradas ao gerar OpenAPI.

A parte do comentário de documentação no código será extraída automaticamente para gerar OpenAPI. A primeira linha é usada para gerar summary, e toda a parte do comentário será usada para gerar description.

/// Este é um resumo da operação
///
/// Todas as linhas do comentário de documentação serão incluídas na descrição da operação.
#[endpoint]
fn endpoint() {}

ToSchema

Você pode usar #[derive(ToSchema)] para definir estruturas de dados:

#[derive(ToSchema)]
struct Pet {
    id: u64,
    name: String,
}

Você pode usar #[salvo(schema(...))] para definir configurações opcionais:

  • example = ... pode ser json!(...). json!(...) será analisado por serde_json::json! como serde_json::Value.

    #[derive(ToSchema)]
    #[salvo(schema(example = json!({"name": "bob the cat", "id": 0})))]
    struct Pet {
        id: u64,
        name: String,
    }
  • xml(...) pode ser usado para definir propriedades do objeto Xml:

    #[derive(ToSchema)]
    struct Pet {
        id: u64,
        #[salvo(schema(xml(name = "pet_name", prefix = "u")))]
        name: String,
    }

ToParameters

Gera [parâmetros de caminho][path_parameters] a partir dos campos da estrutura.

Esta é a implementação #[derive] do trait [ToParameters][to_parameters].

Normalmente, os parâmetros de caminho precisam ser definidos em [#[salvo_oapi::endpoint(...parameters(...))]][path_parameters] do endpoint. No entanto, ao usar uma [struct][struct] para definir parâmetros, as etapas acima podem ser omitidas. No entanto, se você precisar fornecer uma descrição ou alterar a configuração padrão, então parâmetros de caminho de [tipos primitivos][primitive] e [String][std_string] ou parâmetros de caminho no estilo [tupla] ainda precisam ser definidos em parameters(...).

Você pode usar o atributo #[deprecated] embutido do Rust para marcar campos como obsoletos, o que será refletido na especificação OpenAPI gerada.

O atributo #[deprecated] suporta a adição de informações extras, como o motivo da depreciação ou a versão a partir da qual está obsoleto, mas o OpenAPI não o suporta. O OpenAPI suporta apenas um valor booleano para determinar se está obsoleto. Embora seja perfeitamente possível declarar uma depreciação com um motivo, como #[deprecated = "Há uma maneira melhor de fazer isso"], esse motivo não será apresentado na especificação OpenAPI.

O documento de comentário no campo da estrutura será usado como descrição do parâmetro na especificação OpenAPI gerada.

#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
    /// Consulta itens de tarefa por nome.
    name: String
}

Atributos de Contêiner ToParameters para #[salvo(parameters(...))]

Os seguintes atributos podem ser usados no atributo de contêiner #[salvo(parameters(…))] da estrutura derivada de ToParameters

  • names(...) Define uma lista separada por vírgulas de nomes para os campos não nomeados da estrutura usados como parâmetros de caminho. Suportado apenas em estruturas não nomeadas.
  • style = ... Define o método de serialização para todos os parâmetros, especificado por [ParameterStyle][style]. O valor padrão é baseado no atributo parameter_in.
  • default_parameter_in = ... Define a posição padrão usada pelos parâmetros deste campo. O valor desta posição vem de [parameter::ParameterIn][in_enum]. Se este atributo não for fornecido, o padrão é query.
  • rename_all = ... pode ser usado como uma alternativa ao rename_all do serde. Na verdade, fornece a mesma funcionalidade.

Use names para definir um nome para um único parâmetro não nomeado.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id")))]
struct Id(u64);

Use names para definir nomes para vários parâmetros não nomeados.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id", "name")))]
struct IdAndName(u64, String);

Atributos de Campo ToParameters para #[salvo(parameter(...))]

Os seguintes atributos podem ser usados em campos de estrutura #[salvo(parameter(...))]:

  • style = ... Define como os parâmetros são serializados por [ParameterStyle][style]. O valor padrão é baseado no atributo parameter_in.

  • parameter_in = ... Use o valor de [parameter::ParameterIn][in_enum] para definir onde este parâmetro de campo está. Se este valor não for fornecido, o padrão é query.

  • explode Define se deve criar um novo par parameter=value para cada parâmetro em object ou array.

  • allow_reserved Define se caracteres reservados :/?#[]@!$&'()*+,;= são permitidos no valor do parâmetro.

  • example = ... pode ser uma referência de método ou json!(...). O exemplo fornecido substituirá quaisquer exemplos do tipo de parâmetro subjacente.

  • value_type = ... pode ser usado para substituir o tipo padrão usado pelos campos na especificação OpenAPI. Isso é útil quando o tipo padrão não corresponde ao tipo real, por exemplo, ao usar tipos de terceiros não definidos em [ToSchema][to_schema] ou tipos [primitivos][primitive]. O valor pode ser qualquer tipo Rust que possa ser serializado para JSON em circunstâncias normais, ou um tipo personalizado como _Object_._Object`_ que será renderizado como um objeto OpenAPI genérico.

  • inline Se habilitado, a definição deste tipo de campo deve vir de [ToSchema][to_schema], e esta definição será embutida.

  • default = ... pode ser uma referência de método ou json!(...).

  • format = ... pode ser uma variante do enum [KnownFormat][known_format], ou um valor aberto na forma de string. Por padrão, o formato é inferido do tipo do atributo de acordo com a especificação OpenApi.

  • write_only Define que o atributo é usado apenas para operações de escrita POST,PUT,PATCH e não GET.

  • read_only Define que o atributo é usado apenas para operações de leitura GET e não POST,PUT,PATCH.

  • nullable Define se o atributo pode ser null (note que isso é diferente de não obrigatório).

  • required = ... é usado para forçar o parâmetro a ser obrigatório. Veja as regras.

  • rename = ... pode ser usado como uma alternativa ao rename do serde. Na verdade, fornece a mesma funcionalidade.

  • multiple_of = ... é usado para definir um múltiplo do valor. O valor do parâmetro é considerado válido apenas quando o valor do parâmetro é dividido pelo valor desta palavra-chave e o resultado é um inteiro. O valor múltiplo deve ser estritamente maior que 0.

  • maximum = ... é usado para definir o limite superior do valor, incluindo o valor atual.

  • minimum = ... é usado para definir o limite inferior do valor, incluindo o valor atual.

  • exclusive_maximum = ... é usado para definir o limite superior do valor, excluindo o valor atual.

  • exclusive_minimum = ... é usado para definir o limite inferior do valor, excluindo o valor atual.

  • max_length = ... é usado para definir o comprimento máximo de um valor do tipo string.

  • min_length = ... é usado para definir o comprimento mínimo de um valor do tipo string.

  • pattern = ... é usado para definir uma expressão regular válida que o valor do campo deve corresponder. A expressão regular adota a versão ECMA-262.

  • max_items = ... pode ser usado para definir o número máximo de itens permitidos em um campo do tipo array. O valor deve ser um inteiro não negativo.

  • min_items = ... pode ser usado para definir o número mínimo de itens permitidos em um campo do tipo array. O valor deve ser um inteiro não negativo.

  • with_schema = ... usa uma referência de função para criar um schema em vez do schema padrão. A função deve satisfazer a definição fn() -> Into<RefOr<Schema>>. Ela não recebe nenhum parâmetro e deve retornar qualquer valor que possa ser convertido em RefOr<Schema>.

  • additional_properties = ... é usado para definir tipos de forma livre para map, como HashMap e BTreeMap. Tipos de forma livre permitem que qualquer tipo seja usado nos valores do mapa. Os formatos suportados são additional_properties e additional_properties = true.

Regras de nulidade e obrigatoriedade de campos

Algumas das regras para nulidade e obrigatoriedade de atributos de campo ToParameters também podem ser usadas para atributos de campo ToSchema. Veja as regras.

Suporte parcial a atributos #[serde(...)]

A derivação ToParameters atualmente suporta alguns [atributos serde][serde attributes]. Esses atributos suportados serão refletidos na documentação OpenAPI gerada. Os seguintes atributos são atualmente suportados:

  • rename_all = "..." é suportado no nível do contêiner.
  • rename = "..." é apenas suportado no nível do campo.
  • default é suportado no nível do contêiner e do campo de acordo com [atributos serde][serde attributes].