athena_rs 3.26.1

Hyper performant polyglot Database driver
Documentation
use actix_web::{
    HttpRequest, Responder, delete, get, patch, post,
    web::{self, Data, Json, Path},
};
use serde::Deserialize;
use serde_json::{Value, json};

use crate::AppState;
use crate::api::auth::authorize_static_admin_key;
use crate::api::response::{api_created, api_success, bad_request, not_found};
use crate::data::service_routes::{
    PatchPublicServiceRouteParams, SavePublicServiceRouteParams, create_public_service_route,
    get_public_service_route_by_key, list_public_service_routes, patch_public_service_route,
    soft_delete_public_service_route,
};

use super::{
    client_catalog_pool, database_error_response, normalize_public_route_target_url,
    normalize_route_key,
};

const SERVICE_ROUTE_LOCAL_PATH: &str = "local_path";
const SERVICE_ROUTE_HTTP_UPSTREAM: &str = "http_upstream";

#[derive(Debug, Deserialize)]
struct SavePublicServiceRouteRequest {
    route_key: String,
    service_key: String,
    target_kind: String,
    #[serde(default)]
    local_path_prefix: Option<String>,
    #[serde(default)]
    target_url: Option<String>,
    #[serde(default = "default_true")]
    is_active: bool,
    #[serde(default)]
    metadata: Option<Value>,
}

#[derive(Debug, Deserialize)]
struct PatchPublicServiceRouteRequest {
    #[serde(default)]
    target_kind: Option<String>,
    #[serde(default)]
    local_path_prefix: Option<String>,
    #[serde(default)]
    target_url: Option<String>,
    #[serde(default)]
    is_active: Option<bool>,
    #[serde(default)]
    metadata: Option<Value>,
}

fn default_true() -> bool {
    true
}

fn normalize_service_key(service_key: &str) -> Result<String, actix_web::HttpResponse> {
    let normalized = service_key.trim().to_ascii_lowercase();
    let canonical = match normalized.as_str() {
        "auth" => "auth",
        "storage" => "storage",
        "db" => "db",
        "realtime" | "real-time" => "realtime",
        _ => {
            return Err(bad_request(
                "Invalid service_key",
                "service_key must be one of: auth, storage, db, realtime.",
            ));
        }
    };

    Ok(canonical.to_string())
}

fn normalize_target_kind(target_kind: &str) -> Result<String, actix_web::HttpResponse> {
    let normalized = target_kind.trim().to_ascii_lowercase();
    match normalized.as_str() {
        SERVICE_ROUTE_LOCAL_PATH | SERVICE_ROUTE_HTTP_UPSTREAM => Ok(normalized),
        _ => Err(bad_request(
            "Invalid target_kind",
            "target_kind must be one of: local_path, http_upstream.",
        )),
    }
}

fn normalize_local_path_prefix(
    value: Option<&str>,
) -> Result<Option<String>, actix_web::HttpResponse> {
    let Some(raw) = value else {
        return Ok(None);
    };
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(bad_request(
            "Invalid local_path_prefix",
            "local_path_prefix must not be empty when provided.",
        ));
    }
    if !trimmed.starts_with('/') {
        return Err(bad_request(
            "Invalid local_path_prefix",
            "local_path_prefix must start with '/'.",
        ));
    }
    if trimmed.contains('?') || trimmed.contains('#') {
        return Err(bad_request(
            "Invalid local_path_prefix",
            "local_path_prefix must be a path prefix without query parameters or fragments.",
        ));
    }

    if trimmed == "/" {
        return Ok(Some("/".to_string()));
    }

    Ok(Some(trimmed.trim_end_matches('/').to_string()))
}

fn validate_target_fields(
    target_kind: &str,
    local_path_prefix: Option<&str>,
    target_url: Option<&str>,
) -> Result<(), actix_web::HttpResponse> {
    match target_kind {
        SERVICE_ROUTE_LOCAL_PATH if local_path_prefix.is_none() => Err(bad_request(
            "Missing local_path_prefix",
            "local_path_prefix is required when target_kind is local_path.",
        )),
        SERVICE_ROUTE_HTTP_UPSTREAM if target_url.is_none() => Err(bad_request(
            "Missing target_url",
            "target_url is required when target_kind is http_upstream.",
        )),
        _ => Ok(()),
    }
}

#[get("/admin/service-routes")]
async fn admin_list_public_service_routes(
    req: HttpRequest,
    app_state: Data<AppState>,
) -> impl Responder {
    if let Err(resp) = authorize_static_admin_key(&req) {
        return resp;
    }

    let pool = match client_catalog_pool(app_state.get_ref()) {
        Ok(pool) => pool,
        Err(resp) => return resp,
    };

    match list_public_service_routes(&pool).await {
        Ok(routes) => api_success("Listed service routes", json!({ "routes": routes })),
        Err(err) => database_error_response("Failed to list service routes", err),
    }
}

#[post("/admin/service-routes")]
async fn admin_create_public_service_route(
    req: HttpRequest,
    app_state: Data<AppState>,
    body: Json<SavePublicServiceRouteRequest>,
) -> impl Responder {
    if let Err(resp) = authorize_static_admin_key(&req) {
        return resp;
    }

    let pool = match client_catalog_pool(app_state.get_ref()) {
        Ok(pool) => pool,
        Err(resp) => return resp,
    };

    let route_key = match normalize_route_key(&body.route_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let service_key = match normalize_service_key(&body.service_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let target_kind = match normalize_target_kind(&body.target_kind) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let local_path_prefix = match normalize_local_path_prefix(body.local_path_prefix.as_deref()) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let target_url = match normalize_public_route_target_url(body.target_url.as_deref()) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    if let Err(resp) = validate_target_fields(
        &target_kind,
        local_path_prefix.as_deref(),
        target_url.as_deref(),
    ) {
        return resp;
    }

    let params = SavePublicServiceRouteParams {
        route_key,
        service_key,
        target_kind,
        local_path_prefix,
        target_url,
        is_active: body.is_active,
        metadata: body.metadata.clone().unwrap_or_else(|| json!({})),
    };

    match create_public_service_route(&pool, params).await {
        Ok(route) => api_created("Created service route", json!({ "route": route })),
        Err(err) => database_error_response("Failed to create service route", err),
    }
}

#[patch("/admin/service-routes/{route_key}/{service_key}")]
async fn admin_patch_public_service_route(
    req: HttpRequest,
    app_state: Data<AppState>,
    path: Path<(String, String)>,
    body: Json<PatchPublicServiceRouteRequest>,
) -> impl Responder {
    if let Err(resp) = authorize_static_admin_key(&req) {
        return resp;
    }

    let pool = match client_catalog_pool(app_state.get_ref()) {
        Ok(pool) => pool,
        Err(resp) => return resp,
    };

    let (raw_route_key, raw_service_key) = path.into_inner();
    let route_key = match normalize_route_key(&raw_route_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let service_key = match normalize_service_key(&raw_service_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };

    let Some(existing) =
        (match get_public_service_route_by_key(&pool, &route_key, &service_key).await {
            Ok(route) => route,
            Err(err) => return database_error_response("Failed to load service route", err),
        })
    else {
        return not_found(
            "Service route not found",
            format!(
                "No service route exists for route_key '{}' and service_key '{}'.",
                route_key, service_key
            ),
        );
    };

    let normalized_target_kind = match body.target_kind.as_deref() {
        Some(value) => match normalize_target_kind(value) {
            Ok(kind) => Some(kind),
            Err(resp) => return resp,
        },
        None => None,
    };
    let normalized_local_path_prefix =
        match normalize_local_path_prefix(body.local_path_prefix.as_deref()) {
            Ok(value) => value,
            Err(resp) => return resp,
        };
    let normalized_target_url = match normalize_public_route_target_url(body.target_url.as_deref())
    {
        Ok(value) => value,
        Err(resp) => return resp,
    };

    let next_target_kind = normalized_target_kind
        .clone()
        .unwrap_or_else(|| existing.target_kind.clone());
    let next_local_path_prefix = normalized_local_path_prefix
        .clone()
        .or_else(|| existing.local_path_prefix.clone());
    let next_target_url = normalized_target_url
        .clone()
        .or_else(|| existing.target_url.clone());
    if let Err(resp) = validate_target_fields(
        &next_target_kind,
        next_local_path_prefix.as_deref(),
        next_target_url.as_deref(),
    ) {
        return resp;
    }

    let patch = PatchPublicServiceRouteParams {
        target_kind: normalized_target_kind,
        local_path_prefix: normalized_local_path_prefix,
        target_url: normalized_target_url,
        is_active: body.is_active,
        metadata: body.metadata.clone(),
    };

    match patch_public_service_route(&pool, &route_key, &service_key, patch).await {
        Ok(Some(route)) => api_success("Updated service route", json!({ "route": route })),
        Ok(None) => not_found(
            "Service route not found",
            format!(
                "No service route exists for route_key '{}' and service_key '{}'.",
                route_key, service_key
            ),
        ),
        Err(err) => database_error_response("Failed to update service route", err),
    }
}

#[delete("/admin/service-routes/{route_key}/{service_key}")]
async fn admin_delete_public_service_route(
    req: HttpRequest,
    app_state: Data<AppState>,
    path: Path<(String, String)>,
) -> impl Responder {
    if let Err(resp) = authorize_static_admin_key(&req) {
        return resp;
    }

    let pool = match client_catalog_pool(app_state.get_ref()) {
        Ok(pool) => pool,
        Err(resp) => return resp,
    };

    let (raw_route_key, raw_service_key) = path.into_inner();
    let route_key = match normalize_route_key(&raw_route_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };
    let service_key = match normalize_service_key(&raw_service_key) {
        Ok(value) => value,
        Err(resp) => return resp,
    };

    match soft_delete_public_service_route(&pool, &route_key, &service_key).await {
        Ok(Some(route)) => api_success("Deleted service route", json!({ "route": route })),
        Ok(None) => not_found(
            "Service route not found",
            format!(
                "No service route exists for route_key '{}' and service_key '{}'.",
                route_key, service_key
            ),
        ),
        Err(err) => database_error_response("Failed to delete service route", err),
    }
}

pub(super) fn services(cfg: &mut web::ServiceConfig) {
    cfg.service(admin_list_public_service_routes)
        .service(admin_create_public_service_route)
        .service(admin_patch_public_service_route)
        .service(admin_delete_public_service_route);
}