Files
charybdis/src/adapters/yaml/mod.rs
T
2026-05-12 17:06:43 +02:00

155 lines
4.5 KiB
Rust

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<EntityRepository>,
pub base_url: String,
cache: Arc<RwLock<Option<CachedResponse>>>,
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<EntityRepository>,
base_url: String,
) -> Result<(), Box<dyn std::error::Error>> {
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<YamlAdapterState>) -> 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<String>, State(state): State<YamlAdapterState>) -> 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()
}
}
}