Files
IdeA/crates/application/src/layout/usecases.rs
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

233 lines
7.9 KiB
Rust

//! [`LoadLayout`] and [`MutateLayout`] (ARCHITECTURE §6, §7; L4 + #4).
//!
//! A project owns **several named layouts** (see [`super::store`]). These two use
//! cases operate on **one** layout — the one identified by `layout_id`, or the
//! active layout when `layout_id` is `None`. They stay thin orchestrators: the
//! mutating operations are the domain's pure `LayoutTree` functions.
use std::sync::Arc;
use domain::ports::{EventBus, FileSystem, ProjectStore};
use domain::{AgentId, Direction, DomainEvent, LayoutError, LayoutId, LayoutTree, LeafCell, NodeId, ProjectId, SessionId};
use crate::error::AppError;
use super::store::{persist_doc, resolve_doc};
/// Maps a [`LayoutError`] to the application error type.
fn map_layout_err(e: LayoutError) -> AppError {
match e {
LayoutError::NodeNotFound(id) => AppError::NotFound(format!("layout node {id}")),
other => AppError::Invalid(other.to_string()),
}
}
/// A layout mutation expressed in terms of the pure domain operations.
///
/// Each variant maps 1:1 to a pure `LayoutTree` method
/// (`split`/`merge`/`resize`/`move`/`set_session`). Decoupling the *operation*
/// from the *tree* keeps the use case a thin orchestrator and lets the
/// presentation layer (and undo/redo) speak in intentions.
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutOperation {
/// Split a leaf into a two-child split (original + a new empty leaf).
Split {
/// The leaf to split.
target: NodeId,
/// Row (columns) or Column (rows).
direction: Direction,
/// Id for the new sibling leaf.
new_leaf: NodeId,
/// Id for the wrapping split container.
container: NodeId,
},
/// Collapse a split container back to one of its children.
Merge {
/// The split container to collapse.
container: NodeId,
/// Index of the child to keep.
keep_index: usize,
},
/// Reassign the relative weights of a split's children.
Resize {
/// The split container to resize.
container: NodeId,
/// New weights (one per child, all `> 0`).
weights: Vec<f32>,
},
/// Move the session hosted by one leaf to another (empty) leaf.
Move {
/// Source leaf (left empty).
from: NodeId,
/// Target leaf (must be empty).
to: NodeId,
},
/// Attach or detach a session to/from a leaf (cell ↔ terminal binding).
SetSession {
/// The hosting leaf.
target: NodeId,
/// Session to host, or `None` to clear.
session: Option<SessionId>,
},
/// Attach or detach an agent to/from a leaf (per-cell agent, feature #3).
SetCellAgent {
/// The hosting leaf.
target: NodeId,
/// Agent to associate, or `None` to clear.
agent: Option<AgentId>,
},
}
impl LayoutOperation {
/// Applies this operation to `tree`, returning the new validated tree.
fn apply(&self, tree: &LayoutTree) -> Result<LayoutTree, AppError> {
let result = match self {
Self::Split {
target,
direction,
new_leaf,
container,
} => tree.split(
*target,
*direction,
LeafCell {
id: *new_leaf,
session: None,
agent: None,
},
*container,
),
Self::Merge {
container,
keep_index,
} => tree.merge(*container, *keep_index),
Self::Resize { container, weights } => tree.resize(*container, weights),
Self::Move { from, to } => tree.move_session(*from, *to),
Self::SetSession { target, session } => tree.set_session(*target, *session),
Self::SetCellAgent { target, agent } => tree.set_cell_agent(*target, *agent),
};
result.map_err(map_layout_err)
}
}
/// Input for [`LoadLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadLayoutInput {
/// Project whose layout to load.
pub project_id: ProjectId,
/// Which named layout to load; `None` loads the active one.
pub layout_id: Option<LayoutId>,
}
/// Output of [`LoadLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct LoadLayoutOutput {
/// The id of the layout that was loaded (resolved from active when omitted).
pub layout_id: LayoutId,
/// The loaded layout tree.
pub layout: LayoutTree,
}
/// Loads one named layout (the active one by default), self-healing / migrating
/// the layouts store as needed (see [`super::store::resolve_doc`]).
pub struct LoadLayout {
store: Arc<dyn ProjectStore>,
fs: Arc<dyn FileSystem>,
}
impl LoadLayout {
/// Builds the use case from its injected ports.
#[must_use]
pub fn new(store: Arc<dyn ProjectStore>, fs: Arc<dyn FileSystem>) -> Self {
Self { store, fs }
}
/// Executes the load.
///
/// # Errors
/// - [`AppError::NotFound`] if the project or the requested layout is unknown,
/// - [`AppError::FileSystem`] if seeding the default layouts fails to persist,
/// - [`AppError::Store`] on registry I/O failure.
pub async fn execute(&self, input: LoadLayoutInput) -> Result<LoadLayoutOutput, AppError> {
let project = self.store.load_project(input.project_id).await?;
let doc = resolve_doc(self.fs.as_ref(), &project).await?;
let id = doc.resolve_id(input.layout_id);
let named = doc
.find(id)
.ok_or_else(|| AppError::NotFound(format!("layout {id}")))?;
Ok(LoadLayoutOutput {
layout_id: id,
layout: named.tree.clone(),
})
}
}
/// Input for [`MutateLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct MutateLayoutInput {
/// Project whose layout to mutate.
pub project_id: ProjectId,
/// Which named layout to mutate; `None` mutates the active one.
pub layout_id: Option<LayoutId>,
/// The operation to apply.
pub operation: LayoutOperation,
}
/// Output of [`MutateLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct MutateLayoutOutput {
/// The id of the mutated layout.
pub layout_id: LayoutId,
/// The new, persisted layout tree.
pub layout: LayoutTree,
}
/// Applies a pure layout operation to one named layout, persists the whole
/// layouts store and publishes [`DomainEvent::LayoutChanged`].
pub struct MutateLayout {
store: Arc<dyn ProjectStore>,
fs: Arc<dyn FileSystem>,
events: Arc<dyn EventBus>,
}
impl MutateLayout {
/// Builds the use case from its injected ports.
#[must_use]
pub fn new(
store: Arc<dyn ProjectStore>,
fs: Arc<dyn FileSystem>,
events: Arc<dyn EventBus>,
) -> Self {
Self { store, fs, events }
}
/// Executes the mutation.
///
/// # Errors
/// - [`AppError::NotFound`] if the project, layout or a referenced node is unknown,
/// - [`AppError::Invalid`] if the operation violates a layout invariant,
/// - [`AppError::FileSystem`] on persistence failure,
/// - [`AppError::Store`] on registry I/O failure.
pub async fn execute(&self, input: MutateLayoutInput) -> Result<MutateLayoutOutput, AppError> {
let project = self.store.load_project(input.project_id).await?;
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
let id = doc.resolve_id(input.layout_id);
let named = doc
.find_mut(id)
.ok_or_else(|| AppError::NotFound(format!("layout {id}")))?;
let next = input.operation.apply(&named.tree)?;
named.tree = next.clone();
persist_doc(self.fs.as_ref(), &project, &doc).await?;
self.events.publish(DomainEvent::LayoutChanged {
project_id: input.project_id,
});
Ok(MutateLayoutOutput {
layout_id: id,
layout: next,
})
}
}