Add typed data contract editing and bounded dictionary views
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user