API REST desarrollada en Rust para la gestión de clínicas veterinarias, permitiendo el manejo de clínicas, clientes, mascotas e historias clínicas.
rocket: Framework web para Rustserde: Serialización/deserialización de datosuuid: Generación de identificadores únicoschrono: Manejo de fechasrocket_cors: Soporte para CORSlogyenv_logger: Sistema de logging
El proyecto sigue una arquitectura en capas:
-
Controllers: Manejo de endpoints HTTP y DTOs
- Clínicas
- Clientes
- Mascotas
- Historias Clínicas
-
Services: Lógica de negocio
- Validaciones
- Transformación de datos
- Coordinación entre repositorios
-
Repositories: Persistencia de datos
- Implementación en memoria
- Interfaces genéricas para futura extensibilidad
-
Models: Entidades del dominio
- Clínica
- Cliente
- Mascota
- Historia Clínica
- Entrada de Historia Clínica
- Mutex: Utilizado para el acceso seguro a los servicios compartidos
rocket::build()
.attach(make_cors())
.manage(Mutex::new(clinica_service))
.manage(Mutex::new(cliente_service))
.manage(Mutex::new(mascota_service))
.manage(Mutex::new(historia_clinica_service))
- Uso de traits para definir comportamientos de repositorios
- Implementaciones genéricas en servicios para permitir diferentes tipos de repositorios
pub struct ClienteService<T: ClienteRepository> {
repository: T,
}
impl<T: ClienteRepository> ClienteService<T> {
pub fn new(repository: T) -> Self {
Self { repository }
}
}
- Manejo de errores mediante
ResultyOption - Match expressions para control de flujo
match result {
Ok(cliente) => Ok(Json(cliente)),
Err(_) => Err(Status::NotFound),
}
- Clonación controlada de datos
- Referencias compartidas para lectura
- Referencias mutables para escritura
pub fn actualizar_clinica(
&mut self,
id: Uuid,
nombre: String,
direccion: String,
telefono: String,
correo: String,
) -> Result<Clinica, String> {
let clinica = self.repository.obtener(id)
.ok_or_else(|| "La clínica no existe".to_string())?;
let clinica_actualizada = Clinica {
id: clinica.id,
nombre,
direccion,
telefono,
correo,
};
self.repository.guardar(clinica_actualizada.clone())?;
Ok(clinica_actualizada)
}
- Macros de Rocket para definir rutas
- Atributos para derivar traits
#[derive(Debug, Deserialize)]
pub struct ClinicaCreateDto {
pub nombre: String,
pub direccion: String,
pub telefono: String,
pub correo: String,
}
#[put("/clinicas/<id>", data = "<clinica_dto>")]
pub async fn actualizar_clinica(
id: &str,
clinica_dto: Json<ClinicaCreateDto>,
service: &State<ClinicaServiceType>
) -> Result<Json<Clinica>, Status>
- Uso de
Resultpara manejo de errores - Propagación de errores con el operador
? - Conversión de errores entre tipos
let uuid = Uuid::parse_str(&id)
.map_err(|err| {
error!("Error parsing UUID '{}': {}", id, err);
Status::BadRequest
})?;
service.lock()
.map_err(|_| Status::InternalServerError)?
.obtener_cliente(uuid)
.map(|cliente| Json(cliente.clone()))
.ok_or(Status::NotFound)
-
CORS Configurado
- Soporte para múltiples orígenes
- Métodos HTTP permitidos configurables
- Headers personalizables
-
Logging Integrado
- Diferentes niveles de log
- Formato personalizable
- Útil para debugging y monitoreo
-
DTOs y Validación
- Separación entre modelos de dominio y DTOs
- Validación de UUIDs y datos de entrada
- Transformación controlada de datos
-
Gestión de Estado
- Estado compartido thread-safe
- Acceso controlado mediante Mutex
- Manejo de errores de concurrencia
Las anotaciones en Rust son metadatos que se aplican a módulos, tipos o funciones. Algunas importantes son:
- Derive Attributes:
#[derive(Debug, Deserialize)]
pub struct ClinicaCreateDto {
pub nombre: String,
pub direccion: String,
pub telefono: String,
pub correo: String,
}
#[derive(Debug)]: Implementa automáticamente la capacidad de depuración#[derive(Deserialize)]: Permite deserializar JSON a estructuras Rust- Estas anotaciones son macros que generan código automáticamente
- Endpoint Attributes:
#[put("/clinicas/<id>", data = "<clinica_dto>")]
pub async fn actualizar_clinica(
id: &str,
clinica_dto: Json<ClinicaCreateDto>,
service: &State<ClinicaServiceType>
) -> Result<Json<Clinica>, Status>
#[put("/clinicas/<id>")]: Define la ruta y método HTTPdata = "<clinica_dto>": Indica que el endpoint espera datos en el body
Cada endpoint sigue un patrón común:
- Declaración de Ruta:
#[post("/ruta/<parametro>")]- Función Asíncrona:
pub async fn nombre_funcion(
parametro: TipoParametro,
dto: Json<TipoDto>,
service: &State<TipoService>
) -> Result<Json<TipoRespuesta>, Status>- Manejo de Estado:
- Acceso al servicio mediante
&State<T> - Uso de Mutex para acceso seguro
#[get("/clientes")]
pub async fn listar_clientes(service: &State<ClienteServiceType>) -> Result<Json<Vec<Cliente>>, Status> {
let clientes = service.lock()
.map_err(|_| Status::InternalServerError)?
.listar_clientes()
.into_iter()
.cloned()
.collect();
Ok(Json(clientes))
}
- Respuestas:
- Uso de
Resultpara manejar éxito/error Json<T>para serializar respuestasStatuspara códigos HTTP
match result {
Ok(cliente) => Ok(Json(cliente)),
Err(_) => Err(Status::NotFound),
}
Los DTOs son estructuras que definen el formato de datos para las peticiones:
- Definición:
#[derive(Debug, Deserialize)]
pub struct MascotaCreateDto {
pub nombre: String,
pub especie: String,
pub raza: String,
pub fecha_nacimiento: Option<NaiveDate>,
pub id_cliente: String,
}
- Uso:
- Validación de datos de entrada
- Transformación a modelos de dominio
- Separación entre API y lógica de negocio