Files
ARCADIA-Studio/crates/oa-desktop/src/forms/data_fields.rs
T

1216 lines
48 KiB
Rust

//! Typed data contracts use the same draft and command boundary as other properties.
use super::*;
use iced::widget::column;
fn element(draft: &Draft, project: &Project) -> Option<Element> {
serde_json::from_value(draft.context_value())
.ok()
.or_else(|| project.elements.get(&draft.original).cloned())
}
fn definition(project: &Project, reference: &TypeRef) -> Option<DataTypeDefinition> {
Some(match reference {
TypeRef::Boolean => DataTypeDefinition::Boolean,
TypeRef::Integer => DataTypeDefinition::Integer {
minimum: None,
maximum: None,
},
TypeRef::Real => DataTypeDefinition::Real {
minimum: None,
maximum: None,
},
TypeRef::String => DataTypeDefinition::String {
minimum_length: 0,
maximum_length: None,
},
TypeRef::Named(id) => match project.elements.get(id)?.shared()? {
SharedBody::DataType { definition, .. } => definition.clone(),
_ => return None,
},
})
}
fn literal(definition: &DataTypeDefinition) -> Option<Value> {
Some(match definition {
DataTypeDefinition::Boolean => json!({"Boolean": false}),
DataTypeDefinition::Integer { minimum, .. } => json!({"Integer": minimum.unwrap_or(0)}),
DataTypeDefinition::Real { minimum, .. } => json!({"Real": minimum.unwrap_or(0.)}),
DataTypeDefinition::String { .. } => json!({"String": ""}),
DataTypeDefinition::Enumeration { literals } => json!({"Enumeration": literals.first()?}),
DataTypeDefinition::Record { .. } => return None,
})
}
pub(super) fn property<'a>(
draft: &'a Draft,
project: &'a Project,
value: &'a Value,
path: &str,
) -> Option<View<'a, Message>> {
if path.split('/').rev().nth(1) == Some("values") && value.is_object() {
return Some(message_value(draft, project, value, path));
}
let leaf = path.rsplit('/').next()?;
if leaf == "operation" && path.ends_with("/Occurrence/body/Exchange/operation") {
let message = element(draft, project)?;
let operation = match message.shared()? {
SharedBody::Occurrence {
body: OccurrenceBody::Exchange { operation, .. },
..
} => operation.as_ref(),
_ => return None,
};
let choices = crate::data_values::operation_choices(project, &message);
let selected = operation
.and_then(|o| o.allocation)
.map(|id| crate::app::Choice {
id,
label: crate::data_values::operation_identity(project, id),
});
let path = path.to_owned();
let mut out = column![
text("Exact interface operation allocation").size(12),
pick_list(choices, selected, {
let path = path.clone();
move |choice| Message::FieldShape(path.clone(), json!({"allocation":choice.id}))
})
.placeholder("Choose an eligible allocation…")
.text_size(12)
.padding(8)
.width(Fill),
button("Keep operation unresolved").on_press(Message::FieldShape(
path.clone(),
json!({"allocation":null})
)),
button("Use ordinary channel semantics")
.on_press(Message::FieldShape(path, Value::Null))
]
.spacing(7);
if let Some(label) = crate::data_values::operation_label(project, &message) {
out = out.push(text(label).size(11));
}
return Some(out.into());
}
if leaf == "Exchange" && path.contains("/Occurrence/") && value.is_object() {
let mut out = column![].spacing(7);
if let Some(message) = element(draft, project) {
let operation_path = format!("{path}/operation");
let body = message.shared();
if let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind,
operation,
..
},
..
}) = body
{
let scenario = validation::ancestor_of_kind(project, message.id, Kind::Scenario);
let boundary = scenario
.and_then(|id| project.elements.get(&id))
.is_some_and(|e| {
matches!(
e.shared(),
Some(SharedBody::Scenario {
viewpoint: ScenarioViewpoint::BoundaryInterface,
..
})
)
});
if message.layer() == Layer::Sa
&& boundary
&& matches!(message_kind, MessageKind::Call | MessageKind::Asynchronous)
{
let enabled = operation.is_some();
out = out.push(
checkbox(enabled)
.label("Invoke a specific interface operation")
.on_toggle({
let path = operation_path.clone();
move |enabled| {
Message::FieldShape(
path.clone(),
if enabled {
json!({"allocation":null})
} else {
Value::Null
},
)
}
}),
);
if let Some(operation) = operation {
let mut choices = crate::data_values::operation_choices(project, &message);
let selected = operation.allocation.map(|id| crate::app::Choice {
id,
label: crate::data_values::operation_identity(project, id),
});
if let Some(selected) = &selected
&& !choices.iter().any(|c| c.id == selected.id)
{
choices.push(selected.clone());
}
out = out
.push(
pick_list(choices, selected, {
let path = operation_path.clone();
move |choice| {
Message::FieldShape(
path.clone(),
json!({"allocation":choice.id}),
)
}
})
.placeholder("Choose exact interface operation allocation…")
.text_size(12)
.padding(8)
.width(Fill),
)
.push(button("Keep operation unresolved").on_press(
Message::FieldShape(operation_path, json!({"allocation":null})),
));
}
}
if let Some(description) = crate::data_values::operation_label(project, &message) {
out = out.push(text(description).size(11));
}
}
}
for (key, field) in value.as_object()? {
if key != "operation" {
out = out.push(super::property(
draft,
project,
field,
&format!("{path}/{key}"),
&friendly(key),
false,
));
}
}
return Some(out.into());
}
if leaf == "direction" && path.contains("/DataField/") {
let operation = element(draft, project)
.and_then(|e| e.parent)
.and_then(|id| data::item_mechanism(project, id))
== Some(ExchangeMechanism::Operation);
if !operation {
let mut out = column![
text("Parameter direction").size(11).color(style::MUTED),
text("Unspecified · direction applies to direct Operation parameters.").size(11)
]
.spacing(5);
if value.as_str() != Some("Unspecified") {
out = out
.push(
text(format!(
"Current {:?} requires review.",
value.as_str().unwrap_or_default()
))
.size(11),
)
.push(
button(text("Use Unspecified").size(11))
.style(style::nav)
.on_press(Message::FieldShape(path.into(), json!("Unspecified"))),
);
}
return Some(out.into());
}
}
let bounded = (path.contains("/definition/Integer/") || path.contains("/definition/Real/"))
&& matches!(leaf, "minimum" | "maximum");
let count_max = matches!(leaf, "maximum_length" | "source_maximum" | "target_maximum")
|| (leaf == "maximum" && path.contains("/DataField/"));
if bounded || count_max {
let label = match leaf {
"minimum" => "Inclusive lower bound",
"maximum" if bounded => "Inclusive upper bound",
"maximum_length" => "Maximum Unicode characters",
"source_maximum" => "Source maximum count",
"target_maximum" => "Target maximum count",
_ => "Maximum count",
};
let mut out = column![text(label).size(11).color(style::MUTED)].spacing(5);
if value.is_null() {
out = out.push(
button(
text(if bounded {
"Unbounded · set bound"
} else {
"Unlimited · set maximum"
})
.size(12),
)
.style(style::nav)
.on_press(Message::FieldShape(
path.into(),
if path.contains("/Real/") {
json!(0.)
} else if count_max {
let minimum_path = path.replace("maximum", "minimum");
json!(
draft
.context_value()
.pointer(&minimum_path)
.and_then(Value::as_u64)
.unwrap_or(1)
.max(1)
)
} else {
json!(0)
},
)),
);
} else if let Some(input) = draft.inputs.get(path) {
let owned = path.to_owned();
out = out
.push(
text_input("", &input.raw)
.on_input(move |s| Message::Field(owned.clone(), s))
.size(12)
.padding(8),
)
.push(
button(text("Clear bound").size(10))
.style(style::nav)
.on_press(Message::FieldShape(path.into(), Value::Null)),
);
}
return Some(out.into());
}
if leaf == "default" && (path.contains("/DataField/") || path.contains("/DataType/")) {
let owner = element(draft, project);
let (definition, collection, reference, minimum) =
match owner.as_ref().and_then(Element::shared) {
Some(SharedBody::DataType { definition, .. }) => {
(Some(definition.clone()), false, false, 0)
}
Some(SharedBody::DataField {
type_ref,
storage,
minimum,
..
}) => (
definition(project, type_ref),
true,
*storage == FieldStorage::Reference,
*minimum,
),
_ => (None, false, false, 0),
};
let initial = definition.as_ref().and_then(literal);
let mut out = column![text("Default").size(11).color(style::MUTED)].spacing(6);
if reference {
out = out.push(
text("References have no literal default; they identify an existing record.")
.size(11)
.color(style::MUTED),
);
}
if value.is_null() {
out = out.push(text("Absent · no authored default").size(12));
if !reference {
if let Some(initial) = initial {
out = out.push(
button(text("Set literal default").size(11))
.style(style::nav)
.on_press(Message::FieldShape(
path.into(),
if collection {
json!([initial])
} else {
initial
},
)),
);
}
if collection && minimum == 0 {
out = out.push(
button(text("Set explicit empty collection").size(11))
.style(style::nav)
.on_press(Message::FieldShape(path.into(), json!([]))),
);
}
}
} else {
out = out.push(
button(text("Clear default · make absent").size(11))
.style(style::nav)
.on_press(Message::FieldShape(path.into(), Value::Null)),
);
if let Some(values) = value.as_array() {
if values.is_empty() {
out = out.push(text("Explicit empty collection []").size(12));
}
for (i, value) in values.iter().enumerate() {
out = out
.push(literal_editor(
draft,
project,
value,
&format!("{path}/{i}"),
definition.as_ref(),
))
.push(
button(text("Remove value").size(10))
.style(style::nav)
.on_press(Message::FieldArray(path.into(), Some(i), 0)),
);
}
if let Some(initial) = initial
&& !reference
{
out = out.push(
button(text("+ Add literal").size(11))
.style(style::nav)
.on_press(Message::FieldAppend(path.into(), initial)),
);
}
} else {
out = out.push(literal_editor(
draft,
project,
value,
path,
definition.as_ref(),
));
}
}
return Some(out.into());
}
None
}
fn literal_editor<'a>(
draft: &'a Draft,
project: &'a Project,
value: &'a Value,
path: &str,
definition: Option<&DataTypeDefinition>,
) -> View<'a, Message> {
if let Some(DataTypeDefinition::Enumeration { literals }) = definition {
let current = draft
.materialize()
.ok()
.and_then(|v| {
v.pointer(&format!("{path}/Enumeration"))
.and_then(Value::as_str)
.map(str::to_owned)
})
.or_else(|| {
value
.get("Enumeration")
.and_then(Value::as_str)
.map(str::to_owned)
});
let mut choices = literals.clone();
if let Some(current) = &current
&& !choices.contains(current)
{
choices.push(current.clone());
}
let path = path.to_owned();
let picker = pick_list(choices, current, move |s| {
Message::FieldShape(path.clone(), json!({"Enumeration": s}))
})
.text_size(12)
.padding(8)
.width(Fill);
return if value.get("Enumeration").is_none() {
column![text("Current default uses another literal type. Choose an enumeration literal or clear the default.").size(11).color(style::MUTED),picker].spacing(5).into()
} else {
picker.into()
};
}
let mut out = column![].spacing(5);
if let Some(fields) = value.as_object() {
for (tag, payload) in fields {
out = out.push(super::property(
draft,
project,
payload,
&format!("{path}/{tag}"),
&friendly(tag),
true,
));
}
} else {
out = out.push(
text("Current literal requires review; clear it to choose a typed value.").size(11),
);
}
out.into()
}
fn message_value<'a>(
draft: &'a Draft,
project: &'a Project,
value: &'a Value,
path: &str,
) -> View<'a, Message> {
let edited = draft.context_value();
let row = edited.pointer(path).unwrap_or(value).clone();
let current = serde_json::from_value::<MessageValue>(row.clone()).ok();
let owner = element(draft, project);
let signature = owner.as_ref().and_then(|message| {
crate::data_values::message_signature(project, message)
.ok()
.flatten()
});
let phase = owner
.as_ref()
.and_then(Element::shared)
.and_then(|body| match body {
SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind,
operation,
..
},
..
} => Some(crate::data_values::operation_value_phase(
message_kind,
operation.as_ref(),
)),
_ => None,
});
let context = signature.as_ref().zip(phase.as_ref());
let current_key = current
.as_ref()
.map(|value| crate::data_values::FieldPathKey {
path: value.path.clone(),
field: value.field,
});
let selected = context.and_then(|(exchange, phase)| {
current_key
.as_ref()
.and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
});
let browser = draft
.value_browsers
.get(path)
.cloned()
.unwrap_or_else(|| ValueBrowser {
scope: selected
.as_ref()
.and_then(|choice| crate::data_values::parent(&choice.key)),
page: 0,
});
let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6);
if let Some(choice) = &selected {
out = out.push(text(choice.label.clone()).size(11));
} else if let Some(key) = &current_key
&& !key.field.is_nil()
{
out = out.push(
text(format!(
"Current address needs review: {} / {}. Choose an explicit field.",
key.path
.iter()
.map(|id| project.named(*id))
.collect::<Vec<_>>()
.join(" → "),
project.named(key.field)
))
.size(11)
.color(style::MUTED),
);
}
if let Some((exchange, phase)) = context {
let location = browser
.scope
.as_ref()
.and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
.map(|choice| choice.label)
.unwrap_or_else(|| "Root carried fields".into());
out = out.push(
text(format!("Browse: {location}"))
.size(11)
.color(style::MUTED),
);
if let Some(scope) = &browser.scope {
let mut navigation = row![button(text("↑ Up").size(11)).style(style::nav).on_press(
Message::FieldValueBrowse(path.into(), crate::data_values::parent(scope), 0)
)]
.spacing(4);
if !scope.path.is_empty() {
navigation = navigation.push(
button(text("Root fields").size(11))
.style(style::nav)
.on_press(Message::FieldValueBrowse(path.into(), None, 0)),
);
}
out = out.push(navigation);
}
match crate::data_values::at_scope(project, exchange, phase, browser.scope.as_ref()) {
Ok(choices) => {
let count = choices.len();
let (visible, page, pages) = field_page(choices, browser.page);
// Selection detail stays visible independently of browser location/page.
let visible_selection = selected
.clone()
.filter(|choice| visible.iter().any(|c| c.key == choice.key));
let owned = path.to_owned();
out = out.push(
pick_list(visible, visible_selection, move |choice| {
Message::FieldValuePath(owned.clone(), choice.key)
})
.placeholder("Choose a field here…")
.text_size(12)
.padding(8)
.width(Fill),
);
if pages > 1 {
out = out.push(
row![
button(text("Previous").size(11))
.style(style::nav)
.on_press_maybe((page > 0).then(|| Message::FieldValueBrowse(
path.into(),
browser.scope.clone(),
page - 1
))),
text(format!("{} / {pages} · {count} fields", page + 1)).size(11),
button(text("Next").size(11))
.style(style::nav)
.on_press_maybe((page + 1 < pages).then(|| {
Message::FieldValueBrowse(
path.into(),
browser.scope.clone(),
page + 1,
)
}))
]
.spacing(4)
.align_y(iced::Alignment::Center),
);
} else if count == 0 {
out = out.push(
text("No compatible fields at this location.")
.size(11)
.color(style::MUTED),
);
}
}
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
}
if let Some(choice) = &selected
&& browser.scope.as_ref() != Some(&choice.key)
&& crate::data_values::children(project, exchange, phase, &choice.key)
.is_ok_and(|children| !children.is_empty())
{
out = out.push(
button(
text(format!(
"Browse fields in {} →",
project.named(choice.key.field)
))
.size(11),
)
.style(style::nav)
.on_press(Message::FieldValueBrowse(
path.into(),
Some(choice.key.clone()),
0,
)),
);
}
} else {
out = out.push(
text("Choose a message exchange to browse its carried fields.")
.size(11)
.color(style::MUTED),
);
}
if let Some(expression) = value.get("expression") {
out = out.push(super::property(
draft,
project,
expression,
&format!("{path}/expression"),
"Value expression",
false,
));
}
out.into()
}
const FIELDS_PER_PAGE: usize = 24;
fn field_page(
choices: Vec<crate::data_values::FieldPathChoice>,
requested: usize,
) -> (Vec<crate::data_values::FieldPathChoice>, usize, usize) {
let pages = choices.len().div_ceil(FIELDS_PER_PAGE).max(1);
let page = requested.min(pages - 1);
(
choices
.into_iter()
.skip(page * FIELDS_PER_PAGE)
.take(FIELDS_PER_PAGE)
.collect(),
page,
pages,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn semantic_state(app: &crate::app::App) -> &crate::semantic_review::State {
let Some(crate::workflow::Workflow::Semantic(state)) = &app.workflow else {
panic!()
};
state
}
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::RowEvent) {
let event = semantic_state(app).address_event(event);
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(event)));
}
fn semantic_decide(app: &mut crate::app::App, facet: FieldFacet, decision: SemanticDecision) {
use crate::semantic_review::RowEvent as Event;
semantic_send(app, Event::Select(FacetSelector::Field(facet)));
semantic_send(
app,
Event::Decide(FacetSelector::Field(facet), Some(decision)),
);
}
fn default_buttons(draft: &Draft, project: &Project, path: &str) -> usize {
use iced::advanced::widget::{Tree, tree::Tag};
let button: View<'_, Message> = button(text("Button")).into();
let tag = Tree::new(button.as_widget()).tag;
let view = property(draft, project, draft.value.pointer(path).unwrap(), path).unwrap();
fn count(tree: &Tree, tag: Tag) -> usize {
usize::from(tree.tag == tag)
+ tree
.children
.iter()
.map(|child| count(child, tag))
.sum::<usize>()
}
count(&Tree::new(view.as_widget()), tag)
}
#[test]
fn semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep() {
use crate::semantic_review::{RowEvent as Event, State};
let mut p = Project::empty();
let oa = p
.add_element(Kind::ExchangeItem, Layer::Oa, "Source", None)
.unwrap();
let sa = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Target", None)
.unwrap();
let operation = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Operation base", None)
.unwrap();
let flow = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Flow base", None)
.unwrap();
for (id, next) in [
(oa, ExchangeMechanism::Operation),
(sa, ExchangeMechanism::Flow),
(operation, ExchangeMechanism::Operation),
(flow, ExchangeMechanism::Flow),
] {
let Some(SharedBody::ExchangeItem { mechanism, .. }) =
p.elements.get_mut(&id).unwrap().shared_mut()
else {
panic!()
};
*mechanism = next;
}
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "item_trace".into(),
name: "Review item".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: None,
},
);
let mut state = State::new(&p, oa).unwrap();
let facet = FacetSelector::ExchangeItem(ExchangeItemFacet::Mechanism);
let path = "/body/Sa/Shared/ExchangeItem/supertype";
let raw = state.draft.as_ref().unwrap().value.clone();
state.update(
&p,
Event::Decide(facet.clone(), Some(SemanticDecision::TakeSource)),
);
assert!(state.can_apply(), "{:?}", state.error);
let choices = reference_ids(state.draft.as_ref().unwrap(), &p, path);
assert!(choices.contains(&operation));
assert!(!choices.contains(&flow));
assert_eq!(state.draft.as_ref().unwrap().value, raw);
state.update(&p, Event::Decide(facet, Some(SemanticDecision::KeepTarget)));
let choices = reference_ids(state.draft.as_ref().unwrap(), &p, path);
assert!(!choices.contains(&operation));
assert!(choices.contains(&flow));
}
#[test]
fn independent_review_combined_take_type_informs_actual_default_form_in_both_directions() {
use crate::semantic_review::RowEvent as Event;
for reverse in [false, true] {
for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] {
let mut p = Project::empty();
let mut pair = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let source = (layer == Layer::Oa) != reverse;
let record = p
.add_element(Kind::DataType, layer, "Record", None)
.unwrap();
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() =
SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: None },
default: None,
unit: String::new(),
};
let field = p
.add_element(Kind::DataField, layer, "Payload", Some(record))
.unwrap();
*p.elements.get_mut(&field).unwrap().shared_mut().unwrap() =
SharedBody::DataField {
type_ref: if source {
TypeRef::Boolean
} else {
TypeRef::Integer
},
minimum: 0,
maximum: Some(1),
unit: String::new(),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
default: None,
};
pair.push(field);
}
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "payload_trace".into(),
name: "Payload review".into(),
description: String::new(),
oa: pair[0],
sa: pair[1],
kind: TraceKind::Manual,
baseline: None,
},
);
let (source, target, layer) = if reverse {
(pair[1], pair[0], "Oa")
} else {
(pair[0], pair[1], "Sa")
};
let root = format!("/body/{layer}/Shared/DataField");
let default = format!("{root}/default");
let type_path = format!("{root}/type_ref");
let minimum = format!("{root}/minimum");
let mut app = crate::app::App::with_project(p);
let before = app.document.snapshot();
let _ = app.update(Message::StartSemanticReview(source));
// An unfinished candidate must not make the context fall back to
// the old target type, or contaminate a later Keep decision.
semantic_decide(&mut app, FieldFacet::Minimum, SemanticDecision::Candidate);
semantic_send(&mut app, Event::Input(minimum.clone(), "-".into()));
semantic_decide(&mut app, FieldFacet::Type, decision);
if decision == SemanticDecision::Candidate {
semantic_send(
&mut app,
Event::Variant(type_path.clone(), json!("Boolean")),
);
}
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
assert!(!semantic_state(&app).can_apply());
let draft = semantic_state(&app).draft.as_ref().unwrap();
assert!(matches!(
element(draft, app.project()).unwrap().shared(),
Some(SharedBody::DataField {
type_ref: TypeRef::Boolean,
..
})
));
assert_eq!(draft.inputs[&minimum].raw, "-");
semantic_decide(&mut app, FieldFacet::Minimum, SemanticDecision::KeepTarget);
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
assert!(semantic_state(&app).can_apply());
let draft = semantic_state(&app).draft.as_ref().unwrap();
assert_eq!(default_buttons(draft, app.project(), &default), 2);
// These are the same element/definition/literal helpers used by
// the visible Set literal default and Add literal buttons.
let owner = element(draft, app.project()).unwrap();
let Some(SharedBody::DataField { type_ref, .. }) = owner.shared() else {
panic!()
};
let initial = literal(&definition(app.project(), type_ref).unwrap()).unwrap();
assert_eq!(initial, json!({"Boolean": false}));
if decision == SemanticDecision::TakeSource {
assert_eq!(draft.value.pointer(&type_path), Some(&json!("Integer")));
}
// Reverting the type changes the controls without changing its
// retained candidate. Storage constraints likewise read context.
semantic_decide(&mut app, FieldFacet::Type, SemanticDecision::KeepTarget);
assert!(matches!(
element(semantic_state(&app).draft.as_ref().unwrap(), app.project())
.unwrap()
.shared(),
Some(SharedBody::DataField {
type_ref: TypeRef::Integer,
..
})
));
semantic_decide(&mut app, FieldFacet::Type, decision);
semantic_decide(&mut app, FieldFacet::Storage, SemanticDecision::Candidate);
semantic_send(
&mut app,
Event::Variant(format!("{root}/storage"), json!("Reference")),
);
assert_eq!(
default_buttons(
semantic_state(&app).draft.as_ref().unwrap(),
app.project(),
&default
),
0
);
semantic_decide(&mut app, FieldFacet::Storage, SemanticDecision::KeepTarget);
assert_eq!(
default_buttons(
semantic_state(&app).draft.as_ref().unwrap(),
app.project(),
&default
),
2
);
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
semantic_send(&mut app, Event::Shape(default, json!([initial])));
assert!(
semantic_state(&app).can_apply(),
"{:?}",
semantic_state(&app).error
);
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
assert!(
matches!(app.project().elements[&target].shared(), Some(SharedBody::DataField {
type_ref: TypeRef::Boolean, default: Some(values), minimum: 0, storage: FieldStorage::Value, ..
}) if values == &vec![DataValue::Boolean(false)])
);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
}
}
}
#[test]
fn selecting_qualified_path_preserves_expression_typed_before_the_next_render() {
let (mut f, next) = crate::data_values::tests::recursive_payload();
if let Some(SharedBody::DataField { direction, .. }) = f
.project
.elements
.get_mut(&f.destination)
.unwrap()
.shared_mut()
{
*direction = ParameterDirection::InOut;
}
let call = f
.project
.add_element(Kind::Occurrence, Layer::Oa, "Call", Some(f.scenario))
.unwrap();
*f.project
.elements
.get_mut(&call)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
operation: None,
exchange: Some(f.exchange),
sender: None,
receiver: None,
message_kind: MessageKind::Call,
reply_to: None,
values: vec![MessageValue {
path: vec![f.origin],
field: f.leaf,
expression: "old".into(),
}],
},
precondition: String::new(),
postcondition: String::new(),
};
f.project
.elements
.get_mut(&f.scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(call);
let mut app = crate::app::App::with_project(f.project.clone());
let path = "/body/Oa/Shared/Occurrence/body/Exchange/values/0";
let _ = app.update(Message::Select(call));
let _ = app.update(Message::Field(
format!("{path}/expression"),
"newly typed".into(),
));
let before_browse = app.draft.as_ref().unwrap().materialize().unwrap();
for (scope, page) in [
(None, 0),
(
Some(crate::data_values::FieldPathKey {
path: vec![f.origin],
field: next,
}),
999,
),
] {
let _ = app.update(Message::FieldValueBrowse(path.into(), scope, page));
let _ = super::super::view(app.draft.as_ref().unwrap(), app.project());
assert_eq!(
app.draft.as_ref().unwrap().materialize().unwrap(),
before_browse
);
assert_eq!(app.project(), &f.project);
}
let _ = app.update(Message::FieldValuePath(
path.into(),
crate::data_values::FieldPathKey {
path: vec![f.destination],
field: f.leaf,
},
));
let _ = app.update(Message::Apply);
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = app.project().elements[&call].shared()
else {
panic!()
};
assert_eq!(
values,
&vec![MessageValue {
path: vec![f.destination],
field: f.leaf,
expression: "newly typed".into()
}],
"{}",
app.status
);
let _ = app.update(Message::Undo);
assert_eq!(app.project(), &f.project);
let nested = crate::data_values::children(
app.project(),
&app.project().relations[&f.exchange],
&MessageKind::Call,
&crate::data_values::FieldPathKey {
path: vec![f.origin],
field: next,
},
)
.unwrap()
.into_iter()
.find(|c| c.key.field == f.leaf)
.unwrap();
let _ = app.update(Message::Select(call));
let _ = app.update(Message::FieldValuePath(path.into(), nested.key.clone()));
let _ = app.update(Message::Apply);
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = app.project().elements[&call].shared()
else {
panic!()
};
assert_eq!(values[0].path, vec![f.origin, next], "{}", app.status);
assert!(
crate::data_values::resolve(
app.project(),
&app.project().relations[&f.exchange],
&MessageKind::Call,
&nested.key
)
.is_some()
);
let _ = super::super::view(app.draft.as_ref().unwrap(), app.project());
}
#[test]
fn field_browser_pages_every_direct_field_without_flattening_other_records() {
let f = crate::data_values::tests::wide_payload();
let exchange = &f.project.relations[&f.exchange];
let roots =
crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, None).unwrap();
assert!(roots.len() > FIELDS_PER_PAGE);
assert!(roots.iter().all(|choice| choice.key.path.is_empty()));
let scope = crate::data_values::FieldPathKey {
path: vec![],
field: f.origin,
};
let children =
crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, Some(&scope))
.unwrap();
assert!(children.len() > FIELDS_PER_PAGE);
assert!(
children
.iter()
.all(|choice| choice.key.path == vec![f.origin])
);
for choices in [roots, children] {
let (_, _, pages) = field_page(choices.clone(), 0);
let visited: Vec<_> = (0..pages)
.flat_map(|page| {
let (visible, actual, _) = field_page(choices.clone(), page);
assert_eq!(actual, page);
assert!(visible.len() <= FIELDS_PER_PAGE);
visible
})
.collect();
assert_eq!(visited, choices);
assert_eq!(field_page(choices, usize::MAX).1, pages - 1);
}
assert_eq!(field_page(vec![], usize::MAX), (vec![], 0, 1));
}
#[test]
fn numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state() {
let mut p = Project::empty();
let id = p
.add_element(Kind::DataType, Layer::Oa, "Counter", None)
.unwrap();
let mut draft = Draft::from_project(&p, id).unwrap();
let definition = "/body/Oa/Shared/DataType/definition";
draft
.shape(
definition,
json!({"Integer":{"minimum":null,"maximum":i64::MAX}}),
)
.unwrap();
let maximum = format!("{definition}/Integer/maximum");
draft.input(&maximum, "-".into());
assert!(draft.command().is_err());
assert!(draft.shape(&maximum, Value::Null).is_err());
assert_eq!(draft.inputs[&maximum].raw, "-");
draft.input(&maximum, "9223372036854775808".into());
assert!(draft.command().is_err());
draft.input(&maximum, i64::MAX.to_string());
draft
.shape(
"/body/Oa/Shared/DataType/default",
json!({"Integer":i64::MAX}),
)
.unwrap();
let accepted = execute_batch(&p, &[draft.command().unwrap()]).unwrap();
assert!(matches!(
accepted.elements[&id].shared(),
Some(SharedBody::DataType {
definition: DataTypeDefinition::Integer {
maximum: Some(i64::MAX),
..
},
default: Some(DataValue::Integer(i64::MAX)),
..
})
));
draft.input(&maximum, "10".into());
assert!(execute_batch(&accepted, &[draft.command().unwrap()]).is_err());
assert_eq!(draft.inputs[&maximum].raw, "10");
}
#[test]
fn defaults_distinguish_absent_empty_and_typed_literals_with_escaped_unicode() {
let mut p = Project::empty();
let item = p
.add_element(Kind::ExchangeItem, Layer::Oa, "Payload", None)
.unwrap();
let id = p
.add_element(Kind::DataField, Layer::Oa, "Optional names", Some(item))
.unwrap();
let mut draft = Draft::from_project(&p, id).unwrap();
let root = "/body/Oa/Shared/DataField";
draft.shape(&format!("{root}/minimum"), json!(0)).unwrap();
draft
.shape(&format!("{root}/maximum"), Value::Null)
.unwrap();
draft.shape(&format!("{root}/default"), json!([])).unwrap();
let empty = execute_batch(&p, &[draft.command().unwrap()]).unwrap();
assert!(
matches!(empty.elements[&id].shared(),Some(SharedBody::DataField {default:Some(v),..}) if v.is_empty())
);
draft
.shape(&format!("{root}/default"), Value::Null)
.unwrap();
let absent = execute_batch(&empty, &[draft.command().unwrap()]).unwrap();
assert!(matches!(
absent.elements[&id].shared(),
Some(SharedBody::DataField { default: None, .. })
));
draft
.shape(&format!("{root}/default"), json!([{"String":"é🙂\n"}]))
.unwrap();
draft.input(&format!("{root}/default/0/String"), "é🙂\n".into());
draft
.append(&format!("{root}/default"), json!({"String":"next"}))
.unwrap();
assert_eq!(
draft
.materialize()
.unwrap()
.pointer(&format!("{root}/default/0/String")),
Some(&json!("é🙂\n"))
);
draft.array(&format!("{root}/default"), Some(1), 0).unwrap();
draft.set_exact(true).unwrap();
let saved = execute_batch(&absent, &[draft.command().unwrap()]).unwrap();
assert!(
matches!(saved.elements[&id].shared(),Some(SharedBody::DataField {default:Some(v),..}) if v == &vec![DataValue::String("é🙂\n".into())])
);
let _ = super::super::view(&draft, &saved);
draft
.shape(&format!("{root}/storage"), json!("Reference"))
.unwrap();
assert!(execute_batch(&saved, &[draft.command().unwrap()]).is_err());
}
#[test]
fn supertype_picker_excludes_descendants_and_keeps_original_inherited_field_identity() {
let mut p = Project::empty();
let base = p
.add_element(Kind::DataType, Layer::Oa, "Base", None)
.unwrap();
let child = p
.add_element(Kind::DataType, Layer::Oa, "Child", None)
.unwrap();
for (id, supertype) in [(base, None), (child, Some(base))] {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
default: None,
unit: String::new(),
};
}
let field = p
.add_element(Kind::DataField, Layer::Oa, "Owned by base", Some(base))
.unwrap();
let draft = Draft::from_project(&p, base).unwrap();
let path = "/body/Oa/Shared/DataType/definition/Record/supertype";
assert!(!reference_ids(&draft, &p, path).contains(&child));
let child_draft = Draft::from_project(&p, child).unwrap();
assert!(reference_ids(&child_draft, &p, path).contains(&base));
assert_eq!(data::effective_fields(&p, child)[0].id, field);
assert_eq!(p.elements[&field].parent, Some(base));
}
}