use crate::database::EntityRepository; use axum::{ Router, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::get, }; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; use tracing::{debug, error, info}; mod backstage; struct CachedResponse { data: String, cached_at: Instant, } /// State shared across YAML adapter handlers #[derive(Clone)] pub struct YamlAdapterState { pub repository: Arc, pub base_url: String, cache: Arc>>, cache_ttl: Duration, } impl YamlAdapterState { /// Invalidate the locations cache (for event-based invalidation on entity changes) pub async fn invalidate_cache(&self) { let mut cache = self.cache.write().await; *cache = None; } } /// Start the YAML adapter HTTP server pub async fn start_yaml_adapter( host: String, port: u16, repository: Arc, base_url: String, ) -> Result<(), Box> { let state = YamlAdapterState { repository, base_url, cache: Arc::new(RwLock::new(None)), cache_ttl: Duration::from_secs(30), }; let app = Router::new() .route("/yaml/locations", get(get_locations)) .route("/yaml/entities/:id", get(get_entity)) .with_state(state); let addr = format!("{}:{}", host, port); info!("Starting YAML adapter HTTP server on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await?; axum::serve(listener, app).await?; Ok(()) } /// GET /yaml/locations /// Returns a Backstage Location entity listing all entities in Charybdis async fn get_locations(State(state): State) -> Response { info!("Received GET /yaml/locations request"); // Check cache first { let cache = state.cache.read().await; if let Some(ref cached) = *cache { if cached.cached_at.elapsed() < state.cache_ttl { debug!("Serving /yaml/locations from cache"); return ( StatusCode::OK, [("content-type", "text/yaml; charset=utf-8")], cached.data.clone(), ) .into_response(); } } } // Cache miss or expired — fetch from database match state.repository.list_all().await { Ok(entities) => { let location_yaml = backstage::create_location_yaml(&entities, &state.base_url); // Update cache { let mut cache = state.cache.write().await; *cache = Some(CachedResponse { data: location_yaml.clone(), cached_at: Instant::now(), }); } ( StatusCode::OK, [("content-type", "text/yaml; charset=utf-8")], location_yaml, ) .into_response() } Err(e) => { error!(error = %e, "Failed to list entities for location"); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve entities", ) .into_response() } } } /// GET /yaml/entities/:id /// Returns a specific entity in Backstage YAML format async fn get_entity(Path(id): Path, State(state): State) -> Response { info!(entity_id = %id, "Received GET /yaml/entities/:id request"); match state.repository.get_by_id(&id).await { Ok(Some(entity)) => match backstage::entity_to_yaml(&entity) { Ok(yaml) => ( StatusCode::OK, [("content-type", "text/yaml; charset=utf-8")], yaml, ) .into_response(), Err(e) => { error!(entity_id = %id, error = %e, "Failed to convert entity to YAML"); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to convert entity to YAML", ) .into_response() } }, Ok(None) => { info!(entity_id = %id, "Entity not found"); (StatusCode::NOT_FOUND, "Entity not found").into_response() } Err(e) => { error!(entity_id = %id, error = %e, "Failed to retrieve entity"); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve entity", ) .into_response() } } }