234 lines
7.8 KiB
Rust
234 lines
7.8 KiB
Rust
//! Alias-only worker preparation and guarded adoption. Public compiled DTOs do
|
|
//! not authorize adoption; only this module can mint a prepared rename.
|
|
use super::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
/// A pinned intent minted by its owning Document. Move it to a worker and call
|
|
/// `prepare`; preparation has no access to the live Document or its histories.
|
|
#[derive(Debug)]
|
|
pub struct AliasRenameRequest {
|
|
owner: Arc<()>,
|
|
before: Arc<CompiledSourceProject>,
|
|
revision: u64,
|
|
limits: ProjectLimits,
|
|
target: Id,
|
|
old_alias: String,
|
|
new_alias: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct PreparedData {
|
|
request: AliasRenameRequest,
|
|
after: Arc<CompiledSourceProject>,
|
|
before_bytes: usize,
|
|
after_bytes: usize,
|
|
changed: bool,
|
|
}
|
|
|
|
/// Immutable, Clone-compatible transport for preview and approval. Neither
|
|
/// mutable candidate access nor deserialization can manufacture this authority.
|
|
#[derive(Debug, Clone)]
|
|
pub struct PreparedAliasRename(Arc<PreparedData>);
|
|
|
|
impl PreparedAliasRename {
|
|
pub fn before(&self) -> &CompiledSourceProject {
|
|
&self.0.request.before
|
|
}
|
|
|
|
pub fn after(&self) -> &CompiledSourceProject {
|
|
&self.0.after
|
|
}
|
|
|
|
pub fn target(&self) -> Id {
|
|
self.0.request.target
|
|
}
|
|
|
|
pub fn old_alias(&self) -> &str {
|
|
&self.0.request.old_alias
|
|
}
|
|
|
|
pub fn new_alias(&self) -> &str {
|
|
&self.0.request.new_alias
|
|
}
|
|
|
|
pub fn revision(&self) -> u64 {
|
|
self.0.request.revision
|
|
}
|
|
|
|
pub fn changed(&self) -> bool {
|
|
self.0.changed
|
|
}
|
|
}
|
|
|
|
fn refusal(message: &str) -> DocumentError {
|
|
ModelError(message.into()).into()
|
|
}
|
|
|
|
fn check_history_capacity(before_bytes: usize, after_bytes: usize) -> Result<(), DocumentError> {
|
|
if before_bytes.saturating_add(after_bytes) > HISTORY_BYTES {
|
|
return Err(refusal(
|
|
"This rename exceeds the model Undo budget. No rename was applied.",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl Document {
|
|
pub fn alias_rename_request(
|
|
&self,
|
|
target: Id,
|
|
replacement: String,
|
|
) -> Result<AliasRenameRequest, DocumentError> {
|
|
if self.revision == u64::MAX {
|
|
return Err(refusal(
|
|
"The document revision is exhausted. Reopen before renaming.",
|
|
));
|
|
}
|
|
let old_alias = self
|
|
.project()
|
|
.identities()
|
|
.into_iter()
|
|
.find_map(|(id, alias)| (id == target).then(|| alias.to_owned()))
|
|
.ok_or_else(|| refusal("The alias target is no longer present."))?;
|
|
// A candidate has a nonzero estimate. Avoid expensive preparation when
|
|
// even the current snapshot leaves no room for its immediate Undo.
|
|
if old_alias != replacement && self.current_bytes >= HISTORY_BYTES {
|
|
return Err(refusal(
|
|
"This rename exceeds the model Undo budget. No rename was applied.",
|
|
));
|
|
}
|
|
Ok(AliasRenameRequest {
|
|
owner: self.alias_owner.clone(),
|
|
before: self.current.clone(),
|
|
revision: self.revision,
|
|
limits: self.limits,
|
|
target,
|
|
old_alias,
|
|
new_alias: replacement,
|
|
})
|
|
}
|
|
|
|
/// App must separately consume its current approval and check workspace,
|
|
/// invocation and draft freshness. This checks accepted Document authority.
|
|
pub fn commit_alias_rename(
|
|
&mut self,
|
|
prepared: &PreparedAliasRename,
|
|
) -> Result<bool, DocumentError> {
|
|
let data = &prepared.0;
|
|
let request = &data.request;
|
|
if self.revision == u64::MAX
|
|
|| self.revision != request.revision
|
|
|| !Arc::ptr_eq(&self.alias_owner, &request.owner)
|
|
|| !Arc::ptr_eq(&self.current, &request.before)
|
|
{
|
|
return Err(refusal(
|
|
"This rename preview is stale. Preview the current alias again.",
|
|
));
|
|
}
|
|
if !data.changed {
|
|
return Ok(false);
|
|
}
|
|
if self.current_bytes != data.before_bytes {
|
|
return Err(refusal(
|
|
"The rename history estimate is stale. Preview again.",
|
|
));
|
|
}
|
|
check_history_capacity(self.current_bytes, data.after_bytes)?;
|
|
Ok(self.commit_snapshot(data.after.clone(), data.after_bytes))
|
|
}
|
|
}
|
|
|
|
impl AliasRenameRequest {
|
|
/// Run on the preparation worker. The typed command and source updater are
|
|
/// the only candidate construction path, followed by alias-only invariants.
|
|
pub fn prepare(self) -> Result<PreparedAliasRename, DocumentError> {
|
|
let before_project = &self.before.decoded.project;
|
|
let candidate = prepare_command(
|
|
before_project,
|
|
&Command::SetAlias(self.target, self.new_alias.clone()),
|
|
)?;
|
|
let mut expected = before_project.clone();
|
|
assign_expected_alias(&mut expected, self.target, &self.new_alias)?;
|
|
let identities: BTreeMap<_, _> = before_project
|
|
.identities()
|
|
.into_iter()
|
|
.map(|(id, _)| (id, id))
|
|
.collect();
|
|
if candidate.project != expected || candidate.identity_map != identities {
|
|
return Err(refusal(
|
|
"Alias preparation changed unrelated model data or identities.",
|
|
));
|
|
}
|
|
let after = source_project::update_bundle_with_identity_map(
|
|
&self.before,
|
|
&candidate.project,
|
|
&candidate.identity_map,
|
|
self.limits,
|
|
)?;
|
|
let before = &self.before;
|
|
if after.decoded.project != expected
|
|
|| before.bundle.entry != after.bundle.entry
|
|
|| !before.bundle.files.keys().eq(after.bundle.files.keys())
|
|
|| before.index.lexical_owners != after.index.lexical_owners
|
|
|| !before
|
|
.index
|
|
.declarations
|
|
.iter()
|
|
.map(|(id, loc)| (id, &loc.path))
|
|
.eq(after
|
|
.index
|
|
.declarations
|
|
.iter()
|
|
.map(|(id, loc)| (id, &loc.path)))
|
|
{
|
|
return Err(refusal(
|
|
"Alias preparation changed source membership or ownership.",
|
|
));
|
|
}
|
|
let changed = before.bundle != after.bundle;
|
|
let before_bytes = estimated_bytes(before);
|
|
let after_bytes = estimated_bytes(&after);
|
|
if changed {
|
|
check_history_capacity(before_bytes, after_bytes)?;
|
|
}
|
|
Ok(PreparedAliasRename(Arc::new(PreparedData {
|
|
request: self,
|
|
after: Arc::new(after),
|
|
before_bytes,
|
|
after_bytes,
|
|
changed,
|
|
})))
|
|
}
|
|
}
|
|
|
|
// Independent full-model expectation: SetAlias may change exactly one header
|
|
// field, across every supported identity family. Do not reuse command execution
|
|
// to derive the expected result, which would hide collateral command changes.
|
|
fn assign_expected_alias(project: &mut Project, id: Id, alias: &str) -> Result<(), DocumentError> {
|
|
if project.id == id {
|
|
project.alias = alias.to_owned();
|
|
} else if let Some(record) = project.elements.get_mut(&id) {
|
|
record.alias = alias.to_owned();
|
|
} else if let Some(record) = project.relations.get_mut(&id) {
|
|
record.alias = alias.to_owned();
|
|
} else if let Some(record) = project.diagrams.get_mut(&id) {
|
|
record.alias = alias.to_owned();
|
|
} else if let Some(record) = project.artifact_links.get_mut(&id) {
|
|
record.alias = alias.to_owned();
|
|
} else if let Some(record) = project.traces.get_mut(&id) {
|
|
record.alias = alias.to_owned();
|
|
} else {
|
|
project
|
|
.diagrams
|
|
.values_mut()
|
|
.find_map(|diagram| diagram.nodes.get_mut(&id))
|
|
.ok_or_else(|| refusal("The alias target is no longer present."))?
|
|
.alias = alias.to_owned();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "document_alias_rename_tests.rs"]
|
|
mod tests;
|