255 lines
9.5 KiB
Rust
255 lines
9.5 KiB
Rust
//! Independent membership and complete final order; the core validates every proposal.
|
|
use super::*;
|
|
|
|
pub(super) fn collection(which: OrderedCollection) -> ReferenceCollection {
|
|
match which {
|
|
OrderedCollection::Steps => ReferenceCollection::Steps,
|
|
OrderedCollection::FragmentBranches => ReferenceCollection::FragmentBranches,
|
|
}
|
|
}
|
|
pub(super) fn ordered(which: ReferenceCollection) -> Option<OrderedCollection> {
|
|
match which {
|
|
ReferenceCollection::Steps => Some(OrderedCollection::Steps),
|
|
ReferenceCollection::FragmentBranches => Some(OrderedCollection::FragmentBranches),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub(super) fn read(value: &Value, which: OrderedCollection) -> Result<Vec<Id>, String> {
|
|
let path = keyed::path(value, keyed::Family::Member(collection(which)))
|
|
.ok_or("This ordered collection is unavailable on the destination.")?;
|
|
serde_json::from_value(value.pointer(&path).unwrap().clone()).map_err(|e| e.to_string())
|
|
}
|
|
fn write(value: &mut Value, which: OrderedCollection, ids: Vec<Id>) -> Result<(), String> {
|
|
let path = keyed::path(value, keyed::Family::Member(collection(which)))
|
|
.ok_or("This ordered collection is unavailable on the destination.")?;
|
|
*value.pointer_mut(&path).unwrap() = serde_json::to_value(ids).expect("Ordered identities");
|
|
Ok(())
|
|
}
|
|
|
|
impl ReviewRow {
|
|
pub(super) fn ordered_members(
|
|
&self,
|
|
project: &Project,
|
|
which: OrderedCollection,
|
|
) -> Result<Vec<Id>, String> {
|
|
read(&self.combine(project, true, true)?, which)
|
|
}
|
|
|
|
pub(super) fn initialize_order(
|
|
&mut self,
|
|
project: &Project,
|
|
which: OrderedCollection,
|
|
) -> Result<(), String> {
|
|
if !self.order_drafts.contains_key(&which) {
|
|
let ids = self.ordered_members(project, which)?;
|
|
self.order_drafts.insert(which, ids);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn reconcile_order(
|
|
&mut self,
|
|
project: &Project,
|
|
which: OrderedCollection,
|
|
) -> Result<(), String> {
|
|
self.require_order_candidate(which)?;
|
|
let members = self.ordered_members(project, which)?;
|
|
let draft = self
|
|
.order_drafts
|
|
.get_mut(&which)
|
|
.ok_or("Choose a final order first.")?;
|
|
// This is an explicit user action. Ordinary refresh never discards an authored order.
|
|
let mut seen = BTreeSet::new();
|
|
draft.retain(|id| members.contains(id) && seen.insert(*id));
|
|
for id in members {
|
|
if seen.insert(id) {
|
|
draft.push(id);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn require_order_candidate(&self, which: OrderedCollection) -> Result<(), String> {
|
|
let facet = FacetSelector::Order(which);
|
|
if self.selected.as_ref() != Some(&facet)
|
|
|| self.decisions.get(&facet) != Some(&SemanticDecision::Candidate)
|
|
{
|
|
return Err("Select Set final value for this order before changing it.".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn move_order(
|
|
&mut self,
|
|
which: OrderedCollection,
|
|
index: usize,
|
|
delta: i32,
|
|
) -> Result<(), String> {
|
|
self.require_order_candidate(which)?;
|
|
let ids = self
|
|
.order_drafts
|
|
.get_mut(&which)
|
|
.ok_or("Choose a final order first.")?;
|
|
let destination = match delta {
|
|
-1 => index.checked_sub(1),
|
|
1 => index.checked_add(1),
|
|
_ => None,
|
|
};
|
|
let destination = destination
|
|
.filter(|i| index < ids.len() && *i < ids.len())
|
|
.ok_or("Choose an existing adjacent position in the final order.")?;
|
|
ids.swap(index, destination);
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn project_order(
|
|
&self,
|
|
project: &Project,
|
|
trace: &Trace,
|
|
value: &mut Value,
|
|
which: OrderedCollection,
|
|
decision: SemanticDecision,
|
|
) -> Result<(), String> {
|
|
let current = read(value, which)?;
|
|
let supplied = match decision {
|
|
SemanticDecision::KeepTarget => return Ok(()),
|
|
SemanticDecision::Candidate => self
|
|
.order_drafts
|
|
.get(&which)
|
|
.cloned()
|
|
.ok_or("Choose a final order.")?,
|
|
SemanticDecision::TakeSource => {
|
|
let fact =
|
|
capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?;
|
|
let source: Vec<Id> = serde_json::from_value(
|
|
scalar::value(&fact, self.direction.source_layer())
|
|
.ok_or("The source has no applicable order.")?,
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
let map = self.order_reference_map()?;
|
|
source
|
|
.into_iter()
|
|
.map(|id| {
|
|
map.get(&id).copied().ok_or_else(|| {
|
|
format!(
|
|
"Pair source order member {} explicitly in Reference pairs.",
|
|
project.named(id)
|
|
)
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
}
|
|
};
|
|
let actual: BTreeSet<_> = current.iter().copied().collect();
|
|
let desired: BTreeSet<_> = supplied.iter().copied().collect();
|
|
if desired.len() != supplied.len() {
|
|
return Err(
|
|
"The final order contains a duplicate identity; reconcile its members explicitly."
|
|
.into(),
|
|
);
|
|
}
|
|
if desired != actual {
|
|
if decision == SemanticDecision::TakeSource
|
|
&& desired.is_subset(&actual)
|
|
&& current
|
|
.iter()
|
|
.filter(|id| desired.contains(id))
|
|
.copied()
|
|
.collect::<Vec<_>>()
|
|
== supplied
|
|
{
|
|
return Ok(());
|
|
}
|
|
return Err("Final order must contain every surviving destination member exactly once. Reconcile members, then choose their positions; membership is reviewed separately.".into());
|
|
}
|
|
write(value, which, supplied)
|
|
}
|
|
|
|
fn order_reference_map(&self) -> Result<BTreeMap<Id, Id>, String> {
|
|
let mut map = BTreeMap::new();
|
|
for (oa, sa) in &self.references {
|
|
let (a, b) = if self.direction == TransitionDirection::OaToSa {
|
|
(*oa, *sa)
|
|
} else {
|
|
(*sa, *oa)
|
|
};
|
|
if map.insert(a, b).is_some_and(|old| old != b) {
|
|
return Err("Reverse reference correspondence is ambiguous.".into());
|
|
}
|
|
}
|
|
Ok(map)
|
|
}
|
|
|
|
pub(super) fn project_ordered_member(
|
|
&self,
|
|
project: &Project,
|
|
trace: &Trace,
|
|
value: &mut Value,
|
|
facet: &FacetSelector,
|
|
decision: SemanticDecision,
|
|
form_context: bool,
|
|
) -> Result<(), String> {
|
|
let FacetSelector::Member {
|
|
collection: member_collection,
|
|
keys,
|
|
} = facet
|
|
else {
|
|
unreachable!()
|
|
};
|
|
let which = ordered(*member_collection).expect("Ordered member");
|
|
let before = read(value, which)?;
|
|
self.keyed_value(project, trace, value, facet, decision)?;
|
|
let after = read(value, which)?;
|
|
let explicit = self
|
|
.decisions
|
|
.get(&FacetSelector::Order(which))
|
|
.is_some_and(|d| *d != SemanticDecision::KeepTarget);
|
|
if decision != SemanticDecision::TakeSource || explicit || after.len() <= before.len() {
|
|
return Ok(());
|
|
}
|
|
let (source_key, target_key) = if self.direction == TransitionDirection::OaToSa {
|
|
(keys.oa, keys.sa)
|
|
} else {
|
|
(keys.sa, keys.oa)
|
|
};
|
|
let (Some(source_key), Some(target_key)) = (source_key, target_key) else {
|
|
return Ok(());
|
|
};
|
|
let fact = capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?;
|
|
let source: Vec<Id> = serde_json::from_value(
|
|
scalar::value(&fact, self.direction.source_layer())
|
|
.ok_or("Source order is unavailable.")?,
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
let position = source
|
|
.iter()
|
|
.position(|id| *id == source_key)
|
|
.ok_or("Source member is absent.")?;
|
|
let map = self.order_reference_map()?;
|
|
let anchor = |id: &Id| {
|
|
map.get(id)
|
|
.and_then(|mapped| before.iter().position(|id| id == mapped))
|
|
};
|
|
let left = source[..position].iter().rev().find_map(anchor);
|
|
let right = source[position + 1..].iter().find_map(anchor);
|
|
let at = match (left, right) {
|
|
(Some(a), Some(b)) if a + 1 == b => Some(b),
|
|
(Some(a), None) if a + 1 == before.len() => Some(a + 1),
|
|
(None, Some(0)) => Some(0),
|
|
(None, None) if before.is_empty() => Some(0),
|
|
_ => None,
|
|
};
|
|
if let Some(at) = at {
|
|
let mut ordered = before;
|
|
ordered.insert(at, target_key);
|
|
write(value, which, ordered)
|
|
} else if form_context {
|
|
// An uncommitted membership list lets the user author a final order.
|
|
// The actual request still rejects ambiguous placement until explicitly decided.
|
|
Ok(())
|
|
} else {
|
|
Err("Ordered member insertion has no unique unchanged anchor; select an explicit final Order in Properties.".into())
|
|
}
|
|
}
|
|
}
|