feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
232
crates/application/src/layout/usecases.rs
Normal file
232
crates/application/src/layout/usecases.rs
Normal file
@ -0,0 +1,232 @@
|
||||
//! [`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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user