//! 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 { serde_json::from_value(draft.context_value()) .ok() .or_else(|| project.elements.get(&draft.original).cloned()) } fn definition(project: &Project, reference: &TypeRef) -> Option { 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 { 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> { 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) = ¤t && !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::(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) = ¤t_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::>() .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, requested: usize, ) -> (Vec, 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::() } 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)); } }