351 lines
14 KiB
Rust
351 lines
14 KiB
Rust
//! Closed presence-only adapter. The core independently validates the full final record.
|
|
use super::*;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
pub enum Family {
|
|
Member(ReferenceCollection),
|
|
Literal,
|
|
Annotation,
|
|
Binding,
|
|
}
|
|
impl std::fmt::Display for Family {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
Self::Member(ReferenceCollection::FunctionInputItems) => "Function input items",
|
|
Self::Member(ReferenceCollection::FunctionOutputItems) => "Function output items",
|
|
Self::Member(ReferenceCollection::SignalItems) => "Signal items",
|
|
Self::Member(ReferenceCollection::CategoryExchanges) => "Category exchanges",
|
|
Self::Member(ReferenceCollection::ExchangeItems) => "Exchange items",
|
|
Self::Member(ReferenceCollection::TransitionTriggers) => "Transition triggers",
|
|
Self::Member(ReferenceCollection::TransitionEffects) => "Transition effects",
|
|
Self::Member(ReferenceCollection::Constrained) => "Constrained identities",
|
|
Self::Member(ReferenceCollection::Steps) => "Ordered steps",
|
|
Self::Member(ReferenceCollection::FragmentBranches) => "Fragment branches",
|
|
Self::Literal => "Enumeration literals",
|
|
Self::Annotation => "Message values",
|
|
Self::Binding => "Scenario lifeline bindings",
|
|
})
|
|
}
|
|
}
|
|
pub(super) const FAMILIES: [Family; 13] = [
|
|
Family::Member(ReferenceCollection::Steps),
|
|
Family::Member(ReferenceCollection::FragmentBranches),
|
|
Family::Member(ReferenceCollection::FunctionInputItems),
|
|
Family::Member(ReferenceCollection::FunctionOutputItems),
|
|
Family::Member(ReferenceCollection::SignalItems),
|
|
Family::Member(ReferenceCollection::ExchangeItems),
|
|
Family::Member(ReferenceCollection::CategoryExchanges),
|
|
Family::Member(ReferenceCollection::TransitionTriggers),
|
|
Family::Member(ReferenceCollection::TransitionEffects),
|
|
Family::Member(ReferenceCollection::Constrained),
|
|
Family::Literal,
|
|
Family::Annotation,
|
|
Family::Binding,
|
|
];
|
|
pub(super) fn family(facet: &FacetSelector) -> Option<Family> {
|
|
let family = match facet {
|
|
FacetSelector::Member { collection, .. } => Family::Member(*collection),
|
|
FacetSelector::Literal(_) => Family::Literal,
|
|
FacetSelector::Annotation(_) => Family::Annotation,
|
|
FacetSelector::Binding(_) => Family::Binding,
|
|
_ => return None,
|
|
};
|
|
FAMILIES.contains(&family).then_some(family)
|
|
}
|
|
pub(super) fn supported(facet: &FacetSelector) -> bool {
|
|
family(facet).is_some()
|
|
}
|
|
|
|
// Fixed paths for the closed collections and the exact enum literal set.
|
|
// They are selected by the typed facet; callers cannot supply arbitrary paths.
|
|
pub(super) fn path(value: &Value, family: Family) -> Option<String> {
|
|
let prefixes = ["/body/Oa/Shared", "/body/Sa/Shared"];
|
|
let tails: &[&str] = match family {
|
|
Family::Member(ReferenceCollection::Steps) => {
|
|
return [
|
|
"/body/Oa/Process/steps",
|
|
"/body/Sa/FunctionalChain/steps",
|
|
"/body/Oa/Shared/Scenario/steps",
|
|
"/body/Sa/Shared/Scenario/steps",
|
|
"/body/Oa/Shared/Occurrence/body/Branch/occurrences",
|
|
"/body/Sa/Shared/Occurrence/body/Branch/occurrences",
|
|
]
|
|
.into_iter()
|
|
.find(|p| value.pointer(p).is_some_and(Value::is_array))
|
|
.map(str::to_owned);
|
|
}
|
|
Family::Member(ReferenceCollection::FragmentBranches) => {
|
|
&["Occurrence/body/Fragment/branches"]
|
|
}
|
|
Family::Member(ReferenceCollection::FunctionInputItems) => {
|
|
&["Port/contract/FunctionInput/incoming_items"]
|
|
}
|
|
Family::Member(ReferenceCollection::FunctionOutputItems) => {
|
|
&["Port/contract/FunctionOutput/outgoing_items"]
|
|
}
|
|
Family::Member(ReferenceCollection::CategoryExchanges) => &["ExchangeCategory/exchanges"],
|
|
Family::Member(ReferenceCollection::SignalItems) => &["Event/body/Signal/items"],
|
|
Family::Member(ReferenceCollection::Constrained) => &["Constraint/constrained"],
|
|
Family::Literal => &["DataType/definition/Enumeration/literals"],
|
|
Family::Annotation => &["Occurrence/body/Exchange/values"],
|
|
Family::Binding => &["Occurrence/body/ScenarioUse/bindings"],
|
|
Family::Member(ReferenceCollection::ExchangeItems) => {
|
|
return [
|
|
"Interaction",
|
|
"FunctionalExchange",
|
|
"BoundaryExchange",
|
|
"CommunicationMean",
|
|
]
|
|
.iter()
|
|
.map(|kind| format!("/body/{kind}/items"))
|
|
.find(|p| value.pointer(p).is_some_and(Value::is_array));
|
|
}
|
|
Family::Member(ReferenceCollection::TransitionTriggers) => {
|
|
return value
|
|
.pointer("/body/StateTransition/triggers")
|
|
.is_some_and(Value::is_array)
|
|
.then(|| "/body/StateTransition/triggers".into());
|
|
}
|
|
Family::Member(ReferenceCollection::TransitionEffects) => {
|
|
return value
|
|
.pointer("/body/StateTransition/effects")
|
|
.is_some_and(Value::is_array)
|
|
.then(|| "/body/StateTransition/effects".into());
|
|
}
|
|
};
|
|
prefixes
|
|
.iter()
|
|
.flat_map(|prefix| tails.iter().map(move |tail| format!("{prefix}/{tail}")))
|
|
.find(|p| value.pointer(p).is_some_and(Value::is_array))
|
|
}
|
|
pub(super) fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
|
|
match facet {
|
|
FacetSelector::Member { keys, .. } => if layer == Layer::Oa { keys.oa } else { keys.sa }
|
|
.map(|id| serde_json::to_value(id).expect("Id")),
|
|
FacetSelector::Literal(keys) => if layer == Layer::Oa {
|
|
keys.oa.as_ref()
|
|
} else {
|
|
keys.sa.as_ref()
|
|
}
|
|
.map(|v| Value::String(v.clone())),
|
|
FacetSelector::Annotation(keys) => if layer == Layer::Oa {
|
|
keys.oa.as_ref()
|
|
} else {
|
|
keys.sa.as_ref()
|
|
}
|
|
.map(|key| serde_json::to_value(key).expect("Annotation key")),
|
|
FacetSelector::Binding(keys) => if layer == Layer::Oa { keys.oa } else { keys.sa }
|
|
.map(|id| serde_json::to_value(id).expect("Binding key")),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub(super) fn overlap(a: &FacetSelector, b: &FacetSelector) -> bool {
|
|
if family(a).is_none() || family(a) != family(b) {
|
|
return false;
|
|
}
|
|
[Layer::Oa, Layer::Sa]
|
|
.into_iter()
|
|
.any(|layer| key(a, layer).is_some() && key(a, layer) == key(b, layer))
|
|
}
|
|
pub(super) fn project_presence(
|
|
value: &mut Value,
|
|
facet: &FacetSelector,
|
|
layer: Layer,
|
|
present: bool,
|
|
) -> Result<(), String> {
|
|
let family = family(facet).ok_or("This keyed family remains read-only.")?;
|
|
let key = key(facet, layer)
|
|
.ok_or("Choose the destination identity in Members before setting or taking presence.")?;
|
|
let path =
|
|
path(value, family).ok_or("This collection is not applicable to the destination kind.")?;
|
|
let list = value
|
|
.pointer_mut(&path)
|
|
.and_then(Value::as_array_mut)
|
|
.ok_or("Destination collection is unavailable.")?;
|
|
if present {
|
|
if !list.contains(&key) {
|
|
list.push(key);
|
|
}
|
|
} else {
|
|
list.retain(|v| v != &key);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl ReviewRow {
|
|
pub(super) fn keyed_value(
|
|
&self,
|
|
project: &Project,
|
|
trace: &Trace,
|
|
value: &mut Value,
|
|
facet: &FacetSelector,
|
|
decision: SemanticDecision,
|
|
) -> Result<(), String> {
|
|
if keyed_rows::supported(facet) {
|
|
return self.project_row(project, trace, value, facet, decision);
|
|
}
|
|
let present = match decision {
|
|
SemanticDecision::KeepTarget => return Ok(()),
|
|
SemanticDecision::TakeSource => {
|
|
// A missing source identity is a target-local row, not a removal.
|
|
if key(facet, self.direction.source_layer()).is_none() {
|
|
return Ok(());
|
|
}
|
|
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
|
|
scalar::value(&fact, self.direction.source_layer()).and_then(|v| v.as_bool()).ok_or("Source collection is not applicable; choose an explicit final presence or Keep.")?
|
|
}
|
|
SemanticDecision::Candidate => *self
|
|
.presence
|
|
.get(facet)
|
|
.ok_or("Choose final membership presence.")?,
|
|
};
|
|
project_presence(value, facet, self.direction.target_layer(), present)
|
|
}
|
|
|
|
pub(super) fn member_families(&self, project: &Project) -> Vec<Family> {
|
|
let Some(trace) = self.effective_trace(project) else {
|
|
return vec![];
|
|
};
|
|
let values: Vec<_> = [trace.oa, trace.sa]
|
|
.into_iter()
|
|
.filter_map(|id| Draft::from_project(project, id).map(|d| d.value))
|
|
.collect();
|
|
FAMILIES
|
|
.into_iter()
|
|
.filter(|family| values.iter().any(|value| path(value, *family).is_some()))
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn member_choices(&self, project: &Project, layer: Layer) -> Vec<Choice> {
|
|
if self.member_family == Family::Binding {
|
|
return self.binding_choices(project, layer, true);
|
|
}
|
|
let Some(trace) = self.effective_trace(project) else {
|
|
return vec![];
|
|
};
|
|
let id = if layer == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
let Some(mut draft) = Draft::from_project(project, id) else {
|
|
return vec![];
|
|
};
|
|
if layer == self.direction.target_layer() {
|
|
draft.set_context(self.combined_value(project, true).ok());
|
|
}
|
|
let selected = if layer == Layer::Oa {
|
|
&self.member_oa
|
|
} else {
|
|
&self.member_sa
|
|
};
|
|
let Some(path) = path(&draft.context_value(), self.member_family) else {
|
|
return selected.iter().cloned().collect();
|
|
};
|
|
crate::forms::reference_choices(
|
|
&draft,
|
|
project,
|
|
&format!("{path}/0"),
|
|
selected.as_ref().map_or(Id::nil(), |c| c.id),
|
|
)
|
|
.into_iter()
|
|
.filter(|c| !c.id.is_nil())
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn edit_pair(
|
|
&mut self,
|
|
project: &Project,
|
|
facet: FacetSelector,
|
|
) -> Result<(), String> {
|
|
self.member_family = family(&facet).ok_or("This keyed family remains read-only.")?;
|
|
match facet {
|
|
FacetSelector::Annotation(keys) => {
|
|
self.annotation_oa.selected = keys.oa.map(|key| crate::data_values::FieldPathKey {
|
|
path: key.path,
|
|
field: key.field,
|
|
});
|
|
self.annotation_sa.selected = keys.sa.map(|key| crate::data_values::FieldPathKey {
|
|
path: key.path,
|
|
field: key.field,
|
|
});
|
|
}
|
|
FacetSelector::Binding(keys) => {
|
|
self.member_oa = keys.oa.map(|id| view::choice(project, id));
|
|
self.member_sa = keys.sa.map(|id| view::choice(project, id));
|
|
}
|
|
FacetSelector::Member { keys, .. } => {
|
|
self.member_oa = keys.oa.map(|id| view::choice(project, id));
|
|
self.member_sa = keys.sa.map(|id| view::choice(project, id));
|
|
}
|
|
FacetSelector::Literal(keys) => {
|
|
self.literal_oa = keys.oa.is_some();
|
|
self.literal_sa = keys.sa.is_some();
|
|
self.literal = keys.oa.or(keys.sa).unwrap_or_default();
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
self.tab = Tab::Members;
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn stage_member(&mut self, project: &Project) -> Result<(), String> {
|
|
if !FAMILIES.contains(&self.member_family) {
|
|
return Err("This keyed family remains read-only.".into());
|
|
}
|
|
let facet = match self.member_family {
|
|
Family::Member(collection) => FacetSelector::Member {
|
|
collection,
|
|
keys: KeyPair {
|
|
oa: self.member_oa.as_ref().map(|c| c.id),
|
|
sa: self.member_sa.as_ref().map(|c| c.id),
|
|
},
|
|
},
|
|
Family::Literal => FacetSelector::Literal(KeyPair {
|
|
oa: self.literal_oa.then(|| self.literal.clone()),
|
|
sa: self.literal_sa.then(|| self.literal.clone()),
|
|
}),
|
|
Family::Annotation => FacetSelector::Annotation(KeyPair {
|
|
oa: self
|
|
.annotation_oa
|
|
.selected
|
|
.as_ref()
|
|
.map(|key| AnnotationKey {
|
|
path: key.path.clone(),
|
|
field: key.field,
|
|
}),
|
|
sa: self
|
|
.annotation_sa
|
|
.selected
|
|
.as_ref()
|
|
.map(|key| AnnotationKey {
|
|
path: key.path.clone(),
|
|
field: key.field,
|
|
}),
|
|
}),
|
|
Family::Binding => FacetSelector::Binding(KeyPair {
|
|
oa: self.member_oa.as_ref().map(|c| c.id),
|
|
sa: self.member_sa.as_ref().map(|c| c.id),
|
|
}),
|
|
};
|
|
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
|
|
capture_facet(project, trace.oa, trace.sa, &facet)?;
|
|
self.stage_row_references(&facet)?;
|
|
if let FacetSelector::Member {
|
|
keys:
|
|
KeyPair {
|
|
oa: Some(oa),
|
|
sa: Some(sa),
|
|
},
|
|
..
|
|
} = &facet
|
|
{
|
|
self.references.insert(*oa, *sa);
|
|
}
|
|
self.staged.insert(facet.clone());
|
|
self.selected = Some(facet);
|
|
self.tab = Tab::Facts;
|
|
Ok(())
|
|
}
|
|
}
|