486 lines
17 KiB
Rust
486 lines
17 KiB
Rust
//! One address for a carried value, shared by scenario creation and property editing.
|
|
use oa_core::*;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct FieldPathKey {
|
|
pub path: Vec<Id>,
|
|
pub field: Id,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct FieldPathChoice {
|
|
pub key: FieldPathKey,
|
|
pub label: String,
|
|
}
|
|
impl std::fmt::Display for FieldPathChoice {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(&self.label)
|
|
}
|
|
}
|
|
|
|
fn phase_allows(project: &Project, entry: &data::CarriedFieldPath, phase: &MessageKind) -> bool {
|
|
data::item_mechanism(project, entry.item) != Some(ExchangeMechanism::Operation)
|
|
|| match phase {
|
|
MessageKind::Call => matches!(
|
|
entry.direction,
|
|
ParameterDirection::Unspecified
|
|
| ParameterDirection::In
|
|
| ParameterDirection::InOut
|
|
),
|
|
MessageKind::Reply => matches!(
|
|
entry.direction,
|
|
ParameterDirection::Unspecified
|
|
| ParameterDirection::Out
|
|
| ParameterDirection::InOut
|
|
| ParameterDirection::Return
|
|
),
|
|
_ => true,
|
|
}
|
|
}
|
|
fn named_choice(project: &Project, entry: data::CarriedFieldPath) -> FieldPathChoice {
|
|
let qualified = entry
|
|
.path
|
|
.iter()
|
|
.chain(std::iter::once(&entry.field))
|
|
.map(|id| {
|
|
project
|
|
.elements
|
|
.get(id)
|
|
.map(|e| format!("{} (@{})", e.name, e.alias))
|
|
.unwrap_or_else(|| id.to_string())
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" → ");
|
|
let direction =
|
|
if data::item_mechanism(project, entry.item) == Some(ExchangeMechanism::Operation) {
|
|
match entry.direction {
|
|
ParameterDirection::Unspecified => " · direction unspecified — review",
|
|
ParameterDirection::In => " · In",
|
|
ParameterDirection::Out => " · Out",
|
|
ParameterDirection::InOut => " · InOut",
|
|
ParameterDirection::Return => " · Return",
|
|
}
|
|
} else {
|
|
""
|
|
};
|
|
FieldPathChoice {
|
|
key: FieldPathKey {
|
|
path: entry.path,
|
|
field: entry.field,
|
|
},
|
|
label: format!("{} / {qualified}{direction}", project.named(entry.item)),
|
|
}
|
|
}
|
|
|
|
pub fn choices(
|
|
project: &Project,
|
|
exchange: &Relation,
|
|
phase: &MessageKind,
|
|
) -> Result<Vec<FieldPathChoice>, String> {
|
|
Ok(data::try_carried_field_paths(project, exchange)?
|
|
.into_iter()
|
|
.filter(|entry| phase_allows(project, entry, phase))
|
|
.map(|entry| named_choice(project, entry))
|
|
.collect())
|
|
}
|
|
|
|
/// Direct carried fields only: an unrelated deep branch must not prevent
|
|
/// browsing a root field. Preserve the core resolver's first carried owner.
|
|
pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec<FieldPathChoice> {
|
|
let mut seen = std::collections::BTreeSet::new();
|
|
exchange
|
|
.body
|
|
.items()
|
|
.iter()
|
|
.flat_map(|item| {
|
|
data::effective_fields(project, *item)
|
|
.into_iter()
|
|
.filter_map(|field| {
|
|
let SharedBody::DataField { direction, .. } = field.shared()? else {
|
|
return None;
|
|
};
|
|
Some(data::CarriedFieldPath {
|
|
item: *item,
|
|
path: vec![],
|
|
field: field.id,
|
|
direction: *direction,
|
|
})
|
|
})
|
|
})
|
|
.filter(|entry| seen.insert(entry.field))
|
|
.filter(|entry| phase_allows(project, entry, phase))
|
|
.map(|entry| named_choice(project, entry))
|
|
.collect()
|
|
}
|
|
|
|
/// Resolve authored finite paths as well as the finite representative picker list.
|
|
/// In particular, optional recursive Value records may be traversed repeatedly.
|
|
pub fn resolve(
|
|
project: &Project,
|
|
exchange: &Relation,
|
|
phase: &MessageKind,
|
|
key: &FieldPathKey,
|
|
) -> Option<FieldPathChoice> {
|
|
data::carried_value_path(project, exchange, &key.path, key.field)
|
|
.filter(|entry| phase_allows(project, entry, phase))
|
|
.map(|entry| named_choice(project, entry))
|
|
}
|
|
|
|
pub fn parent(key: &FieldPathKey) -> Option<FieldPathKey> {
|
|
let (&field, path) = key.path.split_last()?;
|
|
Some(FieldPathKey {
|
|
path: path.to_vec(),
|
|
field,
|
|
})
|
|
}
|
|
|
|
/// Expand exactly one selected record. This keeps recursive authoring explicit
|
|
/// and finite, and never traverses Reference storage or manufactures value paths.
|
|
pub fn children(
|
|
project: &Project,
|
|
exchange: &Relation,
|
|
phase: &MessageKind,
|
|
key: &FieldPathKey,
|
|
) -> Result<Vec<FieldPathChoice>, String> {
|
|
let Some(entry) = data::carried_value_path(project, exchange, &key.path, key.field)
|
|
.filter(|entry| phase_allows(project, entry, phase))
|
|
else {
|
|
return Ok(vec![]);
|
|
};
|
|
let Some(SharedBody::DataField {
|
|
type_ref: TypeRef::Named(record),
|
|
storage: FieldStorage::Value,
|
|
..
|
|
}) = project.elements.get(&key.field).and_then(Element::shared)
|
|
else {
|
|
return Ok(vec![]);
|
|
};
|
|
if !data::is_record(project, *record) {
|
|
return Ok(vec![]);
|
|
}
|
|
if key.path.len() >= data::MAX_VALUE_PATH_DEPTH {
|
|
return Err(format!(
|
|
"A value path can contain at most {} enclosing fields.",
|
|
data::MAX_VALUE_PATH_DEPTH
|
|
));
|
|
}
|
|
let mut path = key.path.clone();
|
|
path.push(key.field);
|
|
Ok(data::effective_fields(project, *record)
|
|
.into_iter()
|
|
.map(|field| {
|
|
named_choice(
|
|
project,
|
|
data::CarriedFieldPath {
|
|
item: entry.item,
|
|
path: path.clone(),
|
|
field: field.id,
|
|
direction: entry.direction,
|
|
},
|
|
)
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod tests {
|
|
use super::*;
|
|
|
|
pub struct Payload {
|
|
pub project: Project,
|
|
pub exchange: Id,
|
|
pub item: Id,
|
|
pub leaf: Id,
|
|
pub origin: Id,
|
|
pub destination: Id,
|
|
pub scenario: Id,
|
|
}
|
|
pub fn payload() -> Payload {
|
|
let mut p = Project::empty();
|
|
let item = p
|
|
.add_element(Kind::ExchangeItem, Layer::Oa, "Move", None)
|
|
.unwrap();
|
|
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
|
kind: ExchangeItemKind::Information,
|
|
mechanism: ExchangeMechanism::Operation,
|
|
supertype: None,
|
|
};
|
|
let record = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Coordinate", None)
|
|
.unwrap();
|
|
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: None },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
let leaf = p
|
|
.add_element(Kind::DataField, Layer::Oa, "latitude", Some(record))
|
|
.unwrap();
|
|
let origin = p
|
|
.add_element(Kind::DataField, Layer::Oa, "origin", Some(item))
|
|
.unwrap();
|
|
let destination = p
|
|
.add_element(Kind::DataField, Layer::Oa, "destination", Some(item))
|
|
.unwrap();
|
|
for (id, type_ref, direction) in [
|
|
(leaf, TypeRef::Real, ParameterDirection::Unspecified),
|
|
(origin, TypeRef::Named(record), ParameterDirection::In),
|
|
(destination, TypeRef::Named(record), ParameterDirection::Out),
|
|
] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataField {
|
|
type_ref,
|
|
direction,
|
|
minimum: 1,
|
|
maximum: Some(1),
|
|
unit: String::new(),
|
|
storage: FieldStorage::Value,
|
|
default: None,
|
|
};
|
|
}
|
|
let source = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Request", None)
|
|
.unwrap();
|
|
let target = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Perform", None)
|
|
.unwrap();
|
|
let exchange = p.relate(RelationKind::Interaction, "Movement", source, target);
|
|
p.relations.get_mut(&exchange).unwrap().body = RelationBody::Interaction {
|
|
source,
|
|
target,
|
|
items: vec![item],
|
|
};
|
|
let scenario = p
|
|
.add_element(Kind::Scenario, Layer::Oa, "Conversation", None)
|
|
.unwrap();
|
|
validate_candidate(&p).unwrap();
|
|
Payload {
|
|
project: p,
|
|
exchange,
|
|
item,
|
|
leaf,
|
|
origin,
|
|
destination,
|
|
scenario,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn phase_choices_keep_reused_record_addresses_distinct() {
|
|
let mut f = payload();
|
|
assert_eq!(
|
|
data::item_mechanism(&f.project, f.item),
|
|
Some(ExchangeMechanism::Operation)
|
|
);
|
|
let call = choices(
|
|
&f.project,
|
|
&f.project.relations[&f.exchange],
|
|
&MessageKind::Call,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
call.iter()
|
|
.any(|c| c.key.path == [f.origin] && c.key.field == f.leaf)
|
|
);
|
|
assert!(!call.iter().any(|c| c.key.path == [f.destination]));
|
|
let reply = choices(
|
|
&f.project,
|
|
&f.project.relations[&f.exchange],
|
|
&MessageKind::Reply,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
reply
|
|
.iter()
|
|
.any(|c| c.key.path == [f.destination] && c.key.field == f.leaf)
|
|
);
|
|
assert!(!reply.iter().any(|c| c.key.path == [f.origin]));
|
|
if let Some(SharedBody::DataField { direction, .. }) = f
|
|
.project
|
|
.elements
|
|
.get_mut(&f.destination)
|
|
.unwrap()
|
|
.shared_mut()
|
|
{
|
|
*direction = ParameterDirection::InOut;
|
|
}
|
|
let both = choices(
|
|
&f.project,
|
|
&f.project.relations[&f.exchange],
|
|
&MessageKind::Call,
|
|
)
|
|
.unwrap();
|
|
let leaves = both
|
|
.iter()
|
|
.filter(|c| c.key.field == f.leaf)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(leaves.len(), 2);
|
|
assert_ne!(leaves[0].key, leaves[1].key);
|
|
assert_ne!(leaves[0].label, leaves[1].label);
|
|
let values = leaves
|
|
.into_iter()
|
|
.map(|c| (c.key.clone(), "1".to_string()))
|
|
.collect::<std::collections::BTreeMap<_, _>>();
|
|
assert_eq!(values.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn reference_parameter_stops_descent_and_unspecified_direction_remains_visible() {
|
|
let mut f = payload();
|
|
if let Some(SharedBody::DataField {
|
|
storage, direction, ..
|
|
}) = f.project.elements.get_mut(&f.origin).unwrap().shared_mut()
|
|
{
|
|
*storage = FieldStorage::Reference;
|
|
*direction = ParameterDirection::Unspecified;
|
|
}
|
|
let call = choices(
|
|
&f.project,
|
|
&f.project.relations[&f.exchange],
|
|
&MessageKind::Call,
|
|
)
|
|
.unwrap();
|
|
let direct = call.iter().find(|c| c.key.field == f.origin).unwrap();
|
|
assert!(direct.key.path.is_empty());
|
|
assert!(direct.label.contains("unspecified"));
|
|
assert!(!call.iter().any(|c| c.key.path.contains(&f.origin)));
|
|
}
|
|
pub fn recursive_payload() -> (Payload, Id) {
|
|
let mut f = payload();
|
|
let record = f.project.elements[&f.leaf].parent.unwrap();
|
|
let next = f
|
|
.project
|
|
.add_element(Kind::DataField, Layer::Oa, "next", Some(record))
|
|
.unwrap();
|
|
*f.project
|
|
.elements
|
|
.get_mut(&next)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::DataField {
|
|
type_ref: TypeRef::Named(record),
|
|
minimum: 0,
|
|
maximum: Some(1),
|
|
unit: String::new(),
|
|
direction: ParameterDirection::Unspecified,
|
|
storage: FieldStorage::Value,
|
|
default: None,
|
|
};
|
|
validate_candidate(&f.project).unwrap();
|
|
(f, next)
|
|
}
|
|
pub fn wide_payload() -> Payload {
|
|
let mut f = payload();
|
|
let record = f.project.elements[&f.leaf].parent.unwrap();
|
|
for (owner, template, count) in [(f.item, f.origin, 95), (record, f.leaf, 127)] {
|
|
for index in 0..count {
|
|
let body = f.project.elements[&template].shared().unwrap().clone();
|
|
let id = f
|
|
.project
|
|
.add_element(
|
|
Kind::DataField,
|
|
Layer::Oa,
|
|
&format!("Field {template} {index}"),
|
|
Some(owner),
|
|
)
|
|
.unwrap();
|
|
*f.project
|
|
.elements
|
|
.get_mut(&id)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = body;
|
|
}
|
|
}
|
|
f
|
|
}
|
|
#[test]
|
|
fn roots_and_one_hop_children_do_not_expand_unrelated_payload_branches() {
|
|
let mut f = wide_payload();
|
|
let exchange = &f.project.relations[&f.exchange];
|
|
let direct = roots(&f.project, exchange, &MessageKind::Call);
|
|
assert_eq!(direct.len(), 96);
|
|
assert!(direct.iter().all(|choice| choice.key.path.is_empty()));
|
|
let representative = choices(&f.project, exchange, &MessageKind::Call).unwrap();
|
|
assert_eq!(representative.len(), 96 * 129);
|
|
for root in &direct {
|
|
assert_eq!(
|
|
resolve(&f.project, exchange, &MessageKind::Call, &root.key),
|
|
Some(root.clone())
|
|
);
|
|
}
|
|
let nested = children(&f.project, exchange, &MessageKind::Call, &direct[0].key).unwrap();
|
|
assert_eq!(nested.len(), 128);
|
|
for child in &nested {
|
|
assert_eq!(
|
|
resolve(&f.project, exchange, &MessageKind::Call, &child.key),
|
|
Some(child.clone())
|
|
);
|
|
assert!(child.label.ends_with(" · In"));
|
|
}
|
|
// A temporarily over-limit model still exposes its unrelated root fields;
|
|
// the editor must not expand the whole graph before showing a choice.
|
|
for index in 0..32 {
|
|
let body = f.project.elements[&f.origin].shared().unwrap().clone();
|
|
let id = f
|
|
.project
|
|
.add_element(
|
|
Kind::DataField,
|
|
Layer::Oa,
|
|
&format!("Extra {index}"),
|
|
Some(f.item),
|
|
)
|
|
.unwrap();
|
|
*f.project
|
|
.elements
|
|
.get_mut(&id)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = body;
|
|
}
|
|
let exchange = &f.project.relations[&f.exchange];
|
|
assert!(choices(&f.project, exchange, &MessageKind::Call).is_err());
|
|
assert_eq!(roots(&f.project, exchange, &MessageKind::Call).len(), 128);
|
|
assert_eq!(
|
|
children(&f.project, exchange, &MessageKind::Call, &direct[0].key)
|
|
.unwrap()
|
|
.len(),
|
|
128
|
|
);
|
|
}
|
|
#[test]
|
|
fn recursive_records_can_be_drilled_into_and_valid_imported_paths_resolve() {
|
|
let (f, next) = recursive_payload();
|
|
let exchange = &f.project.relations[&f.exchange];
|
|
let record = FieldPathKey {
|
|
path: vec![f.origin],
|
|
field: next,
|
|
};
|
|
let nested = children(&f.project, exchange, &MessageKind::Call, &record).unwrap();
|
|
let leaf = nested.iter().find(|c| c.key.field == f.leaf).unwrap();
|
|
assert_eq!(leaf.key.path, vec![f.origin, next]);
|
|
assert!(
|
|
!choices(&f.project, exchange, &MessageKind::Call)
|
|
.unwrap()
|
|
.iter()
|
|
.any(|c| c.key == leaf.key)
|
|
);
|
|
assert_eq!(
|
|
resolve(&f.project, exchange, &MessageKind::Call, &leaf.key),
|
|
Some(leaf.clone())
|
|
);
|
|
assert_eq!(parent(&leaf.key), Some(record));
|
|
let deepest = FieldPathKey {
|
|
path: std::iter::once(f.origin)
|
|
.chain(std::iter::repeat_n(next, data::MAX_VALUE_PATH_DEPTH - 1))
|
|
.collect(),
|
|
field: next,
|
|
};
|
|
assert!(resolve(&f.project, exchange, &MessageKind::Call, &deepest).is_some());
|
|
assert!(
|
|
children(&f.project, exchange, &MessageKind::Call, &deepest)
|
|
.unwrap_err()
|
|
.contains("128")
|
|
);
|
|
}
|
|
}
|