Files
ARCADIA-Studio/crates/oa-desktop/src/semantic_review/attachments.rs
T

457 lines
17 KiB
Rust

//! Existing-record child/incident editing with independent exact-field drafts.
//! Every proposal names its actual Parent or relationship endpoint slot.
use super::*;
use crate::forms::RecoveryDraft;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Family {
#[default]
Child,
Source,
Target,
}
impl std::fmt::Display for Family {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Child => "Owned children",
Self::Source => "Relationships at source",
Self::Target => "Relationships at target",
})
}
}
#[derive(Debug, Clone)]
pub enum Event {
Family(Family),
Pick(Layer, Option<Choice>),
Search(String),
Stage,
}
#[derive(Default)]
struct PairDraft {
oa: Option<Choice>,
sa: Option<Choice>,
}
#[derive(Default)]
pub struct Editor {
pub family: Family,
pub search: String,
pairs: BTreeMap<Family, PairDraft>,
pub drafts: BTreeMap<FacetSelector, Draft>,
}
#[derive(Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Saved {
family: Family,
pairs: Vec<(Family, Option<Id>, Option<Id>)>,
drafts: Vec<(FacetSelector, RecoveryDraft)>,
}
pub struct Proposal {
pub slot: (Id, FacetSelector),
pub path: String,
pub record: Value,
}
pub(super) fn family(facet: &FacetSelector) -> Option<Family> {
Some(match facet {
FacetSelector::Child(_) => Family::Child,
FacetSelector::Incident {
end: IncidentEnd::Source,
..
} => Family::Source,
FacetSelector::Incident {
end: IncidentEnd::Target,
..
} => Family::Target,
_ => return None,
})
}
pub(super) fn supported(facet: &FacetSelector) -> bool {
family(facet).is_some()
}
fn keys(facet: &FacetSelector) -> Option<&KeyPair<Id>> {
match facet {
FacetSelector::Child(keys) | FacetSelector::Incident { keys, .. } => Some(keys),
_ => None,
}
}
pub(super) fn key(facet: &FacetSelector, layer: Layer) -> Option<Id> {
let keys = keys(facet)?;
if layer == Layer::Oa { keys.oa } else { keys.sa }
}
pub(super) fn explicit_pair(facet: &FacetSelector) -> Option<(Id, Id)> {
let keys = keys(facet)?;
keys.oa.zip(keys.sa)
}
pub(super) fn actual_slot(facet: &FacetSelector, layer: Layer) -> Option<(Id, FacetSelector)> {
let id = key(facet, layer)?;
Some((
id,
match family(facet)? {
Family::Child => FacetSelector::Header(HeaderFacet::Parent),
Family::Source => FacetSelector::Relation(RelationFacet::Source),
Family::Target => FacetSelector::Relation(RelationFacet::Target),
},
))
}
pub(super) fn path(project: &Project, facet: &FacetSelector, layer: Layer) -> Option<String> {
let (id, slot) = actual_slot(facet, layer)?;
if project.layer_of(id) != Some(layer) {
return None;
}
// Child keys cannot accidentally name a relationship (and vice versa).
match family(facet)? {
Family::Child if !project.elements.contains_key(&id) => return None,
Family::Source | Family::Target if !project.relations.contains_key(&id) => return None,
_ => {}
}
scalar::binding(project, id, &slot)
}
pub(super) fn label(facet: &FacetSelector) -> &'static str {
match family(facet) {
Some(Family::Child) => "Final parent of the destination child",
Some(Family::Source) => "Final source of the destination relationship",
Some(Family::Target) => "Final target of the destination relationship",
None => "Unavailable attachment",
}
}
impl Editor {
pub fn selection(&self, layer: Layer) -> Option<&Choice> {
let pair = self.pairs.get(&self.family)?;
if layer == Layer::Oa {
pair.oa.as_ref()
} else {
pair.sa.as_ref()
}
}
pub fn update(
&mut self,
project: &Project,
trace: &Trace,
event: Event,
) -> Result<Option<FacetSelector>, String> {
match event {
Event::Family(family) => self.family = family,
Event::Search(search) => self.search = search,
Event::Pick(layer, choice) => {
let pair = self.pairs.entry(self.family).or_default();
if layer == Layer::Oa {
pair.oa = choice;
} else {
pair.sa = choice;
}
}
Event::Stage => {
let keys = KeyPair {
oa: self.selection(Layer::Oa).map(|c| c.id),
sa: self.selection(Layer::Sa).map(|c| c.id),
};
let facet = match self.family {
Family::Child => FacetSelector::Child(keys),
Family::Source => FacetSelector::Incident {
keys,
end: IncidentEnd::Source,
},
Family::Target => FacetSelector::Incident {
keys,
end: IncidentEnd::Target,
},
};
for (id, expected) in [
(key(&facet, Layer::Oa), Layer::Oa),
(key(&facet, Layer::Sa), Layer::Sa),
] {
if id
.and_then(|id| project.layer_of(id))
.is_some_and(|actual| actual != expected)
{
return Err(
"Attachment identities must match their Operational or System side."
.into(),
);
}
}
capture_facet(project, trace.oa, trace.sa, &facet)?;
return Ok(Some(facet));
}
}
Ok(None)
}
pub fn select_pair(&mut self, project: &Project, facet: &FacetSelector) -> Result<(), String> {
self.family = family(facet).ok_or("Choose a child or incident relationship address.")?;
let keys = keys(facet).expect("Closed family");
self.pairs.insert(
self.family,
PairDraft {
oa: keys.oa.map(|id| view::choice(project, id)),
sa: keys.sa.map(|id| view::choice(project, id)),
},
);
Ok(())
}
pub fn ensure(
&mut self,
project: &Project,
facet: &FacetSelector,
layer: Layer,
) -> Result<(), String> {
let id = key(facet, layer).ok_or("Choose an existing destination attachment identity.")?;
path(project, facet, layer).ok_or("The destination attachment record is absent or has a different kind. Creation needs an explicit transition mapping.")?;
if !self.drafts.contains_key(facet) {
self.drafts.insert(
facet.clone(),
Draft::from_project(project, id)
.ok_or("Destination attachment record is absent.")?,
);
}
Ok(())
}
pub fn choices(&self, project: &Project, trace: &Trace, layer: Layer) -> Vec<Choice> {
let owner = if layer == Layer::Oa {
trace.oa
} else {
trace.sa
};
let owner_kind = project.elements.get(&owner).map(Element::kind);
let mut ids: BTreeSet<_> = match self.family {
Family::Child => project
.elements
.values()
.filter(|e| {
e.layer() == layer
&& e.id != owner
&& (e.parent == Some(owner)
|| owner_kind.is_some_and(|kind| {
oa_core::validation::parent_permits(kind, e.kind())
}))
})
.map(|e| e.id)
.collect(),
Family::Source | Family::Target => project
.relations
.values()
.filter(|relation| {
if project.layer_of(relation.id) != Some(layer) {
return false;
}
let (a, b) = relation.body.endpoints();
let (current, opposite) = if self.family == Family::Source {
(a, b)
} else {
(b, a)
};
if current == owner {
return true;
}
match (owner_kind, project.elements.get(&opposite)) {
(Some(owner_kind), Some(opposite)) => {
if self.family == Family::Source {
relation.kind().permits(owner_kind, opposite.kind())
} else {
relation.kind().permits(opposite.kind(), owner_kind)
}
}
// Rare relationship-to-relationship endpoints use the ordinary
// closed property picker rather than assuming element kinds.
_ => {
let Some(draft) = Draft::from_project(project, relation.id) else {
return false;
};
let facet = FacetSelector::Relation(if self.family == Family::Source {
RelationFacet::Source
} else {
RelationFacet::Target
});
let Some(path) = scalar::binding(project, relation.id, &facet) else {
return false;
};
crate::forms::reference_choices(&draft, project, &path, Id::nil())
.iter()
.any(|choice| choice.id == owner)
}
}
})
.map(|relation| relation.id)
.collect(),
};
if let Some(selected) = self.selection(layer) {
ids.insert(selected.id);
}
let search = self.search.to_lowercase();
let mut choices: Vec<_> = ids
.into_iter()
.map(|id| view::choice(project, id))
.filter(|choice| {
search.is_empty()
|| choice.label.to_lowercase().contains(&search)
|| self
.selection(layer)
.is_some_and(|selected| selected.id == choice.id)
})
.collect();
choices.sort_by_cached_key(|choice| choice.label.to_lowercase());
choices
}
pub fn propose(
&self,
project: &Project,
trace: &Trace,
direction: TransitionDirection,
facet: &FacetSelector,
decision: SemanticDecision,
form_context: bool,
) -> Result<Proposal, String> {
let layer = direction.target_layer();
let slot = actual_slot(facet, layer)
.ok_or("Choose the destination attachment identity explicitly.")?;
let path = path(project, facet, layer).ok_or(
"Only existing destination child and relationship records are supported here.",
)?;
let mut record = Draft::from_project(project, slot.0)
.ok_or("The attached record is absent.")?
.value;
let owner = if layer == Layer::Oa {
trace.oa
} else {
trace.sa
};
let value = match decision {
SemanticDecision::KeepTarget => record.pointer(&path).expect("Closed path").clone(),
SemanticDecision::Candidate => {
let draft = self
.drafts
.get(facet)
.ok_or("Set a final attachment value first.")?;
let supplied = match draft.materialize_scopes(std::slice::from_ref(&path)) {
Ok(value) => value,
Err(_) if form_context => draft.value.clone(),
Err(error) => return Err(error),
};
supplied
.pointer(&path)
.cloned()
.ok_or("The attachment's exact field is unavailable.")?
}
SemanticDecision::TakeSource if key(facet, direction.source_layer()).is_none() => {
record.pointer(&path).expect("Closed path").clone()
}
SemanticDecision::TakeSource => {
let source = capture_facet(project, trace.oa, trace.sa, facet)?;
let present = scalar::value(&source, direction.source_layer()).and_then(|value| value.as_bool()).ok_or("Source attachment does not apply; review an existing owner correspondence.")?;
if present {
serde_json::to_value(owner).expect("Id")
} else if family(facet) == Some(Family::Child) {
if record.pointer(&path) == Some(&serde_json::to_value(owner).expect("Id")) {
Value::Null
} else {
record.pointer(&path).expect("Closed path").clone()
}
} else {
return Err("Taking an absent relationship endpoint is incomplete. Choose Set final value and an explicit replacement endpoint.".into());
}
}
};
*record.pointer_mut(&path).expect("Closed path") = value;
Ok(Proposal { slot, path, record })
}
pub fn recovery_snapshot(&self) -> Saved {
Saved {
family: self.family,
pairs: self
.pairs
.iter()
.map(|(family, pair)| {
(
*family,
pair.oa.as_ref().map(|c| c.id),
pair.sa.as_ref().map(|c| c.id),
)
})
.collect(),
drafts: self
.drafts
.iter()
.map(|(facet, draft)| (facet.clone(), draft.recovery_snapshot()))
.collect(),
}
}
pub fn from_recovery(project: &Project, layer: Layer, saved: Saved) -> Result<Self, String> {
if saved.drafts.len() > crate::source_workspace::LIMITS.model.max_records
|| saved.pairs.len() > 3
{
return Err("Recovered attachment drafts exceed the supported limits.".into());
}
let mut editor = Self {
family: saved.family,
..Self::default()
};
for (family, oa, sa) in saved.pairs {
for (id, layer) in [(oa, Layer::Oa), (sa, Layer::Sa)] {
if id
.and_then(|id| project.layer_of(id))
.is_some_and(|actual| actual != layer)
{
return Err("Recovered attachment picker has a wrong-layer identity.".into());
}
}
if editor
.pairs
.insert(
family,
PairDraft {
oa: oa.map(|id| view::choice(project, id)),
sa: sa.map(|id| view::choice(project, id)),
},
)
.is_some()
{
return Err("Recovered attachment picker family is duplicated.".into());
}
}
for (facet, saved) in saved.drafts {
for expected in [Layer::Oa, Layer::Sa] {
if key(&facet, expected)
.and_then(|id| project.layer_of(id))
.is_some_and(|actual| actual != expected)
{
return Err("Recovered attachment address has a wrong-layer identity.".into());
}
}
let (id, _) = actual_slot(&facet, layer)
.ok_or("Recovered attachment has no destination record.")?;
let path = path(project, &facet, layer)
.ok_or("Recovered attachment record has a different kind.")?;
let draft = Draft::from_recovery(project, saved)?;
let original =
Draft::from_project(project, id).ok_or("Recovered attachment is absent.")?;
let mut protected = draft.value.clone();
*protected
.pointer_mut(&path)
.ok_or("Recovered attachment field is absent.")? =
original.value.pointer(&path).expect("Closed path").clone();
if draft.original != id
|| protected != original.value
|| draft.inputs.iter().any(|(key, input)| {
key != &path
&& original
.inputs
.get(key)
.is_none_or(|old| old.raw != input.raw)
})
{
return Err(
"Recovered attachment draft changes identity, kind or an unrelated property."
.into(),
);
}
if editor.drafts.insert(facet, draft).is_some() {
return Err("Recovered attachment draft address is duplicated.".into());
}
}
Ok(editor)
}
}
#[cfg(test)]
#[path = "attachments_tests.rs"]
mod tests;