Geração de Documentação OpenAPI

OpenAPI é uma especificação de código aberto para descrever o design de interfaces de APIs RESTful. Ele define a estrutura de requisições e respostas da API, parâmetros, tipos de retorno, códigos de erro e outros detalhes em formatos JSON ou YAML, tornando a comunicação entre cliente e servidor mais clara e padronizada.

Originalmente, OpenAPI era a versão de código aberto da especificação Swagger, mas agora se tornou um projeto independente, com o apoio de muitas grandes empresas e desenvolvedores. Usar a especificação OpenAPI pode ajudar equipes de desenvolvimento a colaborar melhor, reduzir custos de comunicação e aumentar a eficiência. Além disso, OpenAPI oferece ferramentas para gerar automaticamente documentação de API, dados simulados (Mock) e casos de teste, facilitando o desenvolvimento e os testes.

O Salvo fornece integração com OpenAPI (modificado a partir do utoipa). O Salvo, aproveitando suas próprias características, extrai elegantemente informações de tipo OpenAPI automaticamente a partir de Handler. O Salvo também integra interfaces OpenAPI populares como SwaggerUI, scalar, rapidoc e redoc.

Para lidar com nomes de tipos Rust longos, que podem não ser adequados para OpenAPI, o salvo-oapi fornece o tipo Namer, que permite definir regras personalizadas para alterar os nomes dos tipos no OpenAPI.

Código de Exemplo

main.rs
Cargo.toml
oapi-hello/src/main.rs
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:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

Digite http://localhost:5800/swagger-ui no navegador para ver a página do Swagger UI.

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

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

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

  • Usar name: QueryParam<String, false> para obter o valor da string de consulta. Ao acessar a URL http://localhost/hello?name=chris, a string de consulta name será analisada. O false em QueryParam<String, false> indica que o parâmetro é opcional. Se acessar http://localhost/hello, não ocorrerá erro. Por outro lado, QueryParam<String, true> indica que o parâmetro é obrigatório, caso contrário, retornará um erro.

  • Criar um OpenAPI e o Router correspondente. OpenApi::new("test api", "0.0.1").merge_router(&router) significa que o OpenAPI obtém as informações necessárias da documentação analisando um roteador e seus descendentes. Alguns roteadores podem não fornecer informações para gerar documentação e serão ignorados, como Handlers definidos com #[handler] em vez de #[endpoint]. Ou seja, em projetos reais, por questões de progresso no desenvolvimento, você pode optar por não gerar documentação OpenAPI ou gerá-la parcialmente. Posteriormente, você pode aumentar gradualmente o número de interfaces OpenAPI geradas, e tudo o que precisa fazer é substituir #[handler] por #[endpoint] e ajustar a assinatura da função.

Extratores de Dados

Através de use salvo::oapi::extract::*;, você pode importar extratores de dados pré-definidos e comuns. Os extratores fornecem informações necessárias ao Salvo para gerar a documentação OpenAPI.

  • QueryParam<T, const REQUIRED: bool>: Um extrator que obtém dados da string de consulta. QueryParam<T, false> indica que o parâmetro é opcional. QueryParam<T, true> indica que o parâmetro é obrigatório e, se não fornecido, retornará um erro.

  • HeaderParam<T, const REQUIRED: bool>: Um extrator que obtém dados do cabeçalho da requisição. HeaderParam<T, false> indica que o parâmetro é opcional. HeaderParam<T, true> indica que o parâmetro é obrigatório e, se não fornecido, retornará um erro.

  • CookieParam<T, const REQUIRED: bool>: Um extrator que obtém dados do cabeçalho da requisição. CookieParam<T, false> indica que o parâmetro é opcional. CookieParam<T, true> indica que o parâmetro é obrigatório e, se não fornecido, retornará um erro.

  • PathParam<T>: Um extrator que obtém parâmetros de caminho da URL. Se este parâmetro não existir, a correspondência da rota falhará, portanto, não há cenário onde ele possa ser omitido.

  • FormBody<T>: Obtém informações de um formulário enviado na requisição.

  • JsonBody<T>: Obtém informações de uma carga útil JSON enviada na requisição.

#[endpoint]

Ao gerar documentação OpenAPI, use a macro #[endpoint] no lugar da macro #[handler] padrão. Ela é essencialmente uma versão aprimorada de #[handler].

  • Ela pode obter informações necessárias para gerar o OpenAPI a partir da assinatura da função.

  • Para informações que não podem ser fornecidas pela assinatura, você pode adicionar atributos diretamente na macro #[endpoint]. As informações fornecidas dessa maneira serão mescladas com as obtidas da assinatura, e em caso de conflito, as informações dos atributos substituirão as da assinatura.

Você pode usar o atributo #[deprecated] do Rust para marcar um Handler como obsoleto. Embora o atributo #[deprecated] suporte informações como motivo ou versão, o OpenAPI não as suporta, portanto, essas informações serão ignoradas na geração do OpenAPI.

Os comentários de documentação no código serão automaticamente extraídos para gerar o OpenAPI. A primeira linha é usada como summary, e todo o comentário é usado como 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 definir estruturas de dados usando #[derive(ToSchema)]:

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

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

  • 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 de 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 de uma estrutura.

Esta é uma 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 os parâmetros, essa etapa pode ser omitida. Ainda assim, se for necessário fornecer descrições ou alterar configurações padrão, parâmetros de caminho de [tipos primitivos][primitive] e [String][std_string] ou parâmetros de caminho no estilo [tupla] precisarão ser definidos em parameters(...).

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

O atributo #[deprecated] suporta informações adicionais, como motivo ou versão em que foi descontinuado, mas o OpenAPI não. O OpenAPI suporta apenas um valor booleano para indicar se está obsoleto. Embora seja possível declarar uma depreciação com um motivo, como #[deprecated = "Há uma maneira melhor de fazer isso"], o motivo não aparecerá na especificação OpenAPI.

Os comentários de documentação nos campos da estrutura serão usados como descrição dos parâmetros na especificação OpenAPI gerada.

#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
    /// Filtra 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(…))] de estruturas que derivam de ToParameters:

  • names(...) define uma lista separada por vírgulas de nomes para campos não nomeados da estrutura usados como parâmetros de caminho. Suportado apenas em estruturas não nomeadas.
  • style = ... define como todos os parâmetros são serializados, conforme especificado por [ParameterStyle][style]. O padrão é baseado no atributo parameter_in.
  • default_parameter_in = ... define a localização padrão dos parâmetros deste campo, com o valor vindo de [parameter::ParameterIn][in_enum]. Se não fornecido, o padrão é query.
  • rename_all = ... pode ser usado como alternativa ao rename_all do serde. Oferece a mesma funcionalidade.

Use names para definir nomes 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 com #[salvo(parameter(...))]:

  • style = ... define como o parâmetro é serializado por [ParameterStyle][style]. O padrão é baseado no atributo parameter_in.

  • parameter_in = ... define onde o parâmetro deste campo está localizado, com o valor vindo de [parameter::ParameterIn][in_enum]. Se não fornecido, o padrão é query.

  • explode define se novos pares parameter=value são criados para cada parâmetro em um object ou array.

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

  • example = ... pode ser uma referência a um método ou json!(...). O exemplo fornecido substitui qualquer exemplo do tipo subjacente.

  • value_type = ... pode ser usado para substituir o tipo padrão usado para o campo na especificação OpenAPI. Útil quando o tipo padrão não corresponde ao tipo real, como ao usar tipos de terceiros não definidos em [ToSchema][to_schema] ou [tipos primitivos][primitive]. O valor pode ser qualquer tipo Rust que normalmente seria serializado em JSON ou um tipo personalizado como Object. Object será renderizado como um objeto OpenAPI genérico.

  • inline se habilitado, a definição do tipo deste campo deve vir de [ToSchema][to_schema], e essa definição será incluída inline.

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

  • format = ... pode ser uma variante do enum [KnownFormat][known_format] ou um valor aberto como string. Por padrão, o formato é inferido a partir do tipo da propriedade de acordo com a especificação OpenAPI.

  • write_only define que a propriedade é usada apenas para operações de escrita POST,PUT,PATCH e não para GET.

  • read_only define que a propriedade é usada apenas para operações de leitura GET e não para POST,PUT,PATCH.

  • nullable define se a propriedade pode ser null (observação: isso é diferente de não ser obrigatória).

  • required = ... usado para forçar que o parâmetro seja obrigatório. Veja as regras.

  • rename = ... pode ser usado como alternativa ao rename do serde. Oferece a mesma funcionalidade.

  • multiple_of = ... usado para definir um múltiplo do valor. O valor do parâmetro só é considerado válido se dividido por este valor resultar em um inteiro. O valor deve ser estritamente maior que 0.

  • maximum = ... usado para definir um limite superior inclusivo para o valor.

  • minimum = ... usado para definir um limite inferior inclusivo para o valor.

  • exclusive_maximum = ... usado para definir um limite superior exclusivo para o valor.

  • exclusive_minimum = ... usado para definir um limite inferior exclusivo para o valor.

  • max_length = ... usado para definir o comprimento máximo para valores do tipo string.

  • min_length = ... usado para definir o comprimento mínimo para valores do tipo string.

  • pattern = ... usado para definir uma expressão regular que o valor do campo deve corresponder. A expressão regular segue o padrã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 um schema criado por uma referência de função em vez do schema padrão. A função deve satisfazer fn() -> Into<RefOr<Schema>>. Não recebe parâmetros 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 qualquer tipo nos valores do mapa. Os formatos suportados são additional_properties e additional_properties = true.

Regras de nulidade e obrigatoriedade de campos

Algumas regras aplicadas aos atributos de campo ToParameters para nulidade e obrigatoriedade também se aplicam aos 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. Atualmente, os seguintes atributos são suportados:

  • rename_all = "..." suportado no nível do contêiner.
  • rename = "..." suportado apenas no nível do campo.
  • default suportado no nível do contêiner e do campo, de acordo com os [atributos serde][serde attributes].
  • skip_serializing_if = "..." suportado apenas no nível do campo.
  • with = ... suportado apenas no nível do campo.
  • skip_serializing = "..." suportado apenas no nível do campo ou variante.
  • skip_deserializing = "..." suportado apenas no nível do campo ou variante.
  • skip = "..." suportado apenas no nível do campo.

Outros atributos serde afetarão a serialização, mas não serão refletidos na documentação OpenAPI gerada.

Exemplos

_**Demonstra o uso do atributo de contêiner `#[salvo(parameters