814 lines
30 KiB
Rust
814 lines
30 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.materialize().unwrap_or_else(|_| draft.value.clone()))
|
|
.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 == "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
|
|
.materialize()
|
|
.ok()
|
|
.and_then(|v| v.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.materialize().unwrap_or_else(|_| draft.value.clone());
|
|
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 context = owner
|
|
.as_ref()
|
|
.and_then(Element::shared)
|
|
.and_then(|body| match body {
|
|
SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(id),
|
|
message_kind,
|
|
..
|
|
},
|
|
..
|
|
} => project.relations.get(id).map(|r| (r, message_kind)),
|
|
_ => None,
|
|
});
|
|
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::<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::*;
|
|
|
|
#[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 {
|
|
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));
|
|
}
|
|
}
|