Add the cast-allow block to zesdex-entities/src/lib.rs and zesdex-utils/src/lib.rs (which lacked it), then remove from 65 sub-files across all 8 crates. Build and all 223 tests continue to pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
355 lines
12 KiB
Rust
355 lines
12 KiB
Rust
//! Application state initialisation and wiring.
|
|
//!
|
|
//! This module acts as the composition root for the zesdex daemon (and
|
|
//! any other binary that needs a full set of services). It:
|
|
//!
|
|
//! 1. Defines [`IamServiceProvider`] and [`CmsServiceProvider`] trait
|
|
//! objects so callers depend on interfaces, not generics.
|
|
//! 2. Provides default implementations that wire together the
|
|
//! infrastructure/repository adapters with the domain service traits.
|
|
//! 3. Exposes [`initialize_app_context`] as a one-call entry point.
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use uuid::Uuid;
|
|
use zesdex_cms::domain::app_config::ProviderConfig;
|
|
use zesdex_cms::domain::conversation::Conversation;
|
|
use zesdex_cms::domain::memory::Memory;
|
|
use zesdex_cms::domain::repository::{
|
|
AppConfigRepository, ConversationRepository, MemoryRepository, SettingsRepository,
|
|
};
|
|
use zesdex_cms::domain::settings::Settings;
|
|
use zesdex_cms::infrastructure::persistence::{
|
|
JsonAppConfigRepository, JsonConversationRepository, JsonSettingsRepository,
|
|
MarkdownMemoryRepository,
|
|
};
|
|
use zesdex_entities::domain::common::store::Store;
|
|
use zesdex_iam::domain::repository::SessionRepository;
|
|
use zesdex_iam::domain::session::Session;
|
|
use zesdex_iam::infrastructure::persistence::session_repo::FileSystemSessionRepository;
|
|
|
|
use crate::database;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Trait definitions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Session-management service provider.
|
|
///
|
|
/// Abstracts session CRUD behind a trait object so the HTTP / CLI layers
|
|
/// do not depend on concrete repository generics.
|
|
pub trait IamServiceProvider: Send + Sync {
|
|
/// Create a new session with a generated UUID and default fields.
|
|
fn create_session(&self) -> Result<Session>;
|
|
|
|
/// List all available sessions.
|
|
fn list_all(&self) -> Result<Vec<Session>>;
|
|
|
|
/// Archive a session by id (sets `archived = true`).
|
|
fn archive_session(&self, id: &str) -> Result<()>;
|
|
}
|
|
|
|
/// CMS (content-management) service provider.
|
|
///
|
|
/// Combines settings, conversation, and memory operations behind a single
|
|
/// trait object.
|
|
pub trait CmsServiceProvider: Send + Sync {
|
|
// -- Settings --
|
|
/// Load current settings from the default store.
|
|
fn load_settings(&self) -> Result<Settings>;
|
|
|
|
/// Persist updated settings.
|
|
fn save_settings(&self, settings: &Settings) -> Result<()>;
|
|
|
|
/// Update the provider configuration (name and details).
|
|
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<()>;
|
|
|
|
// -- Conversations --
|
|
/// Load a conversation for the given session id.
|
|
fn load_conversation(&self, session_id: &str) -> Result<Conversation>;
|
|
|
|
/// Persist a conversation.
|
|
fn save_conversation(&self, conv: &Conversation) -> Result<()>;
|
|
|
|
// -- Memories --
|
|
/// List all memory slugs.
|
|
fn list_memories(&self) -> Result<Vec<String>>;
|
|
|
|
/// Save (create or update) a memory.
|
|
fn save_memory(&self, memory: &Memory) -> Result<()>;
|
|
|
|
/// Delete a memory by name.
|
|
fn delete_memory(&self, name: &str) -> Result<()>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Default IAM provider
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Default [`IamServiceProvider`] backed by the filesystem session
|
|
/// repository.
|
|
pub struct DefaultIamServiceProvider {
|
|
session_repo: FileSystemSessionRepository,
|
|
base_dir: PathBuf,
|
|
}
|
|
|
|
impl DefaultIamServiceProvider {
|
|
/// Create a new provider using the given data directory.
|
|
pub fn new(base_dir: PathBuf) -> Self {
|
|
Self {
|
|
session_repo: FileSystemSessionRepository::new(),
|
|
base_dir,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IamServiceProvider for DefaultIamServiceProvider {
|
|
fn create_session(&self) -> Result<Session> {
|
|
let id = Uuid::new_v4().to_string();
|
|
let session = Session::new(id, "New Session".to_string());
|
|
self.session_repo
|
|
.save_session(&self.base_dir, &session)
|
|
.context("failed to persist new session")?;
|
|
Ok(session)
|
|
}
|
|
|
|
fn list_all(&self) -> Result<Vec<Session>> {
|
|
self.session_repo
|
|
.list_sessions(&self.base_dir)
|
|
.context("failed to list sessions")
|
|
}
|
|
|
|
fn archive_session(&self, id: &str) -> Result<()> {
|
|
let mut session = self
|
|
.session_repo
|
|
.load_session(&self.base_dir, id)
|
|
.with_context(|| format!("session not found: {id}"))?;
|
|
session.archived = true;
|
|
session.updated_at = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis() as i64;
|
|
self.session_repo
|
|
.save_session(&self.base_dir, &session)
|
|
.context("failed to save archived session")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Default CMS provider
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Default [`CmsServiceProvider`] backed by filesystem repositories.
|
|
pub struct DefaultCmsServiceProvider {
|
|
settings_repo: JsonSettingsRepository,
|
|
app_config_repo: JsonAppConfigRepository,
|
|
conversation_repo: JsonConversationRepository,
|
|
memory_repo: MarkdownMemoryRepository,
|
|
base_dir: PathBuf,
|
|
memory_dir: PathBuf,
|
|
}
|
|
|
|
impl DefaultCmsServiceProvider {
|
|
/// Create a new provider.
|
|
pub fn new(store: &Store) -> Self {
|
|
Self {
|
|
settings_repo: JsonSettingsRepository::new(),
|
|
app_config_repo: JsonAppConfigRepository::new(),
|
|
conversation_repo: JsonConversationRepository::new(),
|
|
memory_repo: MarkdownMemoryRepository::new(),
|
|
base_dir: store.base_dir.clone(),
|
|
memory_dir: store.memory_dir.clone(),
|
|
}
|
|
}
|
|
|
|
/// Compute the session directory for a given session id.
|
|
fn session_dir(&self, session_id: &str) -> PathBuf {
|
|
self.base_dir.join("sessions").join(session_id)
|
|
}
|
|
}
|
|
|
|
impl CmsServiceProvider for DefaultCmsServiceProvider {
|
|
// -- Settings --
|
|
fn load_settings(&self) -> Result<Settings> {
|
|
self.settings_repo
|
|
.load(&self.base_dir)
|
|
.context("failed to load settings")
|
|
}
|
|
|
|
fn save_settings(&self, settings: &Settings) -> Result<()> {
|
|
self.settings_repo
|
|
.save(&self.base_dir, settings)
|
|
.context("failed to save settings")
|
|
}
|
|
|
|
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<()> {
|
|
let mut app_config = self
|
|
.app_config_repo
|
|
.load(&self.base_dir)
|
|
.context("failed to load app config")?;
|
|
app_config
|
|
.providers
|
|
.insert(name.to_string(), config.clone());
|
|
self.app_config_repo
|
|
.save(&self.base_dir, &app_config)
|
|
.context("failed to save app config after provider update")
|
|
}
|
|
|
|
// -- Conversations --
|
|
fn load_conversation(&self, session_id: &str) -> Result<Conversation> {
|
|
let dir = self.session_dir(session_id);
|
|
self.conversation_repo
|
|
.load(&dir)
|
|
.with_context(|| format!("failed to load conversation for session '{session_id}'"))
|
|
}
|
|
|
|
fn save_conversation(&self, conv: &Conversation) -> Result<()> {
|
|
let dir = self.session_dir(&conv.session_id);
|
|
self.conversation_repo.save(&dir, conv).with_context(|| {
|
|
format!(
|
|
"failed to save conversation for session '{}'",
|
|
conv.session_id
|
|
)
|
|
})
|
|
}
|
|
|
|
// -- Memories --
|
|
fn list_memories(&self) -> Result<Vec<String>> {
|
|
self.memory_repo
|
|
.list(&self.memory_dir)
|
|
.context("failed to list memories")
|
|
}
|
|
|
|
fn save_memory(&self, memory: &Memory) -> Result<()> {
|
|
self.memory_repo
|
|
.save(&self.memory_dir, memory)
|
|
.with_context(|| format!("failed to save memory '{}'", memory.name))
|
|
}
|
|
|
|
fn delete_memory(&self, name: &str) -> Result<()> {
|
|
self.memory_repo
|
|
.delete(&self.memory_dir, name)
|
|
.with_context(|| format!("failed to delete memory '{name}'"))
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AppContext
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Aggregated shared state for the zesdex daemon (or any binary using the
|
|
/// full service stack).
|
|
pub struct AppContext {
|
|
/// Filesystem store (resolved paths for all data directories).
|
|
pub store: Store,
|
|
/// IAM service provider (session management).
|
|
pub iam_service: Box<dyn IamServiceProvider>,
|
|
/// CMS service provider (settings, conversations, memories).
|
|
pub cms_service: Box<dyn CmsServiceProvider>,
|
|
/// SQLite database connection.
|
|
pub db: database::DbConn,
|
|
/// JWT HMAC secret used to sign / verify tokens.
|
|
pub jwt_secret: String,
|
|
}
|
|
|
|
impl AppContext {
|
|
/// Return a shared `Arc<AppContext>` for use with Axum's
|
|
/// `axum::extract::State`.
|
|
pub fn into_arc(self) -> Arc<Self> {
|
|
Arc::new(self)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Initialisation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Wire together the full application stack and return an [`AppContext`].
|
|
///
|
|
/// Steps:
|
|
/// 1. Initialise [`Store`] and create all data directories.
|
|
/// 2. Connect to the SQLite database and run migrations.
|
|
/// 3. Instantiate the IAM and CMS service providers.
|
|
/// 4. Determine the JWT secret (env var `ZESDEX_JWT_SECRET` or a default).
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if any step fails (directory creation, DB connection,
|
|
/// migration execution, etc.).
|
|
pub fn initialize_app_context() -> Result<AppContext> {
|
|
// -- Store --
|
|
let store = Store::new();
|
|
store
|
|
.ensure_dirs()
|
|
.context("failed to create store directories")?;
|
|
|
|
// -- Database --
|
|
let db_path = store.base_dir.join("zesdex.db");
|
|
let db_path_str = db_path
|
|
.to_str()
|
|
.ok_or_else(|| anyhow::anyhow!("invalid db path: {}", db_path.display()))?;
|
|
|
|
let db = database::init_db(db_path_str).context("failed to initialise database")?;
|
|
|
|
database::run_migrations(&db).context("failed to run database migrations")?;
|
|
|
|
// -- Services --
|
|
let iam_service: Box<dyn IamServiceProvider> =
|
|
Box::new(DefaultIamServiceProvider::new(store.base_dir.clone()));
|
|
let cms_service: Box<dyn CmsServiceProvider> = Box::new(DefaultCmsServiceProvider::new(&store));
|
|
|
|
// -- JWT secret --
|
|
let jwt_secret = std::env::var("ZESDEX_JWT_SECRET")
|
|
.unwrap_or_else(|_| "zesdex-dev-secret-do-not-use-in-production".to_string());
|
|
|
|
Ok(AppContext {
|
|
store,
|
|
iam_service,
|
|
cms_service,
|
|
db,
|
|
jwt_secret,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_iam_provider_list_all_empty() {
|
|
let tmp = std::env::temp_dir().join(format!("zesdex-test-iam-{}", Uuid::new_v4()));
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
let provider = DefaultIamServiceProvider::new(tmp.clone());
|
|
let sessions = provider.list_all().unwrap();
|
|
assert!(sessions.is_empty());
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_iam_provider_create_and_list() {
|
|
let tmp = std::env::temp_dir().join(format!("zesdex-test-iam-{}", Uuid::new_v4()));
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
let provider = DefaultIamServiceProvider::new(tmp.clone());
|
|
let session = provider.create_session().unwrap();
|
|
assert!(!session.id.is_empty());
|
|
let sessions = provider.list_all().unwrap();
|
|
assert_eq!(sessions.len(), 1);
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_cms_provider_default_settings() {
|
|
let tmp = std::env::temp_dir().join(format!("zesdex-test-cms-{}", Uuid::new_v4()));
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
let mut store = Store::new();
|
|
store.base_dir = tmp.clone();
|
|
store.memory_dir = tmp.join("memory");
|
|
let provider = DefaultCmsServiceProvider::new(&store);
|
|
let settings = provider.load_settings().unwrap();
|
|
assert_eq!(settings.provider, "zen");
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
}
|
|
}
|