diff --git a/veno-core/src/app.rs b/veno-core/src/app.rs index 60eb751..4ef5800 100644 --- a/veno-core/src/app.rs +++ b/veno-core/src/app.rs @@ -75,8 +75,8 @@ fn generate_notification(artifacts: &Vec<(&Artifact, Result>)>) - match result { Ok(Some(new_version)) => { let message = match &artifact.message_prefix { - Some(prefix) => create_custom_message(&prefix, &artifact.name, &new_version), - None => create_default_message(&artifact.name, &new_version), + Some(prefix) => create_custom_message(prefix, &artifact.name, new_version), + None => create_default_message(&artifact.name, new_version), }; messages.push(message); } diff --git a/veno-web/src/resources/v1/artifacts/handlers.rs b/veno-web/src/api/artifacts/handlers.rs similarity index 68% rename from veno-web/src/resources/v1/artifacts/handlers.rs rename to veno-web/src/api/artifacts/handlers.rs index 75772f4..300ed5e 100644 --- a/veno-web/src/resources/v1/artifacts/handlers.rs +++ b/veno-web/src/api/artifacts/handlers.rs @@ -1,17 +1,19 @@ use std::sync::Arc; -use crate::resources::errors::ResourceError; use axum::{ - extract::{Path, State}, + extract::{OriginalUri, Path, State}, http::StatusCode, response::IntoResponse, Json, }; use serde_json::json; use thiserror::Error; +use tracing::trace; use utoipa::OpenApi; use veno_core::app::AppState; +use crate::api::{errors::ApiError, version::ApiVersion}; + use super::{model::ArtifactResponse, service::check_all_artifacts}; #[derive(OpenApi)] @@ -24,13 +26,16 @@ pub struct V1ArtifactsApi; responses( (status= OK, description = "Returns a set of checked artifacts with its new versions if there are any.", body = ArtifactResponse), (status= OK, description = "Retursn a message if there are no new versions", body = serde_json::Value), - (status= INTERNAL_SERVER_ERROR, description = "If during the check a server error occurs", body = ResourceError) + (status= INTERNAL_SERVER_ERROR, description = "If during the check a server error occurs", body = ApiError) ) )] #[tracing::instrument(level = tracing::Level::TRACE, skip_all)] pub async fn check_versions( + version: ApiVersion, + original_uri: OriginalUri, State(app): State>, -) -> Result { +) -> Result { + trace!("Using API version: {}", version); let response = check_all_artifacts(&app).await; match response { Ok(Some(new_versions)) => return Ok(Json(new_versions).into_response()), @@ -40,9 +45,7 @@ pub async fn check_versions( ) .into_response()) } - Err(_err) => { - Err(ArtifactError::InternalServerError("/api/v1/artifacts/check".into()).into()) - } + Err(_err) => Err(ArtifactError::InternalServerError(original_uri.path()).into()), } } @@ -54,7 +57,11 @@ pub async fn check_versions( ) )] #[tracing::instrument(level = tracing::Level::TRACE, skip_all)] -pub async fn all_artifacts(State(app): State>) -> impl IntoResponse { +pub async fn all_artifacts( + version: ApiVersion, + State(app): State>, +) -> impl IntoResponse { + trace!("Using API version: {}", version); let artifacts: Vec = app .artifacts .iter() @@ -68,14 +75,17 @@ pub async fn all_artifacts(State(app): State>) -> impl IntoRespons path="/{artifact_id}", responses( (status= OK, description = "Get a specific artifact with id = artifact_id", body = ArtifactResponse), - (status= NOT_FOUND, description = "Returns not_found if the artifact_id had no match", body = ResourceError), + (status= NOT_FOUND, description = "Returns not_found if the artifact_id had no match", body = ApiError), ) )] #[tracing::instrument(level = tracing::Level::TRACE, skip_all)] pub async fn artifact_for_id( - Path(artifact_id): Path, + version: ApiVersion, + original_uri: OriginalUri, + Path((_version, artifact_id)): Path<(String, String)>, State(app): State>, -) -> Result { +) -> Result { + trace!("Using API version: {}", version); let artifact = app .artifacts .iter() @@ -86,35 +96,33 @@ pub async fn artifact_for_id( let response_boddy = ArtifactResponse::from(artifact.clone()); Ok((StatusCode::OK, Json(response_boddy)).into_response()) } - None => Err(ArtifactError::NotFoundWithParam { + None => Err(ArtifactError::NotFoundParam { param: artifact_id.clone(), - path: format!("/api/v1/artifacts/{artifact_id}"), + path: original_uri.path(), } .into()), } } #[derive(Debug, Error)] -pub enum ArtifactError { +pub enum ArtifactError<'a> { #[error("The artifact with the id={param} was not found.")] - NotFoundWithParam { param: String, path: String }, + NotFoundParam { param: String, path: &'a str }, #[error("There was an internal server error. Please try again later.")] - InternalServerError(String), + InternalServerError(&'a str), } -impl From for ResourceError { +impl<'a> From> for ApiError { fn from(err: ArtifactError) -> Self { let message = err.to_string(); match err { - ArtifactError::NotFoundWithParam { param: _, path } => { - ResourceError::new(StatusCode::NOT_FOUND) - .message(message) - .path(format!("{}", path).as_str()) - } + ArtifactError::NotFoundParam { param: _, path } => ApiError::new(StatusCode::NOT_FOUND) + .message(message) + .path(path), ArtifactError::InternalServerError(path) => { - ResourceError::new(StatusCode::INTERNAL_SERVER_ERROR) + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) .message(message) - .path(format!("{}", path).as_str()) + .path(path) } } } diff --git a/veno-web/src/resources/v1/artifacts/mod.rs b/veno-web/src/api/artifacts/mod.rs similarity index 100% rename from veno-web/src/resources/v1/artifacts/mod.rs rename to veno-web/src/api/artifacts/mod.rs diff --git a/veno-web/src/resources/v1/artifacts/model.rs b/veno-web/src/api/artifacts/model.rs similarity index 100% rename from veno-web/src/resources/v1/artifacts/model.rs rename to veno-web/src/api/artifacts/model.rs diff --git a/veno-web/src/resources/v1/artifacts/routes.rs b/veno-web/src/api/artifacts/routes.rs similarity index 100% rename from veno-web/src/resources/v1/artifacts/routes.rs rename to veno-web/src/api/artifacts/routes.rs diff --git a/veno-web/src/resources/v1/artifacts/service.rs b/veno-web/src/api/artifacts/service.rs similarity index 100% rename from veno-web/src/resources/v1/artifacts/service.rs rename to veno-web/src/api/artifacts/service.rs diff --git a/veno-web/src/resources/errors.rs b/veno-web/src/api/errors.rs similarity index 91% rename from veno-web/src/resources/errors.rs rename to veno-web/src/api/errors.rs index e33720b..8429e02 100644 --- a/veno-web/src/resources/errors.rs +++ b/veno-web/src/api/errors.rs @@ -4,7 +4,7 @@ use tracing::error; use utoipa::ToSchema; #[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)] -pub struct ResourceError { +pub struct ApiError { pub code: u16, pub kind: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -13,7 +13,7 @@ pub struct ResourceError { pub path: Option, } -impl ResourceError { +impl ApiError { pub fn new(status_code: StatusCode) -> Self { let kind = match status_code.canonical_reason() { Some(reason) => reason.to_owned(), @@ -37,13 +37,13 @@ impl ResourceError { } } -impl From for ResourceError { +impl From for ApiError { fn from(status_code: StatusCode) -> Self { Self::new(status_code) } } -impl IntoResponse for ResourceError { +impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { error!("Error response: {:?}", self); let status_code = diff --git a/veno-web/src/resources/mod.rs b/veno-web/src/api/mod.rs similarity index 77% rename from veno-web/src/resources/mod.rs rename to veno-web/src/api/mod.rs index b002deb..da3a43c 100644 --- a/veno-web/src/resources/mod.rs +++ b/veno-web/src/api/mod.rs @@ -1,15 +1,21 @@ -use std::sync::Arc; +mod artifacts; +mod errors; +mod notifiers; +mod openapi; +mod version; use anyhow::Result; -use errors::ResourceError; +use artifacts::routes::artifacts_routes; +use errors::ApiError; +use notifiers::routes::notifiers_routes; use openapi::ApiDoc; use serde_json::json; +use std::sync::Arc; use thiserror::Error; use tower_http::cors::{Any, CorsLayer}; use tracing::trace; use utoipa::OpenApi; use utoipa_redoc::{Redoc, Servable}; -use v1::v1_routes; use veno_core::app::AppState; use axum::{ @@ -25,15 +31,12 @@ use axum::{ Json, Router, }; -mod errors; -mod openapi; -mod v1; - pub fn serve_api(app: Arc) -> Router { Router::new() .merge(Redoc::with_url("/redoc", ApiDoc::openapi())) .route("/health", get(health_handler)) - .nest("/api/v1", v1_routes()) + .nest("/api/{version}/artifacts", artifacts_routes()) + .nest("/api/{version}/notifiers", notifiers_routes()) .fallback(error_404_handler) .layer(cors_layer()) .layer(middleware::from_fn(logging_middleware)) @@ -52,12 +55,12 @@ fn cors_layer() -> CorsLayer { .allow_methods([Method::GET, Method::POST]) .allow_headers([ACCEPT, CONTENT_TYPE]) } -pub async fn error_404_handler(request: Request) -> ResourceError { +pub async fn error_404_handler(request: Request) -> ApiError { tracing::error!("route not found: {:?}", request); RootError::NotFound(format!("{:?}", request.uri())).into() } -pub async fn health_handler() -> Result { +pub async fn health_handler() -> Result { Ok(Json(json!({"status": "healthy"}))) } @@ -67,11 +70,11 @@ enum RootError { NotFound(String), } -impl From for ResourceError { +impl From for ApiError { fn from(value: RootError) -> Self { let message = value.to_string(); match value { - RootError::NotFound(path) => ResourceError::new(StatusCode::NOT_FOUND) + RootError::NotFound(path) => ApiError::new(StatusCode::NOT_FOUND) .message(message) .path(&path), } diff --git a/veno-web/src/resources/v1/notifiers/handlers.rs b/veno-web/src/api/notifiers/handlers.rs similarity index 62% rename from veno-web/src/resources/v1/notifiers/handlers.rs rename to veno-web/src/api/notifiers/handlers.rs index cbe7193..10cc01d 100644 --- a/veno-web/src/resources/v1/notifiers/handlers.rs +++ b/veno-web/src/api/notifiers/handlers.rs @@ -1,16 +1,17 @@ use std::sync::Arc; use axum::{ - extract::{Path, State}, + extract::{OriginalUri, Path, State}, http::StatusCode, response::IntoResponse, Json, }; use thiserror::Error; +use tracing::trace; use utoipa::OpenApi; use veno_core::app::AppState; -use crate::resources::errors::ResourceError; +use crate::api::{errors::ApiError, version::ApiVersion}; use super::model::NotifierResponse; @@ -25,7 +26,11 @@ pub struct V1NotifiersApi; (status= OK, description = "Get all notifier configuration", body = Vec) ) )] -pub async fn all_notifiers(State(app): State>) -> impl IntoResponse { +pub async fn all_notifiers( + version: ApiVersion, + State(app): State>, +) -> impl IntoResponse { + trace!("Using API version: {}", version); let notifiers: Vec = app .notifiers .iter() @@ -42,9 +47,12 @@ pub async fn all_notifiers(State(app): State>) -> impl IntoRespons ) )] pub async fn notifier_for_id( - Path(notifier_id): Path, + version: ApiVersion, + original_uri: OriginalUri, + Path((_version, notifier_id)): Path<(String, String)>, State(app): State>, -) -> Result { +) -> Result { + trace!("Using API version: {}", version); let notifier = app .notifiers .iter() @@ -55,9 +63,9 @@ pub async fn notifier_for_id( let response_boddy = NotifierResponse::from(notifier.clone()); Ok((StatusCode::OK, Json(response_boddy)).into_response()) } - None => Err(NotifierError::NotFoundWithParam { + None => Err(NotifierError::NotFoundParam { param: notifier_id.clone(), - path: format!("/api/v1/notifiers/{notifier_id}"), + path: original_uri.path(), } .into()), } @@ -70,26 +78,25 @@ pub async fn notifier_for_id( (status= OK, description = "Runs all notifiers") ) )] -pub async fn notify(State(app): State>) -> impl IntoResponse { +pub async fn notify(version: ApiVersion, State(app): State>) -> impl IntoResponse { + trace!("Using API version: {}", version); app.notify().await; StatusCode::OK } #[derive(Debug, Error)] -pub enum NotifierError { +pub enum NotifierError<'a> { #[error("The Notifier with the id={param} was not found.")] - NotFoundWithParam { param: String, path: String }, + NotFoundParam { param: String, path: &'a str }, } -impl From for ResourceError { +impl<'a> From> for ApiError { fn from(err: NotifierError) -> Self { let message = err.to_string(); match err { - NotifierError::NotFoundWithParam { param: _, path } => { - ResourceError::new(StatusCode::NOT_FOUND) - .message(message) - .path(format!("{}", path).as_str()) - } + NotifierError::NotFoundParam { param: _, path } => ApiError::new(StatusCode::NOT_FOUND) + .message(message) + .path(path), } } } diff --git a/veno-web/src/resources/v1/notifiers/mod.rs b/veno-web/src/api/notifiers/mod.rs similarity index 100% rename from veno-web/src/resources/v1/notifiers/mod.rs rename to veno-web/src/api/notifiers/mod.rs diff --git a/veno-web/src/resources/v1/notifiers/model.rs b/veno-web/src/api/notifiers/model.rs similarity index 100% rename from veno-web/src/resources/v1/notifiers/model.rs rename to veno-web/src/api/notifiers/model.rs diff --git a/veno-web/src/resources/v1/notifiers/routes.rs b/veno-web/src/api/notifiers/routes.rs similarity index 100% rename from veno-web/src/resources/v1/notifiers/routes.rs rename to veno-web/src/api/notifiers/routes.rs diff --git a/veno-web/src/resources/openapi.rs b/veno-web/src/api/openapi.rs similarity index 62% rename from veno-web/src/resources/openapi.rs rename to veno-web/src/api/openapi.rs index 6da82e4..a8c7c38 100644 --- a/veno-web/src/resources/openapi.rs +++ b/veno-web/src/api/openapi.rs @@ -1,7 +1,7 @@ -use crate::resources::v1::artifacts::handlers::V1ArtifactsApi; -use crate::resources::v1::notifiers::handlers::V1NotifiersApi; use utoipa::OpenApi; +use crate::api::{artifacts::handlers::V1ArtifactsApi, notifiers::handlers::V1NotifiersApi}; + #[derive(OpenApi)] #[openapi( nest( diff --git a/veno-web/src/api/version.rs b/veno-web/src/api/version.rs new file mode 100644 index 0000000..d8fc5fe --- /dev/null +++ b/veno-web/src/api/version.rs @@ -0,0 +1,100 @@ +use std::collections::HashMap; + +use axum::{ + extract::{FromRequestParts, Path}, + http::{request::Parts, StatusCode}, + RequestPartsExt, +}; + +use thiserror::Error; + +use super::errors::ApiError; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ApiVersion { + V1, +} + +impl ApiVersion { + pub fn parse_version(version: &str) -> Result { + match version.parse() { + Ok(version) => Ok(version), + Err(_) => Err(ApiVersionError::InvalidVersion { + version: version.to_owned(), + } + .into()), + } + } +} + +impl std::str::FromStr for ApiVersion { + type Err = ApiError; + fn from_str(s: &str) -> Result { + match s { + "v1" => Ok(Self::V1), + _ => Err(ApiVersionError::VersionParseError.into()), + } + } +} + +impl std::fmt::Display for ApiVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v = match self { + Self::V1 => "v1", + }; + write!(f, "{}", v) + } +} + +impl FromRequestParts for ApiVersion +where + S: Send + Sync, +{ + type Rejection = ApiError; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let params: Path> = parts + .extract() + .await + .map_err(|_| ApiVersionError::VersionExtractError)?; + + let version = params + .get("version") + // This does not trigger since the 404_handler fallback catches before this extractor gets called + .ok_or(ApiVersionError::ParameterMissing)?; + + ApiVersion::parse_version(version) + } +} + +#[derive(Debug, Error)] +pub enum ApiVersionError { + #[error("The version parameter is invalid: '{version}'")] + InvalidVersion { version: String }, + #[error("Parameter is missing: 'version'")] + ParameterMissing, + #[error("Could not extract api version")] + VersionExtractError, + #[error("Could not parse version from request")] + VersionParseError, +} + +impl From for ApiError { + fn from(err: ApiVersionError) -> Self { + let message = err.to_string(); + match err { + ApiVersionError::InvalidVersion { version: _version } => { + ApiError::new(StatusCode::BAD_REQUEST).message(message) + } + ApiVersionError::VersionExtractError => { + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR).message(message) + } + ApiVersionError::ParameterMissing => { + ApiError::new(StatusCode::BAD_REQUEST).message(message) + } + ApiVersionError::VersionParseError => { + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR).message(message) + } + } + } +} diff --git a/veno-web/src/main.rs b/veno-web/src/main.rs index fca3053..e5d70d6 100644 --- a/veno-web/src/main.rs +++ b/veno-web/src/main.rs @@ -1,3 +1,6 @@ +mod api; +mod server; + use std::{env, sync::Arc}; use anyhow::Result; @@ -6,9 +9,6 @@ use veno_core::app::AppState; use clap::Parser; -mod resources; -mod server; - #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { diff --git a/veno-web/src/resources/v1/mod.rs b/veno-web/src/resources/v1/mod.rs deleted file mode 100644 index 17cc625..0000000 --- a/veno-web/src/resources/v1/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::sync::Arc; - -use artifacts::routes::artifacts_routes; -use axum::Router; -use notifiers::routes::notifiers_routes; -use veno_core::app::AppState; - -pub mod artifacts; -pub mod notifiers; - -pub fn v1_routes() -> Router> { - Router::new() - .nest("/artifacts", artifacts_routes()) - .nest("/notifiers", notifiers_routes()) -} diff --git a/veno-web/src/server.rs b/veno-web/src/server.rs index 63a231c..fbd07e9 100644 --- a/veno-web/src/server.rs +++ b/veno-web/src/server.rs @@ -8,7 +8,7 @@ use tokio::signal::{ use tracing::info; use veno_core::app::AppState; -use crate::resources::serve_api; +use crate::api::serve_api; pub async fn start(app: Arc) -> Result<()> { info!("Starting server...");