Add typed data contract editing and bounded dictionary views

This commit is contained in:
2026-09-06 03:58:43 +03:00
parent 7426c03f4e
commit b403039d00
14 changed files with 2956 additions and 137 deletions
+328 -26
View File
@@ -78,7 +78,8 @@ pub enum Event {
SecondGuard(String),
Operator(Operator),
Binding(Id, Choice),
Value(Id, String),
Value(crate::data_values::FieldPathKey, String),
ValueFocus(crate::data_values::FieldPathKey),
}
#[derive(Default)]
pub struct State {
@@ -100,7 +101,8 @@ struct Draft {
second_guard: String,
operator: FragmentOperator,
bindings: BTreeMap<Id, Id>,
values: BTreeMap<Id, String>,
values: BTreeMap<crate::data_values::FieldPathKey, String>,
value_focus: Option<crate::data_values::FieldPathKey>,
}
impl State {
pub(super) fn pending_owner(&self) -> Option<Id> {
@@ -146,6 +148,7 @@ impl State {
operator: FragmentOperator::Alternative,
bindings: BTreeMap::new(),
values: BTreeMap::new(),
value_focus: None,
});
}
Event::Cancel => self.acknowledge_commit(),
@@ -161,21 +164,34 @@ impl State {
d.bindings.clear();
}
Event::Exchange(c) => {
d.exchange = Some(c.id);
d.values.clear();
d.sender = None;
d.receiver = None;
if d.exchange != Some(c.id) {
d.exchange = Some(c.id);
d.values.clear();
d.value_focus = None;
d.sender = None;
d.receiver = None;
}
}
Event::Sender(c) => d.sender = Some(c.id),
Event::Receiver(c) => d.receiver = Some(c.id),
Event::Parent(c) => d.parent = c.id,
Event::Style(style) => {
let previous = d.style;
d.style = style;
d.call = None;
d.exchange = None;
d.sender = None;
d.receiver = None;
d.values.clear();
if previous != style {
if previous == MessageStyle::Timer || style == MessageStyle::Timer {
d.exchange = None;
d.sender = None;
d.receiver = None;
d.call = None;
} else if (previous == MessageStyle::Reply)
!= (style == MessageStyle::Reply)
{
std::mem::swap(&mut d.sender, &mut d.receiver);
d.call = None;
}
d.retain_value_paths(p);
}
}
Event::Call(c) => {
d.call = Some(c.id);
@@ -190,6 +206,10 @@ impl State {
..
}) = p.elements.get(&c.id).and_then(|e| e.shared())
{
if d.exchange != *exchange {
d.values.clear();
d.value_focus = None;
}
d.exchange = *exchange;
d.sender = *receiver;
d.receiver = *sender;
@@ -202,6 +222,14 @@ impl State {
Event::Value(id, s) => {
d.values.insert(id, s);
}
Event::ValueFocus(key)
if d.exchange
.and_then(|id| p.relations.get(&id))
.and_then(|r| crate::data_values::resolve(p, r, &d.kind(), &key))
.is_some() =>
{
d.value_focus = Some(key);
}
_ => {}
}
}
@@ -322,7 +350,7 @@ fn exchanges(p: &Project, d: &Draft) -> Vec<Choice> {
exchange: Some(r.id),
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
message_kind: d.kind(),
reply_to: None,
values: vec![],
},
@@ -373,6 +401,37 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
.collect()
}
impl Draft {
fn retain_value_paths(&mut self, p: &Project) {
let kind = self.kind();
let exchange = self.exchange.and_then(|id| p.relations.get(&id));
self.values.retain(|key, _| {
exchange
.and_then(|r| crate::data_values::resolve(p, r, &kind, key))
.is_some()
});
if self.value_focus.as_ref().is_some_and(|key| {
exchange
.and_then(|r| crate::data_values::resolve(p, r, &kind, key))
.is_none()
}) {
self.value_focus = None;
}
}
fn value_choices(
&self,
p: &Project,
exchange: &Relation,
) -> Result<Vec<crate::data_values::FieldPathChoice>, String> {
let mut choices = crate::data_values::choices(p, exchange, &self.kind())?;
for key in self.values.keys().chain(self.value_focus.iter()) {
if !choices.iter().any(|choice| &choice.key == key)
&& let Some(choice) = crate::data_values::resolve(p, exchange, &self.kind(), key)
{
choices.push(choice);
}
}
Ok(choices)
}
fn kind(&self) -> MessageKind {
match self.style {
MessageStyle::Asynchronous => MessageKind::Asynchronous,
@@ -405,7 +464,8 @@ impl Draft {
.iter()
.filter(|(_, v)| !v.trim().is_empty())
.map(|(field, expression)| MessageValue {
field: *field,
path: field.path.clone(),
field: field.field,
expression: expression.clone(),
})
.collect(),
@@ -762,20 +822,65 @@ pub fn view<'a>(app: &'a App, owner: Id, mut out: Column<'a, Message>) -> Elemen
);
}
if let Some(exchange) = d.exchange.and_then(|id| p.relations.get(&id)) {
for field in validation::carried_fields(p, exchange) {
form = form.push(
row![
label(p.named(field)),
text_input(
"Optional value expression",
d.values.get(&field).map(String::as_str).unwrap_or("")
)
.on_input(move |s| event(Event::Value(field, s)))
match d.value_choices(p, exchange) {
Ok(choices) => {
let focus = d.value_focus.as_ref().and_then(|key| {
crate::data_values::resolve(p, exchange, &d.kind(), key)
});
form = form.push(
pick_list(choices.clone(), focus.clone(), |choice| {
event(Event::ValueFocus(choice.key))
})
.placeholder("Explore a record or choose a value path…")
.text_size(12)
.padding(8)
.size(12)
]
.spacing(7),
);
.width(Fill),
);
if let Some(focus) = focus {
if let Some(parent) = crate::data_values::parent(&focus.key) {
form = form.push(
button(text("Up to enclosing field").size(11))
.on_press(event(Event::ValueFocus(parent))),
);
}
match crate::data_values::children(p, exchange, &d.kind(), &focus.key) {
Ok(children) if !children.is_empty() => {
form = form.push(
pick_list(
children,
None::<crate::data_values::FieldPathChoice>,
|choice| event(Event::ValueFocus(choice.key)),
)
.placeholder("Choose a field inside this record…")
.text_size(12)
.padding(8)
.width(Fill),
)
}
Err(error) => {
form = form.push(text(error).size(11).color(style::MUTED))
}
_ => {}
}
}
for choice in choices {
let key = choice.key;
form = form.push(
column![
text(choice.label).size(11),
text_input(
"Optional value expression",
d.values.get(&key).map(String::as_str).unwrap_or("")
)
.on_input(move |s| event(Event::Value(key.clone(), s)))
.padding(8)
.size(12)
]
.spacing(4),
);
}
}
Err(error) => form = form.push(text(error).size(12).color(style::MUTED)),
}
}
if d.style == MessageStyle::CallReply {
@@ -1189,3 +1294,200 @@ mod tests {
);
}
}
#[cfg(test)]
mod data_contract_tests {
use super::*;
#[test]
fn scenario_form_commits_two_reused_record_values_and_correlated_reply_atomically() {
let mut f = crate::data_values::tests::payload();
if let Some(SharedBody::DataField { direction, .. }) = f
.project
.elements
.get_mut(&f.destination)
.unwrap()
.shared_mut()
{
*direction = ParameterDirection::InOut;
}
let source = f.project.relations[&f.exchange].source();
let target = f.project.relations[&f.exchange].target();
let mut lines = vec![];
for (name, represented) in [("Requester", source), ("Performer", target)] {
let id = f
.project
.add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario))
.unwrap();
*f.project
.elements
.get_mut(&id)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Lifeline {
represented: Some(represented),
};
lines.push(id);
}
let mut state = State::default();
for event in [
Event::Start(f.scenario, Tool::Message),
Event::Name("Move coordinates".into()),
Event::Exchange(named_choice(&f.project, f.exchange)),
Event::Sender(named_choice(&f.project, lines[0])),
Event::Receiver(named_choice(&f.project, lines[1])),
] {
state.update(&f.project, event).unwrap();
}
assert!(
exchanges(&f.project, state.draft.as_ref().unwrap())
.iter()
.any(|c| c.id == f.exchange)
);
for (parameter, expression) in [(f.origin, "45.0"), (f.destination, "46.0")] {
state
.update(
&f.project,
Event::Value(
crate::data_values::FieldPathKey {
path: vec![parameter],
field: f.leaf,
},
expression.into(),
),
)
.unwrap();
}
let before = state.draft.as_ref().unwrap().values.clone();
state
.update(&f.project, Event::Style(MessageStyle::Call))
.unwrap();
state
.update(
&f.project,
Event::Exchange(named_choice(&f.project, f.exchange)),
)
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().values, before);
assert_eq!(state.draft.as_ref().unwrap().sender, Some(lines[0]));
assert_eq!(state.draft.as_ref().unwrap().receiver, Some(lines[1]));
state
.update(&f.project, Event::Style(MessageStyle::CallReply))
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().values, before);
let command = state.prepare_pending(&f.project).unwrap();
let mut document =
crate::document::Document::from_project(f.project.clone(), Default::default()).unwrap();
document.execute(command).unwrap();
let messages = document
.project()
.children(f.scenario)
.filter_map(|e| match e.shared()? {
SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind,
values,
..
},
..
} => Some((message_kind, values)),
_ => None,
})
.collect::<Vec<_>>();
let call = messages
.iter()
.find(|(kind, _)| **kind == MessageKind::Call)
.unwrap()
.1;
assert_eq!(call.len(), 2);
assert_eq!(
call.iter().map(|v| v.field).collect::<BTreeSet<_>>(),
BTreeSet::from([f.leaf])
);
assert_eq!(
call.iter().map(|v| v.path.clone()).collect::<BTreeSet<_>>(),
BTreeSet::from([vec![f.origin], vec![f.destination]])
);
assert!(
messages
.iter()
.find(|(kind, _)| **kind == MessageKind::Reply)
.unwrap()
.1
.is_empty()
);
assert!(document.undo());
assert_eq!(document.project(), &f.project);
assert!(!document.undo());
}
#[test]
fn recursive_value_focus_exposes_authored_leaf_and_phase_changes_retain_surviving_keys() {
let (f, next) = crate::data_values::tests::recursive_payload();
let mut state = State::default();
state
.update(&f.project, Event::Start(f.scenario, Tool::Message))
.unwrap();
state
.update(
&f.project,
Event::Exchange(named_choice(&f.project, f.exchange)),
)
.unwrap();
let key = crate::data_values::FieldPathKey {
path: vec![f.origin, next],
field: f.leaf,
};
state
.update(&f.project, Event::ValueFocus(key.clone()))
.unwrap();
assert!(
state
.draft
.as_ref()
.unwrap()
.value_choices(&f.project, &f.project.relations[&f.exchange])
.unwrap()
.iter()
.any(|c| c.key == key)
);
state
.update(
&f.project,
Event::Value(key.clone(), "recursive expression".into()),
)
.unwrap();
state
.update(&f.project, Event::Style(MessageStyle::Call))
.unwrap();
assert_eq!(
state.draft.as_ref().unwrap().values[&key],
"recursive expression"
);
state
.update(&f.project, Event::Style(MessageStyle::Asynchronous))
.unwrap();
assert_eq!(
state.draft.as_ref().unwrap().values[&key],
"recursive expression"
);
state
.update(&f.project, Event::Style(MessageStyle::Reply))
.unwrap();
assert!(
state.draft.as_ref().unwrap().values.is_empty(),
"In parameters do not become reply values"
);
let out = crate::data_values::FieldPathKey {
path: vec![f.destination],
field: f.leaf,
};
state
.update(&f.project, Event::Value(out.clone(), "reply value".into()))
.unwrap();
state
.update(&f.project, Event::Style(MessageStyle::Asynchronous))
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().values[&out], "reply value");
}
}