//! 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, 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::>() .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, 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 { 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 { 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 { 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, 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::>(); 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::>(); 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") ); } }