Refactor CMS and IAM modules: restructure presentation and command layers
- Removed HTTP adapter module from CMS infrastructure. - Updated CMS infrastructure module to exclude HTTP. - Introduced presentation layer in CMS with DTOs and handlers for REST API. - Added command types for CMS domain operations to encapsulate input data. - Created typed error handling for CMS presentation layer. - Implemented handlers for CMS REST API endpoints. - Removed HTTP DTOs and handlers from IAM infrastructure. - Introduced command types for IAM domain operations. - Created presentation layer in IAM with DTOs and handlers for OAuth flow. - Implemented typed error handling for IAM presentation layer.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
//! Command types for CMS domain operations.
|
||||
//!
|
||||
//! Following the `NewXxx` / `XxxPatch` pattern from clean architecture,
|
||||
//! these types encapsulate the input data for create/update operations
|
||||
//! on domain entities. They decouple presentation DTOs from the entity
|
||||
//! mutation surface and provide a clear boundary for validation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::settings::InternetMode;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Partial update command for `Settings`.
|
||||
///
|
||||
/// Every field is `Option`al — only non-`None` fields are applied to the
|
||||
/// existing settings instance. Use `apply_to()` to merge into a `Settings`
|
||||
/// value.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SettingsPatch {
|
||||
/// Override the internet access mode.
|
||||
pub internet_mode: Option<String>,
|
||||
/// Override the active provider name.
|
||||
pub provider: Option<String>,
|
||||
/// Override the active model name.
|
||||
pub model: Option<String>,
|
||||
/// Replace the entire API-keys map.
|
||||
pub api_keys: Option<HashMap<String, String>>,
|
||||
/// Override the max tokens for completions.
|
||||
pub max_tokens: Option<Option<u32>>,
|
||||
/// Override the temperature for completions.
|
||||
pub temperature: Option<Option<f32>>,
|
||||
/// Override the review max lessons per run.
|
||||
pub review_max_lessons_per_run: Option<usize>,
|
||||
/// Override the adaptive review max skip count.
|
||||
pub adaptive_review_max_skip: Option<u32>,
|
||||
/// Override the verify shell command.
|
||||
pub verify_command: Option<Option<String>>,
|
||||
/// Override the verify timeout in milliseconds.
|
||||
pub verify_timeout_ms: Option<u64>,
|
||||
/// Override the max concurrency for workflow execution.
|
||||
pub workflow_max_concurrency: Option<usize>,
|
||||
/// Override the review-enabled flag.
|
||||
pub review_enabled: Option<bool>,
|
||||
/// Override the session-archive-enabled flag.
|
||||
pub session_archive_enabled: Option<bool>,
|
||||
/// Override the LSP auto-provision flag.
|
||||
pub lsp_auto_provision: Option<bool>,
|
||||
/// Override the list of LSP-managed languages.
|
||||
pub lsp_languages: Option<Vec<String>>,
|
||||
/// Override the hive-mind node timeout in milliseconds.
|
||||
pub hive_mind_node_timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl SettingsPatch {
|
||||
/// Merge this patch into `settings`, overwriting each non-`None` field.
|
||||
///
|
||||
/// Flow: for each optional field, if `Some`, assign it to the target.
|
||||
///
|
||||
/// ## Errors
|
||||
/// Returns `Err` with a message if `internet_mode` is set to an
|
||||
/// unrecognised value.
|
||||
pub fn apply_to(&self, settings: &mut super::settings::Settings) -> Result<(), String> {
|
||||
if let Some(ref val) = self.internet_mode {
|
||||
settings.internet_mode = match val.as_str() {
|
||||
"Off" => InternetMode::Off,
|
||||
"ReadOnly" => InternetMode::ReadOnly,
|
||||
"Full" => InternetMode::Full,
|
||||
_ => return Err(format!("invalid internet_mode '{val}'; expected Off, ReadOnly, or Full")),
|
||||
};
|
||||
}
|
||||
if let Some(ref val) = self.provider {
|
||||
settings.provider = val.clone();
|
||||
}
|
||||
if let Some(ref val) = self.model {
|
||||
settings.model = val.clone();
|
||||
}
|
||||
if let Some(ref val) = self.api_keys {
|
||||
settings.api_keys = val.clone();
|
||||
}
|
||||
if let Some(val) = self.max_tokens {
|
||||
settings.max_tokens = val;
|
||||
}
|
||||
if let Some(val) = self.temperature {
|
||||
settings.temperature = val;
|
||||
}
|
||||
if let Some(val) = self.review_max_lessons_per_run {
|
||||
settings.review_max_lessons_per_run = val;
|
||||
}
|
||||
if let Some(val) = self.adaptive_review_max_skip {
|
||||
settings.adaptive_review_max_skip = val;
|
||||
}
|
||||
if let Some(ref val) = self.verify_command {
|
||||
settings.verify_command = val.clone();
|
||||
}
|
||||
if let Some(val) = self.verify_timeout_ms {
|
||||
settings.verify_timeout_ms = val;
|
||||
}
|
||||
if let Some(val) = self.workflow_max_concurrency {
|
||||
settings.workflow_max_concurrency = val;
|
||||
}
|
||||
if let Some(val) = self.review_enabled {
|
||||
settings.flags.review_enabled = val;
|
||||
}
|
||||
if let Some(val) = self.session_archive_enabled {
|
||||
settings.flags.session_archive_enabled = val;
|
||||
}
|
||||
if let Some(val) = self.lsp_auto_provision {
|
||||
settings.flags.lsp_auto_provision = val;
|
||||
}
|
||||
if let Some(ref val) = self.lsp_languages {
|
||||
settings.lsp_languages = val.clone();
|
||||
}
|
||||
if let Some(val) = self.hive_mind_node_timeout_ms {
|
||||
settings.hive_mind_node_timeout_ms = val;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Memory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Command to create a new memory entry.
|
||||
///
|
||||
/// All required fields are non-optional; optional fields use `Option`
|
||||
/// and default to sensible values (empty or the service default).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewMemory {
|
||||
/// Unique name / slug for the memory.
|
||||
pub name: String,
|
||||
/// One-line summary of what the memory captures.
|
||||
pub description: String,
|
||||
/// The full memory content.
|
||||
pub content: String,
|
||||
/// Category kind (defaults to "reference" in the handler).
|
||||
pub kind: Option<String>,
|
||||
/// Outcome of the remembered action.
|
||||
pub outcome: Option<String>,
|
||||
/// Lifecycle stage (defaults to "new" in the handler).
|
||||
pub lifecycle: Option<String>,
|
||||
/// Scope context for the memory.
|
||||
pub scope: Option<String>,
|
||||
/// Code snippet captured before the action.
|
||||
pub before_snippet: Option<String>,
|
||||
/// Code snippet captured after the action.
|
||||
pub after_snippet: Option<String>,
|
||||
/// Source provenances (files, conversations, etc.).
|
||||
pub provenances: Option<Vec<String>>,
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
//! They contain no I/O, no framework imports, and no side effects.
|
||||
|
||||
pub mod app_config;
|
||||
pub mod commands;
|
||||
pub mod conversation;
|
||||
pub mod edit_log;
|
||||
pub mod error;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//! HTTP adapter — handler functions and DTOs for the CMS REST API.
|
||||
//!
|
||||
//! Provides hyper-based request handlers and serialisation types for
|
||||
//! the CMS HTTP endpoints. Handlers receive domain service trait objects
|
||||
//! via dependency injection (Arc-wrapped trait objects) and translate
|
||||
//! between HTTP request/response formats and domain types.
|
||||
//!
|
||||
//! ## Sub-modules
|
||||
//! - `dto` — request/response DTO types (JSON serialisation)
|
||||
//! - `handlers` — hyper request handler functions
|
||||
//!
|
||||
//! ## Endpoints
|
||||
//! - `GET /settings` — load current application settings
|
||||
//! - `PUT /settings` — update application settings
|
||||
//! - `GET /memories` — list all memory slugs
|
||||
//! - `POST /memories` — create a new memory entry
|
||||
//! - `POST /memories/{name}` — (future) update memory
|
||||
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
|
||||
pub use dto::{
|
||||
ConversationResponse, MemoryCreateRequest, MemoryResponse, SettingsResponse,
|
||||
SettingsUpdateRequest,
|
||||
};
|
||||
pub use handlers::{
|
||||
handle_create_memory, handle_get_settings, handle_list_memories, handle_update_settings,
|
||||
};
|
||||
@@ -6,7 +6,5 @@
|
||||
//!
|
||||
//! ## Sub-modules
|
||||
//! - `persistence` — file-based repository implementations (JSON, markdown, SQLite)
|
||||
//! - `http` — hyper-based HTTP API handlers and DTO types
|
||||
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
|
||||
@@ -23,3 +23,4 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
pub mod presentation;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Typed presentation-layer error type for the CMS crate.
|
||||
//!
|
||||
//! `AppError` replaces bare `anyhow::Result` in handler signatures with a
|
||||
//! structured enum that callers can match on for status-code selection
|
||||
//! and structured error responses.
|
||||
//!
|
||||
//! `From<ServiceError>` auto-converts domain errors so handler code uses
|
||||
//! the `?` operator throughout.
|
||||
//!
|
||||
//! # Variants
|
||||
//!
|
||||
//! - `BadRequest` — invalid input, validation failure
|
||||
//! - `NotFound` — resource not found
|
||||
//! - `Conflict` — resource already exists
|
||||
//! - `Internal` — unexpected errors translated to a generic message
|
||||
|
||||
use crate::domain::error::ServiceError;
|
||||
|
||||
/// Typed presentation-layer error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
/// The request was malformed or contained invalid data.
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
/// The requested resource was not found.
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
/// The request conflicts with the current state.
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
/// An unexpected internal error occurred.
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl From<ServiceError> for AppError {
|
||||
fn from(e: ServiceError) -> Self {
|
||||
match e {
|
||||
ServiceError::Repository(repo_err) => match repo_err {
|
||||
zesdex_utils::Error::NotFound(msg) => AppError::NotFound(msg),
|
||||
zesdex_utils::Error::Conflict(msg) => AppError::Conflict(msg),
|
||||
_ => AppError::Internal(repo_err.to_string()),
|
||||
},
|
||||
ServiceError::InvalidInput(msg) => AppError::BadRequest(msg),
|
||||
ServiceError::Other(msg) => AppError::Internal(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-87
@@ -17,14 +17,15 @@
|
||||
//! 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::commands::{NewMemory, SettingsPatch};
|
||||
use crate::domain::memory::Memory;
|
||||
use crate::domain::service::{MemoryService, SettingsService};
|
||||
use crate::domain::settings::Settings;
|
||||
|
||||
use super::dto::{MemoryCreateRequest, MemoryResponse, SettingsResponse, SettingsUpdateRequest};
|
||||
use super::error::AppError;
|
||||
|
||||
/// Handle `GET /settings`
|
||||
///
|
||||
@@ -32,8 +33,8 @@ use super::dto::{MemoryCreateRequest, MemoryResponse, SettingsResponse, Settings
|
||||
///
|
||||
/// Flow: load settings from service → convert to DTO → return.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_get_settings<S: SettingsService>(service: &S) -> Result<SettingsResponse> {
|
||||
let settings = service.load_settings().context("failed to load settings")?;
|
||||
pub fn handle_get_settings<S: SettingsService>(service: &S) -> Result<SettingsResponse, AppError> {
|
||||
let settings = service.load_settings()?;
|
||||
Ok(SettingsResponse::from(settings))
|
||||
}
|
||||
|
||||
@@ -42,83 +43,44 @@ pub fn handle_get_settings<S: SettingsService>(service: &S) -> Result<SettingsRe
|
||||
/// 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.
|
||||
/// Flow: build `SettingsPatch` from DTO → apply to current settings → save → return DTO.
|
||||
///
|
||||
/// ## Validation
|
||||
/// - `internet_mode` must be "Off", "ReadOnly", or "Full" (case-sensitive).
|
||||
/// - `internet_mode` is validated by `SettingsPatch::apply_to`.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_update_settings<S: SettingsService>(
|
||||
service: &S,
|
||||
req: SettingsUpdateRequest,
|
||||
) -> Result<SettingsResponse> {
|
||||
) -> Result<SettingsResponse, AppError> {
|
||||
// Build the domain patch command from the wire DTO
|
||||
let patch = SettingsPatch {
|
||||
internet_mode: req.internet_mode,
|
||||
provider: req.provider,
|
||||
model: req.model,
|
||||
api_keys: req.api_keys,
|
||||
max_tokens: req.max_tokens,
|
||||
temperature: req.temperature,
|
||||
review_max_lessons_per_run: req.review_max_lessons_per_run,
|
||||
adaptive_review_max_skip: req.adaptive_review_max_skip,
|
||||
verify_command: req.verify_command,
|
||||
verify_timeout_ms: req.verify_timeout_ms,
|
||||
workflow_max_concurrency: req.workflow_max_concurrency,
|
||||
review_enabled: req.review_enabled,
|
||||
session_archive_enabled: req.session_archive_enabled,
|
||||
lsp_auto_provision: req.lsp_auto_provision,
|
||||
lsp_languages: req.lsp_languages,
|
||||
hive_mind_node_timeout_ms: req.hive_mind_node_timeout_ms,
|
||||
};
|
||||
|
||||
// Load current settings as baseline for partial update
|
||||
let mut settings: Settings = service
|
||||
.load_settings()
|
||||
.context("failed to load current settings for update")?;
|
||||
let mut settings: Settings = service.load_settings()?;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Apply the patch via the domain command
|
||||
patch
|
||||
.apply_to(&mut settings)
|
||||
.map_err(AppError::BadRequest)?;
|
||||
|
||||
service
|
||||
.save_settings(&settings)
|
||||
.context("failed to save updated settings")?;
|
||||
service.save_settings(&settings)?;
|
||||
|
||||
Ok(SettingsResponse::from(settings))
|
||||
}
|
||||
@@ -131,12 +93,12 @@ pub fn handle_update_settings<S: SettingsService>(
|
||||
///
|
||||
/// Flow: list slugs from service → map each to minimal MemoryResponse → return.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_list_memories<M: MemoryService>(service: &M) -> Result<Vec<MemoryResponse>> {
|
||||
let slugs = service.list_memories().context("failed to list memories")?;
|
||||
pub fn handle_list_memories<M: MemoryService>(
|
||||
service: &M,
|
||||
) -> Result<Vec<MemoryResponse>, AppError> {
|
||||
let slugs = service.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.
|
||||
// Return minimal responses keyed by slug.
|
||||
let responses: Vec<MemoryResponse> = slugs
|
||||
.into_iter()
|
||||
.map(|slug| MemoryResponse {
|
||||
@@ -170,26 +132,38 @@ pub fn handle_list_memories<M: MemoryService>(service: &M) -> Result<Vec<MemoryR
|
||||
pub fn handle_create_memory<M: MemoryService>(
|
||||
service: &M,
|
||||
req: MemoryCreateRequest,
|
||||
) -> Result<MemoryResponse> {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let memory = Memory {
|
||||
) -> Result<MemoryResponse, AppError> {
|
||||
// Build the domain command from the wire DTO
|
||||
let cmd = NewMemory {
|
||||
name: req.name,
|
||||
description: req.description,
|
||||
content: req.content,
|
||||
kind: req.kind.unwrap_or_else(|| "reference".to_string()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
kind: req.kind,
|
||||
outcome: req.outcome,
|
||||
lifecycle: req.lifecycle.unwrap_or_else(|| "new".to_string()),
|
||||
lifecycle: req.lifecycle,
|
||||
scope: req.scope,
|
||||
before_snippet: req.before_snippet,
|
||||
after_snippet: req.after_snippet,
|
||||
provenances: req.provenances.unwrap_or_default(),
|
||||
provenances: req.provenances,
|
||||
};
|
||||
|
||||
service
|
||||
.save_memory(&memory)
|
||||
.context("failed to save memory")?;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let memory = Memory {
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
content: cmd.content,
|
||||
kind: cmd.kind.unwrap_or_else(|| "reference".to_string()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
outcome: cmd.outcome,
|
||||
lifecycle: cmd.lifecycle.unwrap_or_else(|| "new".to_string()),
|
||||
scope: cmd.scope,
|
||||
before_snippet: cmd.before_snippet,
|
||||
after_snippet: cmd.after_snippet,
|
||||
provenances: cmd.provenances.unwrap_or_default(),
|
||||
};
|
||||
|
||||
service.save_memory(&memory)?;
|
||||
|
||||
Ok(MemoryResponse::from(memory))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! HTTP presentation layer — handler functions and DTOs for the CMS crate.
|
||||
//!
|
||||
//! This is the outermost ring of the Clean Architecture onion. Handlers receive
|
||||
//! domain service trait references via generics and translate between
|
||||
//! request/response DTOs and domain types. They have **no dependency** on
|
||||
//! any HTTP framework — callers (IPC server, Axum, etc.) are responsible for
|
||||
//! mapping results into actual HTTP responses.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`dto`] — request/response DTO types (JSON serialisation)
|
||||
//! - [`handlers`] — handler functions that accept service trait refs + DTOs
|
||||
//! - [`error`] — typed presentation-layer error type
|
||||
//!
|
||||
//! # Dependency rule
|
||||
//!
|
||||
//! presentation → application → domain
|
||||
//! presentation may also depend on infrastructure for wiring/composition.
|
||||
|
||||
pub mod dto;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
|
||||
pub use dto::{
|
||||
ConversationResponse, MemoryCreateRequest, MemoryResponse, SettingsResponse,
|
||||
SettingsUpdateRequest,
|
||||
};
|
||||
pub use error::AppError;
|
||||
pub use handlers::{
|
||||
handle_create_memory, handle_get_settings, handle_list_memories, handle_update_settings,
|
||||
};
|
||||
Reference in New Issue
Block a user