Add typed data contract editing and bounded dictionary views
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
//! 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())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
if resolve(project, exchange, phase, key).is_none() {
|
||||
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()
|
||||
.filter_map(|field| {
|
||||
resolve(
|
||||
project,
|
||||
exchange,
|
||||
phase,
|
||||
&FieldPathKey {
|
||||
path: path.clone(),
|
||||
field: field.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
.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)
|
||||
}
|
||||
#[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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user