//! HTTP handler functions for the CMS REST API. //! //! Each handler takes a service trait (via generics or trait objects) and //! returns domain-level results. These functions are agnostic about the //! HTTP framework — callers (e.g. hyper/Axum routes) are responsible for //! mapping `Result` into HTTP responses with appropriate status codes. //! //! ## Handlers //! - `handle_get_settings` — GET /settings → full settings response //! - `handle_update_settings` — PUT /settings → partial update + full response //! - `handle_list_memories` — GET /memories → list of memory summaries //! - `handle_create_memory` — POST /memories → create/update memory response //! //! ## Design //! Handlers are pure Rust functions with no dependency on the HTTP framework. //! They receive service trait objects (`&S` or `&M`) and return `Result`. //! The caller (e.g. a hyper `Service`) is responsible for serialising the //! response and setting HTTP status codes. use anyhow::{Context, Result}; use tracing::instrument; use crate::domain::memory::Memory; use crate::domain::service::{MemoryService, SettingsService}; use crate::domain::settings::Settings; use super::dto::{MemoryCreateRequest, MemoryResponse, SettingsResponse, SettingsUpdateRequest}; /// Handle `GET /settings` /// /// Returns the current settings as a `SettingsResponse`. /// /// Flow: load settings from service → convert to DTO → return. #[instrument(skip(service))] pub fn handle_get_settings(service: &S) -> Result { tracing::debug!("handling GET /settings"); let settings = service.load_settings().context("failed to load settings")?; Ok(SettingsResponse::from(settings)) } /// Handle `PUT /settings` /// /// Applies a partial update from `req` to the current settings, persists /// the result, and returns the updated `SettingsResponse`. /// /// Flow: load current settings → apply each optional field → save → return DTO. /// /// ## Validation /// - `internet_mode` must be "Off", "ReadOnly", or "Full" (case-sensitive). #[instrument(skip(service))] pub fn handle_update_settings( service: &S, req: SettingsUpdateRequest, ) -> Result { tracing::debug!("handling PUT /settings"); // Load current settings as baseline for partial update let mut settings: Settings = service .load_settings() .context("failed to load current settings for update")?; // Apply each optional field from the request (None = skip, Some = overwrite) if let Some(val) = req.internet_mode { settings.internet_mode = match val.as_str() { "Off" => crate::domain::settings::InternetMode::Off, "ReadOnly" => crate::domain::settings::InternetMode::ReadOnly, "Full" => crate::domain::settings::InternetMode::Full, _ => { return Err(anyhow::anyhow!( "invalid internet_mode '{}'; expected Off, ReadOnly, or Full", val )); } }; } if let Some(val) = req.provider { settings.provider = val; } if let Some(val) = req.model { settings.model = val; } if let Some(val) = req.api_keys { settings.api_keys = val; } if let Some(val) = req.max_tokens { settings.max_tokens = val; } if let Some(val) = req.temperature { settings.temperature = val; } if let Some(val) = req.review_max_lessons_per_run { settings.review_max_lessons_per_run = val; } if let Some(val) = req.adaptive_review_max_skip { settings.adaptive_review_max_skip = val; } if let Some(val) = req.verify_command { settings.verify_command = val; } if let Some(val) = req.verify_timeout_ms { settings.verify_timeout_ms = val; } if let Some(val) = req.workflow_max_concurrency { settings.workflow_max_concurrency = val; } if let Some(val) = req.review_enabled { settings.flags.review_enabled = val; } if let Some(val) = req.session_archive_enabled { settings.flags.session_archive_enabled = val; } if let Some(val) = req.lsp_auto_provision { settings.flags.lsp_auto_provision = val; } if let Some(val) = req.lsp_languages { settings.lsp_languages = val; } if let Some(val) = req.hive_mind_node_timeout_ms { settings.hive_mind_node_timeout_ms = val; } service .save_settings(&settings) .context("failed to save updated settings")?; Ok(SettingsResponse::from(settings)) } /// Handle `GET /memories` /// /// Lists all memory slugs, returning summary responses for each. /// Full content is not loaded — callers who need full content should /// use a dedicated endpoint. /// /// Flow: list slugs from service → map each to minimal MemoryResponse → return. #[instrument(skip(service))] pub fn handle_list_memories(service: &M) -> Result> { tracing::debug!("handling GET /memories"); let slugs = service.list_memories().context("failed to list memories")?; // We can't load individual memories without a load_memory method on the // service. For now, list returns summary info; callers who need full // content use a separate endpoint. Return minimal responses keyed by slug. let responses: Vec = slugs .into_iter() .map(|slug| MemoryResponse { name: slug.clone(), description: String::new(), content: String::new(), kind: String::new(), created_at: 0, updated_at: 0, outcome: None, lifecycle: String::new(), scope: None, before_snippet: None, after_snippet: None, provenances: Vec::new(), }) .collect(); Ok(responses) } /// Handle `POST /memories` /// /// Creates or updates a memory from the request body. /// /// Flow: build Memory from request DTO → save via service → return MemoryResponse. /// /// ## Defaults /// - `kind` defaults to "reference" if not specified /// - `lifecycle` defaults to "new" if not specified #[instrument(skip(service), fields(name = %req.name))] pub fn handle_create_memory( service: &M, req: MemoryCreateRequest, ) -> Result { tracing::debug!("handling POST /memories for '{}'", req.name); let now = chrono::Utc::now().timestamp(); let memory = Memory { name: req.name, description: req.description, content: req.content, kind: req.kind.unwrap_or_else(|| "reference".to_string()), created_at: now, updated_at: now, outcome: req.outcome, lifecycle: req.lifecycle.unwrap_or_else(|| "new".to_string()), scope: req.scope, before_snippet: req.before_snippet, after_snippet: req.after_snippet, provenances: req.provenances.unwrap_or_default(), }; service .save_memory(&memory) .context("failed to save memory")?; Ok(MemoryResponse::from(memory)) }