OpenAPI
OpenAPI es una especificación de código abierto que se utiliza para describir el diseño de interfaz de las API RESTful. Define los detalles de la estructura, parámetros, tipos de retorno, códigos de error y otros aspectos de las solicitudes y respuestas API en formato JSON o YAML, haciendo que la comunicación entre clientes y servidores sea más clara y estandarizada.
Inicialmente desarrollado como una versión de código abierto de la especificación Swagger, OpenAPI ahora se ha convertido en un proyecto independiente y ha obtenido el apoyo de muchas grandes empresas y desarrolladores. El uso de la especificación OpenAPI puede ayudar a los equipos de desarrollo a colaborar mejor, reducir los costos de comunicación y mejorar la eficiencia del desarrollo. Además, OpenAPI proporciona a los desarrolladores herramientas como generación automática de documentación API, datos simulados y casos de prueba para facilitar el trabajo de desarrollo y prueba.
Salvo proporciona integración con OpenAPI (adaptado de utoipa).
Ejemplo
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
.push(doc.into_router("/api-doc/openapi.json"))
.push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));
let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
Server::new(acceptor).serve(router).await;
}
[package]
name = "example-oapi-hello"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
salvo = { workspace = true, features = ["oapi"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true
PAra ver la interfáz de Swagger, escribe en tu navegador lo siguiente: http://localhost:5800/swagger-ui.
La integración de OpenAPI en Salvo es bastante elegante. Para el ejemplo anterior, en comparación con un proyecto Salvo normal, solo necesitamos seguir los siguientes pasos:
Habilita la característica oapien el archivo
Cargo.toml
:salvo = { workspace = true, features = ["oapi"] }
;Reemplaza
#[handler]
por#[endpoint]
;Usa el nombre:
QueryParam<String, false>
para obtener el valor del string consultado. Cuando visitashttp://localhost/hello?name=chris
, la consulta será tipada como corresponde. El valor falso aquí indica que este parámetro es opcional. Si visitahttp://localhost/hello
sin el parámetro de nombre, no provocará un error. Por el contrario, si esQueryParam<String, true>
, significa que este parámetro es obligatorio y se devolverá un error si no se proporciona.Cree una OpenAPI y cree el enrutador correspondiente.
OpenApi::new("test api", "0.0.1").merge_router(&router)
aquí merge_router significa que esta OpenAPI obtiene la información de documentación necesaria analizando una determinada ruta y sus rutas descendientes. Es posible que algunas rutas no proporcionen información para generar documentación y estas rutas se ignorarán, como el controlador definido usando la macro#[handler]
en lugar de la macro#[endpoint]
. En otras palabras, en proyectos reales, por razones como el progreso del desarrollo, puede optar por no generar documentación OpenAPI o generarla solo parcialmente. Más adelante, puede aumentar gradualmente la cantidad de interfaces OpenAPI generadas y todo lo que necesita hacer es cambiar#[handler]
a#[endpoint]
y modificar la firma de la función.
Extractores
Al utilizar use salvo::oapi::extract:*;, puede importar extractores de datos de uso común que están prediseñados en Salvo. Estos extractores proporcionan la información necesaria a Salvo para que pueda generar documentación OpenAPI.
QueryParam<T, const REQUIRED: bool>
: un extractor que extrae datos de cadenas de consulta.QueryParam<T, false>
significa que este parámetro es opcional y se puede omitir.QueryParam<T, true>
significa que este parámetro es obligatorio y no se puede omitir. Si no se proporciona, se devolverá un error;HeaderParam<T, const REQUIRED: bool>
: un extractor que extrae datos de los encabezados de solicitud.HeaderParam<T, false>
significa que este parámetro es opcional y se puede omitir.HeaderParam<T, true>
significa que este parámetro es obligatorio y no se puede omitir. Si no se proporciona, se devolverá un error;CookieParam<T, const REQUIRED: bool>
: un extractor que extrae datos de las cookies de solicitud.CookieParam<T, false>
significa que este parámetro es opcional y se puede omitir.CookieParam<T, true>
significa que este parámetro es obligatorio y no se puede omitir. Si no se proporciona, se devolverá un error;PathParam<T>
: un extractor que extrae los parámetros de ruta de la URL de solicitud. Si este parámetro no existe, la coincidencia de ruta no será exitosa, por lo que no se puede omitir en ningún caso;FormBody<T>
: un extractor que extrae información de los formularios enviados;JsonBody<T>
: un extractor que extrae información de cargas útiles con formato JSON enviadas en solicitudes.
#[endpoint]
Al generar documentación de OpenAPI, se debe utilizar la macro #[endpoint]
en lugar de la macro normal #[handler]
. En realidad, es una versión mejorada de la macro #[handler]
.
Puede obtener la información necesaria para generar documentación OpenAPI a partir de la firma de la función.
Para información que no sea conveniente proporcionar a través de la firma, se puede agregar directamente como atributo en la macro
#[endpoint]
. La información proporcionada de esta manera se fusionará con la información obtenida a través de la firma de la función. Si hay un conflicto, la información proporcionada en el atributo sobrescribirá la información proporcionada a través de la firma de la función.
Puede usar el atributo #[deprecated]
propio de Rust en funciones para marcarlo como obsoleto y lo hará reflejar la especificación OpenAPI generada. Solo parámetros tiene un atributo especial obsoleto para definirlos como obsoletos.
El atributo #[obsoleto]
admite agregar detalles adicionales, como un motivo o desde la versión, pero esto no se admite en API abierta. OpenAPI solo tiene un indicador booleano para determinar la desaprobación. Si bien está totalmente bien declararlo obsoleto con razón #[deprecated = "Hay una mejor manera de hacer esto"]
el motivo no se representaría en la especificación OpenAPI.
El comentario del documento en la función decorada se utilizará para description
y summary
de la ruta. La primera línea del comentario del documento se utilizará como summary
y el comentario completo del documento se utilizado como description
.
/// This is a summary of the operation
///
/// All lines of the doc comment will be included to operation description.
#[endpoint]
fn endpoint() {}
Parámetros
Genere [parámetros de ruta] path_params a partir de los campos de la estructura.
Esta es la implementación #[derive]
para el rasgo ToParameters
.
Normalmente, los parámetros de ruta deben definirse dentro de la sección #[salvo_oapi::endpoint(...parameters(...))]
para el punto final. Pero este rasgo elimina la necesidad de hacerlo cuando se usan struct
s para definir parámetros. Aún es necesario definir los parámetros de ruta [std::primitive
] y String
o los parámetros de ruta de estilo [tuple
] dentro de la sección parámetros(...)
si es necesario proporcionar una descripción u otra configuración que no sea la predeterminada.
Puede utilizar el atributo #[deprecated]
de Rust en el campo para marcarlo como está en desuso y se reflejará en la especificación OpenAPI generada.
El atributo #[deprecated]
admite agregar detalles adicionales como un motivo o desde la versión pero esto no es compatible con OpenAPI. OpenAPI solo tiene un indicador booleano para determinar la desaprobación. Si bien está totalmente bien declararlo obsoleto con razón #[deprecated = "Hay una mejor manera de hacer esto"]
el motivo no se representaría en la especificación OpenAPI.
El comentario del documento en los campos de estructura se utilizará como descripción de los parámetros generados.
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
/// Query todo items by name.
name: String
}
#[salvo(parameters(...))]
Parámetros Contenedores de Atributos Los siguienes atributos están disponibles para usar en el contenedor de atributos #[salvo(parameters(...))]
para la estructura derivando ToParameters
:
names(...)
Defina una lista de nombres separados por comas para los campos sin nombre de la estructura utilizada como parámetro de ruta. Solo soportado en estructuras sin nombre.style = ...
Define cómo se serializan todos los parámetros medianteParameterStyle
. Por defecto los valores se basan en el atributoparameter_in
.default_parameter_in = ...
= Define el valor predeterminado donde se utilizan los parámetros de este campo con un valor deparameter::ParameterIn
. Si no se proporciona este atributo, entonces el valor predeterminado proviene de la consulta.rename_all = ...
Se puede proporcionar como alternativa al atributorename_all
del serde. Proporciona efectivamente la misma funcionalidad.
Utilice names
para definir el nombre de un único argumento sin nombre.
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id")))]
struct Id(u64);
Use names
para definir múltiples argumentos sin nombre.
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id", "name")))]
struct IdAndName(u64, String);
#[salvo(parameter(...))]
Parámetros para Campos de Atributos Los siguientes atributos están disponibles para su uso en el #[salvo(parameter(...))]
en campos de estructura:
style = ...
Define cómo se serializa el parámetro medianteParameterStyle
. Los valores predeterminados se basan en el atributoparameter_in
.parameter_in = ...
= Define dónde se utilizan los parámetros de este campo con un valor deparameter::ParameterIn
. Si no se proporciona este atributo, entonces el valor predeterminado proviene de la consulta.explode
Define nuevo siparameter=value
Se crea un par para cada parámetro dentro deobject
oarray
.allow_reserved
Define si los caracteres reservados:/?#[]@!$&'()*+,;=
está permitido dentro del valor.example = ...
Puede ser una referencia de método ojson!(...)
. Ejemplo dado anulará cualquier ejemplo en el tipo de parámetro subyacente.value_type = ...
Se puede utilizar para anular el tipo predeterminado derivado del tipo de campo utilizado en la especificación OpenAPI. Esto es útil en casos en los que el tipo predeterminado no corresponde al tipo real, p. cuando Se utilizan tipos de terceros que no sonToSchema
ni [tiposprimitivos
][primitivos]. El valor puede ser cualquier tipo de Rust que normalmente podría usarse para serializar a JSON o un tipo personalizado comoObject
.Object
se representará como un objeto OpenAPI genérico.inline
Si se establece, el esquema para el tipo de este campo debe serToSchema
, y la definición del esquema estará incorporada.default = ...
Puede ser una referencia de método ojson!(...)
.format = ...
Puede ser una variante de la enumeraciónKnownFormat
o no un valor abierto como una cadena. Por defecto, el formato se deriva del tipo de propiedad. según las especificaciones de OpenApi.write_only
Define que la propiedad solo se usa en operaciones de escritura POST,PUT,PATCH pero no en GETread_only
Define que la propiedad solo se usa en operaciones de lectura GET pero no en POST,PUT,PATCHnullable
Define que la propiedad admite valores NULL (tenga en cuenta que esto es diferente a no obligatorio).required = ...
Se puede utilizar para imponer el estado requerido para el parámetro. [Ver reglas][derive@ToParameters#campo-nullabilidad-y-reglas-requeridas]rename = ...
Se puede proporcionar como alternativa al atributorename
del serde. Proporciona efectivamente la misma funcionalidad.multiple_of = ...
Se puede utilizar para definir el multiplicador de un valor. El valor se considera válido. La división dará como resultado un "número entero". El valor debe estar estrictamente por encima de0
.maximum = ...
Se puede utilizar para definir el límite superior inclusivo de un valornúmero
.minimum = ...
Se puede utilizar para definir un límite inferior inclusivo para un valornúmero
.exclusive_maximum = ...
Se puede utilizar para definir un límite superior exclusivo para un valornúmero
.exclusive_minimum = ...
Se puede utilizar para definir un límite inferior exclusivo para un valornúmero
.max_length = ...
Se puede utilizar para definir la longitud máxima para tipos decadena
.min_length = ...
Se puede utilizar para definir la longitud mínima para los tipos decadena
.patrón = ...
Se puede utilizar para definir una expresión regular válida en el dialecto ECMA-262 y el valor del campo debe coincidir.max_items = ...
Se puede utilizar para definir el número máximo de elementos permitidos para los campos dematriz
. El valor debe ser un número entero no negativo.min_items = ...
Se puede utilizar para definir los elementos mínimos permitidos para los campos dematriz
. El valor debe ser un número entero no negativo.with_schema = ...
Useschema
creado por la referencia de función proporcionada en lugar delesquema
derivado por defecto. La función debe coincidir confn() -> Into<RefOr<Schema>>
. Lo hace no acepta argumentos y debe devolver cualquier cosa que pueda convertirse enRefOr<Schema>
.additional_properties = ...
Se puede utilizar para definir tipos de formato libre para mapas comoHashMap
yBTreeMap
. El tipo de formato libre permite el uso de tipos arbitrarios dentro de los valores del mapa. Admite formatosadditional_properties
yadditional_properties = true
.
Anulación de campos y reglas requeridas
SSe aplican las mismas reglas de nulidad y estado requerido para los atributos de campo ToParameters
que para
ToSchema
atributos del campo. [Ver las reglas][derive@ToSchema#field-nullability-and-required-rules
].
#[serde(...)]
Soporte parcial de atributos ToParameters derive has partial support for serde attributes. These supported attributes will reflect to the generated OpenAPI doc. The following attributes are currently supported:
rename_all = "..."
Apoyado a nivel del contenedor.rename = "..."
Compatible sólo a nivel de campo.default
Soportado a nivel de contenedor y a nivel de campo según [atributos del servidor].skip_serializing_if = "..."
Compatible sólo a nivel de campo.with = ...
Compatible sólo a nivel de campo.skip_serializing = "..."
Compatible solo a nivel de campo o variante.skip_deserializing = "..."
Compatible solo a nivel de campo o variante.skip = "..."
Compatible sólo a nivel de campo.
Otros atributos serde
afectarán la serialización pero no se reflejarán en el documento OpenAPI generado.
Ejemplos
Demostrar el uso de ToParameters
con el atributo de contenedor #[salvo(parameters(...))]
para usarse como consulta de ruta e incluir un campo de consulta de esquema:
use serde::Deserialize;
use salvo_core::prelude::*;
use salvo_oapi::{ToParameters, ToSchema};
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
enum PetKind {
Dog,
Cat,
}
#[derive(Deserialize, ToParameters)]
struct PetQuery {
/// Name of pet
name: Option<String>,
/// Age of pet
age: Option<i32>,
/// Kind of pet
#[salvo(parameter(inline))]
kind: PetKind
}
#[salvo_oapi::endpoint(
parameters(PetQuery),
responses(
(status_code = 200, description = "success response")
)
)]
async fn get_pet(query: PetQuery) {
// ...
}
Sobrescribir String
con i64
usando atributos value_type
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = i64))]
id: String,
}
Sobrescribir String
con Object
usando atributos value_type
. Object
se verá como type: object
en la especificación de OpenAPI.
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Object))]
id: String,
}
Puede utilizar un tipo genérico para anular el tipo predeterminado del campo.
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Option<String>))]
id: String
}
Incluso puedes anular un [Vec
] con otro.
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Vec<i32>))]
id: Vec<String>
}
Podemos anular el valor con otro ToSchema
.
# use salvo_oapi::{ToParameters, ToSchema};
#[derive(ToSchema)]
struct Id {
value: i64,
}
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Id))]
id: String
}
Ejemplo con atributos de validación.
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Item {
#[salvo(parameter(maximum = 10, minimum = 5, multiple_of = 2.5))]
id: i32,
#[salvo(parameter(max_length = 10, min_length = 5, pattern = "[a-z]*"))]
value: String,
#[salvo(parameter(max_items = 5, min_items = 1))]
items: Vec<String>,
}
Utilice schema_with
para implementar manualmente el esquema de un campo.
# use salvo_oapi::schema::Object;
fn custom_type() -> Object {
Object::new()
.schema_type(salvo_oapi::SchemaType::String)
.format(salvo_oapi::SchemaFormat::Custom(
"email".to_string(),
))
.description("this is the description")
}
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Query {
#[salvo(parameter(schema_with = custom_type))]
email: String,
}
rename_all = ...
: admite una sintaxis similar aserde
para definir reglas para cambiar el nombre de los campos. Si se definen tanto#[serde(rename_all = "...")]
como#[salvo(schema) al mismo tiempo (rename_all = "..."))]
, entonces se prefiere#[serde(rename_all = "...")]
.symbol = ...
: una cadena literal utilizada para definir la ruta del nombre de la estructura en OpenAPI. Por ejemplo,#[salvo(schema(symbol = "path.to.Pet"))]
.default
: Se puede utilizar para completar los valores predeterminados en todos los campos utilizando la implementación predeterminada de la estructura.
Manejo de errores
Para aplicaciones generales, definiremos un tipo de error global (AppError) e implementaremos Writer
o Scribe
para AppError, de modo que los errores puedan enviarse al cliente como información de la página web.
Para OpenAPI, para lograr el mensaje de error necesario, también necesitamos implementar EndpointOutRegister
para este error:
use salvo::http::{StatusCode, StatusError};
use salvo::oapi::{self, EndpointOutRegister, ToSchema};
impl EndpointOutRegister for Error {
fn register(components: &mut oapi::Components, operation: &mut oapi::Operation) {
operation.responses.insert(
StatusCode::INTERNAL_SERVER_ERROR.as_str(),
oapi::Response::new("Internal server error").add_content("application/json", StatusError::to_schema(components)),
);
operation.responses.insert(
StatusCode::NOT_FOUND.as_str(),
oapi::Response::new("Not found").add_content("application/json", StatusError::to_schema(components)),
);
operation.responses.insert(
StatusCode::BAD_REQUEST.as_str(),
oapi::Response::new("Bad request").add_content("application/json", StatusError::to_schema(components)),
);
}
}
Este error define centralmente todos los mensajes de error que puede devolver toda la aplicación web. Sin embargo, en muchos casos, nuestro Handler
puede contener solo unos pocos tipos de error específicos. En este momento, status_codes
se puede utilizar para filtrar los información de tipo de error requerida:
#[endpoint(status_codes(201, 409))]
pub async fn create_todo(new_todo: JsonBody<Todo>) -> Result<StatusCode, Error> {
Ok(StatusCode::CREATED)
}