Bound scenario value authoring to focused paged fields

This commit is contained in:
2026-09-06 04:08:46 +03:00
parent 15ed1ff3af
commit ab661f2d33
2 changed files with 680 additions and 81 deletions
+563 -74
View File
@@ -80,7 +80,15 @@ pub enum Event {
Binding(Id, Choice),
Value(crate::data_values::FieldPathKey, String),
ValueFocus(crate::data_values::FieldPathKey),
ValueBrowse(Option<crate::data_values::FieldPathKey>),
ValuePage(ValueSection, usize),
}
#[derive(Debug, Clone, Copy)]
pub enum ValueSection {
Fields,
Authored,
}
const VALUE_PAGE_SIZE: usize = 24;
#[derive(Default)]
pub struct State {
draft: Option<Draft>,
@@ -103,6 +111,9 @@ struct Draft {
bindings: BTreeMap<Id, Id>,
values: BTreeMap<crate::data_values::FieldPathKey, String>,
value_focus: Option<crate::data_values::FieldPathKey>,
value_browse: Option<crate::data_values::FieldPathKey>,
value_browse_page: usize,
value_authored_page: usize,
}
impl State {
pub(super) fn pending_owner(&self) -> Option<Id> {
@@ -149,6 +160,9 @@ impl State {
bindings: BTreeMap::new(),
values: BTreeMap::new(),
value_focus: None,
value_browse: None,
value_browse_page: 0,
value_authored_page: 0,
});
}
Event::Cancel => self.acknowledge_commit(),
@@ -166,8 +180,7 @@ impl State {
Event::Exchange(c) => {
if d.exchange != Some(c.id) {
d.exchange = Some(c.id);
d.values.clear();
d.value_focus = None;
d.reset_values();
d.sender = None;
d.receiver = None;
}
@@ -207,8 +220,7 @@ impl State {
}) = p.elements.get(&c.id).and_then(|e| e.shared())
{
if d.exchange != *exchange {
d.values.clear();
d.value_focus = None;
d.reset_values();
}
d.exchange = *exchange;
d.sender = *receiver;
@@ -230,6 +242,21 @@ impl State {
{
d.value_focus = Some(key);
}
Event::ValueBrowse(key) => {
if key.as_ref().is_none_or(|key| {
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_browse = key;
d.value_browse_page = 0;
}
}
Event::ValuePage(section, page) => match section {
ValueSection::Fields => d.value_browse_page = page,
ValueSection::Authored => d.value_authored_page = page,
},
_ => {}
}
}
@@ -389,6 +416,9 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
let mut candidate = d.clone();
candidate.style = MessageStyle::Reply;
candidate.call = Some(e.id);
if candidate.exchange != *exchange {
candidate.reset_values();
}
candidate.exchange = *exchange;
candidate.sender = *receiver;
candidate.receiver = *sender;
@@ -401,6 +431,13 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
.collect()
}
impl Draft {
fn reset_values(&mut self) {
self.values.clear();
self.value_focus = None;
self.value_browse = None;
self.value_browse_page = 0;
self.value_authored_page = 0;
}
fn retain_value_paths(&mut self, p: &Project) {
let kind = self.kind();
let exchange = self.exchange.and_then(|id| p.relations.get(&id));
@@ -416,21 +453,31 @@ impl Draft {
}) {
self.value_focus = None;
}
if self.value_browse.as_ref().is_some_and(|key| {
exchange
.and_then(|r| crate::data_values::resolve(p, r, &kind, key))
.is_none()
}) {
self.value_browse = None;
self.value_browse_page = 0;
}
}
fn value_choices(
fn browsed_values(
&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);
}
match &self.value_browse {
Some(key) => crate::data_values::children(p, exchange, &self.kind(), key),
None => Ok(crate::data_values::roots(p, exchange, &self.kind())),
}
Ok(choices)
}
fn authored_values(&self) -> Vec<(&crate::data_values::FieldPathKey, &str)> {
self.values
.iter()
.filter(|(_, value)| !value.trim().is_empty())
.map(|(key, value)| (key, value.as_str()))
.collect()
}
fn kind(&self) -> MessageKind {
match self.style {
@@ -670,6 +717,144 @@ impl Draft {
fn event(e: Event) -> Message {
Message::Behavior(super::Event::Scenario(e))
}
fn value_page(len: usize, requested: usize) -> std::ops::Range<usize> {
let start = requested.min(len.saturating_sub(1) / VALUE_PAGE_SIZE) * VALUE_PAGE_SIZE;
start..(start + VALUE_PAGE_SIZE).min(len)
}
fn value_pager(len: usize, requested: usize, section: ValueSection) -> Element<'static, Message> {
let range = value_page(len, requested);
let page = range.start / VALUE_PAGE_SIZE;
row![
button("Previous")
.on_press_maybe((page > 0).then(|| event(Event::ValuePage(section, page - 1)))),
text(format!(
"Page {} of {} · {} fields",
page + 1,
len.div_ceil(VALUE_PAGE_SIZE).max(1),
len
))
.size(11),
button("Next")
.on_press_maybe((range.end < len).then(|| event(Event::ValuePage(section, page + 1))))
]
.spacing(6)
.into()
}
fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a, Message> {
use crate::data_values;
let mut out = column![text("Message values").size(14)].spacing(7);
if let Some(browse) = &d.value_browse {
let title = data_values::resolve(p, exchange, &d.kind(), browse)
.map(|choice| format!("Fields inside {}", choice.label))
.unwrap_or_else(|| "This record path is no longer available in this message.".into());
out = out.push(text(title).size(11)).push(
row![
button("Root fields").on_press(event(Event::ValueBrowse(None))),
button("Up one level")
.on_press(event(Event::ValueBrowse(data_values::parent(browse))))
]
.spacing(6),
);
} else {
out = out.push(text("Root fields · choose a record to browse its fields").size(11));
}
match d.browsed_values(p, exchange) {
Ok(choices) if !choices.is_empty() => {
let range = value_page(choices.len(), d.value_browse_page);
let visible = choices[range].to_vec();
let selected = visible
.iter()
.find(|choice| Some(&choice.key) == d.value_focus.as_ref())
.cloned();
out = out
.push(value_pager(
choices.len(),
d.value_browse_page,
ValueSection::Fields,
))
.push(
pick_list(visible, selected, |choice| {
event(Event::ValueFocus(choice.key))
})
.placeholder("Choose a field on this page…")
.text_size(12)
.padding(8)
.width(Fill),
);
}
Ok(_) => {
out = out.push(text("No fields at this level for this message phase.").size(11));
}
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
}
match d
.value_focus
.as_ref()
.map(|key| data_values::resolve(p, exchange, &d.kind(), key))
{
Some(Some(focus)) => {
let key = focus.key;
out = out
.push(text(focus.label).size(12))
.push(
text_input(
"Optional value expression",
d.values.get(&key).map(String::as_str).unwrap_or(""),
)
.on_input({
let key = key.clone();
move |value| event(Event::Value(key.clone(), value))
})
.padding(8)
.size(12),
)
.push(text("An empty expression omits this value.").size(11));
let mut actions = row![
button("Show enclosing fields")
.on_press(event(Event::ValueBrowse(data_values::parent(&key))))
]
.spacing(6);
if matches!(p.elements.get(&key.field).and_then(oa_core::Element::shared),
Some(SharedBody::DataField { type_ref: TypeRef::Named(record),
storage: FieldStorage::Value, .. }) if data::is_record(p, *record))
{
actions = actions.push(
button("Browse record fields").on_press(event(Event::ValueBrowse(Some(key)))),
);
}
out = out.push(actions);
}
Some(None) => {
out = out.push(text("The selected path is unavailable in this message. Choose a compatible field; entered values remain in the draft.")
.size(11).color(style::MUTED));
}
None => out = out.push(text("Choose a field to enter its value expression.").size(11)),
}
let authored = d.authored_values();
out = out.push(text(format!("Authored values ({})", authored.len())).size(12));
if authored.is_empty() {
out = out.push(text("No value expressions entered.").size(11));
} else {
out = out.push(value_pager(
authored.len(),
d.value_authored_page,
ValueSection::Authored,
));
for (key, expression) in &authored[value_page(authored.len(), d.value_authored_page)] {
let title = data_values::resolve(p, exchange, &d.kind(), key)
.map(|choice| choice.label)
.unwrap_or_else(|| format!("Unavailable field {} · review this value", key.field));
out = out.push(
button(
column![text(title).size(11), text(short(expression, 100)).size(11)].spacing(3),
)
.on_press(event(Event::ValueFocus((*key).clone())))
.width(Fill),
);
}
}
out.into()
}
fn picker<'a>(
choices: Vec<Choice>,
selected: Option<Id>,
@@ -822,66 +1007,7 @@ 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)) {
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)
.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)),
}
form = form.push(value_panel(p, d, exchange));
}
if d.style == MessageStyle::CallReply {
form=form.push(label("Creates the call and reversed, correlated reply together in one undoable operation."));
@@ -1438,6 +1564,12 @@ mod data_contract_tests {
path: vec![f.origin, next],
field: f.leaf,
};
state
.update(
&f.project,
Event::ValueBrowse(crate::data_values::parent(&key)),
)
.unwrap();
state
.update(&f.project, Event::ValueFocus(key.clone()))
.unwrap();
@@ -1446,7 +1578,7 @@ mod data_contract_tests {
.draft
.as_ref()
.unwrap()
.value_choices(&f.project, &f.project.relations[&f.exchange])
.browsed_values(&f.project, &f.project.relations[&f.exchange])
.unwrap()
.iter()
.any(|c| c.key == key)
@@ -1490,4 +1622,361 @@ mod data_contract_tests {
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().values[&out], "reply value");
}
fn panel_counts(p: &Project, d: &Draft, exchange: Id) -> (usize, usize) {
let input: Element<'_, Message> = text_input("", "").into();
let input_tag = iced::advanced::widget::Tree::new(input.as_widget()).tag;
let panel = value_panel(p, d, &p.relations[&exchange]);
let tree = iced::advanced::widget::Tree::new(panel.as_widget());
fn count(
tree: &iced::advanced::widget::Tree,
tag: iced::advanced::widget::tree::Tag,
) -> (usize, usize) {
tree.children.iter().fold(
(1, usize::from(tree.tag == tag)),
|(nodes, inputs), child| {
let (child_nodes, child_inputs) = count(child, tag);
(nodes + child_nodes, inputs + child_inputs)
},
)
}
count(&tree, input_tag)
}
#[test]
fn large_payload_has_one_editor_with_reachable_field_and_authored_pages() {
let f = crate::data_values::tests::wide_payload();
let p = &f.project;
let exchange = &p.relations[&f.exchange];
let mut state = State::default();
state
.update(p, Event::Start(f.scenario, Tool::Message))
.unwrap();
state
.update(p, Event::Exchange(named_choice(p, f.exchange)))
.unwrap();
let roots = crate::data_values::roots(p, exchange, &MessageKind::Call);
assert_eq!(roots.len(), 96);
assert_eq!(
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
0
);
let mut visited = BTreeSet::new();
for page in 0..roots.len().div_ceil(VALUE_PAGE_SIZE) {
state
.update(p, Event::ValuePage(ValueSection::Fields, page))
.unwrap();
let d = state.draft.as_ref().unwrap();
let choices = d.browsed_values(p, exchange).unwrap();
let visible = choices[value_page(choices.len(), d.value_browse_page)].to_vec();
assert!(visible.len() <= VALUE_PAGE_SIZE);
for choice in visible {
visited.insert(choice.key.clone());
state
.update(p, Event::ValueFocus(choice.key.clone()))
.unwrap();
state
.update(p, Event::Value(choice.key, format!("draft {page}")))
.unwrap();
}
let (nodes, inputs) = panel_counts(p, state.draft.as_ref().unwrap(), f.exchange);
assert_eq!(
inputs, 1,
"one input for over twelve thousand carried paths"
);
assert!(nodes < 200, "{nodes} widgets for the bounded value panel");
}
assert_eq!(
visited,
roots.iter().map(|choice| choice.key.clone()).collect()
);
state
.update(p, Event::ValueBrowse(Some(roots[0].key.clone())))
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().value_browse_page, 0);
let children = state
.draft
.as_ref()
.unwrap()
.browsed_values(p, exchange)
.unwrap();
assert_eq!(children.len(), 128);
let mut visited_children = BTreeSet::new();
for page in 0..children.len().div_ceil(VALUE_PAGE_SIZE) {
state
.update(p, Event::ValuePage(ValueSection::Fields, page))
.unwrap();
let d = state.draft.as_ref().unwrap();
let choices = d.browsed_values(p, exchange).unwrap();
for choice in &choices[value_page(choices.len(), d.value_browse_page)] {
assert_eq!(choice.key.path, [roots[0].key.field]);
visited_children.insert(choice.key.clone());
}
}
assert_eq!(
visited_children,
children.iter().map(|choice| choice.key.clone()).collect()
);
let nested = children[0].key.clone();
state.update(p, Event::ValueFocus(nested.clone())).unwrap();
state
.update(p, Event::Value(nested.clone(), "unfinished(\"".into()))
.unwrap();
let before = state.draft.as_ref().unwrap().values.clone();
let before_browse = state.draft.as_ref().unwrap().value_browse.clone();
let before_page = state.draft.as_ref().unwrap().value_browse_page;
for event in [
Event::Style(MessageStyle::Call),
Event::Exchange(named_choice(p, f.exchange)),
Event::Style(MessageStyle::CallReply),
] {
state.update(p, event).unwrap();
}
let d = state.draft.as_ref().unwrap();
assert_eq!(d.values, before);
assert_eq!(d.value_browse, before_browse);
assert_eq!(d.value_browse_page, before_page);
assert_eq!(d.value_focus, Some(nested));
let mut authored = BTreeSet::new();
for page in 0..before.len().div_ceil(VALUE_PAGE_SIZE) {
state
.update(p, Event::ValuePage(ValueSection::Authored, page))
.unwrap();
let d = state.draft.as_ref().unwrap();
let values = d.authored_values();
for (key, _) in &values[value_page(values.len(), d.value_authored_page)] {
authored.insert((*key).clone());
}
let (nodes, inputs) = panel_counts(p, d, f.exchange);
assert_eq!(inputs, 1);
assert!(nodes < 200, "{nodes} widget nodes");
}
assert_eq!(authored, before.keys().cloned().collect());
state.update(p, Event::ValueBrowse(None)).unwrap();
state
.update(p, Event::ValuePage(ValueSection::Fields, usize::MAX))
.unwrap();
let d = state.draft.as_ref().unwrap();
assert_eq!(
d.values, before,
"browsing and page clamping never remove drafts"
);
let OccurrenceBody::Exchange { values, .. } = d.message_body() else {
panic!()
};
assert_eq!(
values.len(),
before.len(),
"commit includes every authored page"
);
assert_eq!(panel_counts(p, d, f.exchange).1, 1);
}
#[test]
fn recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit() {
let (f, next) = crate::data_values::tests::recursive_payload();
let p = &f.project;
let exchange = &p.relations[&f.exchange];
let mut state = State::default();
state
.update(p, Event::Start(f.scenario, Tool::Message))
.unwrap();
state
.update(p, Event::Exchange(named_choice(p, f.exchange)))
.unwrap();
let root = crate::data_values::FieldPathKey {
path: vec![],
field: f.origin,
};
state.update(p, Event::ValueFocus(root.clone())).unwrap();
state
.update(p, Event::ValueBrowse(Some(root.clone())))
.unwrap();
let inner = state
.draft
.as_ref()
.unwrap()
.browsed_values(p, exchange)
.unwrap()
.into_iter()
.find(|choice| choice.key.field == next)
.unwrap()
.key;
state.update(p, Event::ValueFocus(inner.clone())).unwrap();
state
.update(p, Event::ValueBrowse(Some(inner.clone())))
.unwrap();
let leaf = state
.draft
.as_ref()
.unwrap()
.browsed_values(p, exchange)
.unwrap()
.into_iter()
.find(|choice| choice.key.field == f.leaf)
.unwrap()
.key;
assert_eq!(leaf.path, [f.origin, next]);
state.update(p, Event::ValueFocus(leaf.clone())).unwrap();
state
.update(p, Event::Value(leaf.clone(), "keep this draft".into()))
.unwrap();
state
.update(p, Event::ValueBrowse(crate::data_values::parent(&inner)))
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().value_browse, Some(root));
state.update(p, Event::ValueBrowse(None)).unwrap();
assert_eq!(
state.draft.as_ref().unwrap().values[&leaf],
"keep this draft"
);
assert_eq!(
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
1
);
let deepest = crate::data_values::FieldPathKey {
path: std::iter::once(f.origin)
.chain(std::iter::repeat_n(next, data::MAX_VALUE_PATH_DEPTH - 1))
.collect(),
field: next,
};
state.update(p, Event::ValueBrowse(Some(deepest))).unwrap();
assert!(
state
.draft
.as_ref()
.unwrap()
.browsed_values(p, exchange)
.unwrap_err()
.contains("128")
);
assert_eq!(
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
1
);
assert_eq!(
state.draft.as_ref().unwrap().values[&leaf],
"keep this draft"
);
state
.update(p, Event::Value(leaf.clone(), String::new()))
.unwrap();
let d = state.draft.as_ref().unwrap();
assert!(d.authored_values().is_empty());
assert_eq!(d.values[&leaf], "", "empty input remains an editable draft");
}
#[test]
fn reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange() {
let mut f = crate::data_values::tests::payload();
let p = &mut f.project;
let source = p.relations[&f.exchange].source();
let target = p.relations[&f.exchange].target();
let mut lifelines = vec![];
for (name, represented) in [("Caller", source), ("Callee", target)] {
let id = p
.add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario))
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(represented),
};
lifelines.push(id);
}
let other_item = p
.add_element(Kind::ExchangeItem, Layer::Oa, "Other operation", None)
.unwrap();
*p.elements
.get_mut(&other_item)
.unwrap()
.shared_mut()
.unwrap() = p.elements[&f.item].shared().unwrap().clone();
let other_field = p
.add_element(Kind::DataField, Layer::Oa, "Result", Some(other_item))
.unwrap();
*p.elements
.get_mut(&other_field)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::DataField {
type_ref: TypeRef::Real,
direction: ParameterDirection::Out,
minimum: 1,
maximum: Some(1),
unit: String::new(),
storage: FieldStorage::Value,
default: None,
};
let other_exchange = p.relate(RelationKind::Interaction, "Other exchange", source, target);
p.relations.get_mut(&other_exchange).unwrap().body = RelationBody::Interaction {
source,
target,
items: vec![other_item],
};
let mut call_ids = vec![];
for (name, exchange) in [("First call", f.exchange), ("Other call", other_exchange)] {
let call = occurrence(
p,
f.scenario,
name,
OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: Some(lifelines[0]),
receiver: Some(lifelines[1]),
message_kind: MessageKind::Call,
reply_to: None,
values: vec![],
},
);
call_ids.push(call.id);
*p = execute_batch(p, &[Command::CreateElement(call)]).unwrap();
}
let mut state = State::default();
state
.update(p, Event::Start(f.scenario, Tool::Message))
.unwrap();
state.update(p, Event::Style(MessageStyle::Reply)).unwrap();
state
.update(p, Event::Call(named_choice(p, call_ids[0])))
.unwrap();
let key = crate::data_values::FieldPathKey {
path: vec![f.destination],
field: f.leaf,
};
state.update(p, Event::ValueFocus(key.clone())).unwrap();
state
.update(p, Event::Value(key.clone(), "reply expression".into()))
.unwrap();
state
.update(p, Event::ValueBrowse(crate::data_values::parent(&key)))
.unwrap();
let offered = calls(p, state.draft.as_ref().unwrap());
assert!(
offered.iter().any(|choice| choice.id == call_ids[1]),
"a different exchange remains selectable after entering a value"
);
assert_eq!(
state.draft.as_ref().unwrap().values[&key],
"reply expression",
"preview is read-only"
);
state
.update(p, Event::Call(named_choice(p, call_ids[0])))
.unwrap();
assert_eq!(
state.draft.as_ref().unwrap().values[&key],
"reply expression"
);
state
.update(p, Event::Call(named_choice(p, call_ids[1])))
.unwrap();
let d = state.draft.as_ref().unwrap();
assert!(d.values.is_empty());
assert!(d.value_focus.is_none());
assert!(d.value_browse.is_none());
assert_eq!(d.exchange, Some(other_exchange));
assert!(state.prepare_pending(p).is_ok());
}
}
+117 -7
View File
@@ -84,6 +84,35 @@ pub fn choices(
.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(
@@ -113,9 +142,11 @@ pub fn children(
phase: &MessageKind,
key: &FieldPathKey,
) -> Result<Vec<FieldPathChoice>, String> {
if resolve(project, exchange, phase, key).is_none() {
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,
@@ -137,14 +168,14 @@ pub fn children(
path.push(key.field);
Ok(data::effective_fields(project, *record)
.into_iter()
.filter_map(|field| {
resolve(
.map(|field| {
named_choice(
project,
exchange,
phase,
&FieldPathKey {
data::CarriedFieldPath {
item: entry.item,
path: path.clone(),
field: field.id,
direction: entry.direction,
},
)
})
@@ -337,6 +368,85 @@ pub(crate) mod tests {
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();