Add typed data contract editing and bounded dictionary views
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
//! 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 choices = context
|
||||
.map(|(r, phase)| crate::data_values::choices(project, r, phase))
|
||||
.unwrap_or_else(|| Ok(vec![]));
|
||||
let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6);
|
||||
match choices {
|
||||
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
|
||||
Ok(mut choices) => {
|
||||
let current_key = current.as_ref().map(|v| crate::data_values::FieldPathKey {
|
||||
path: v.path.clone(),
|
||||
field: v.field,
|
||||
});
|
||||
let mut selected = current_key
|
||||
.as_ref()
|
||||
.and_then(|key| choices.iter().find(|c| &c.key == key))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
context.and_then(|(r, phase)| {
|
||||
current_key
|
||||
.as_ref()
|
||||
.and_then(|key| crate::data_values::resolve(project, r, phase, key))
|
||||
})
|
||||
});
|
||||
if let Some(choice) = &selected
|
||||
&& !choices.iter().any(|c| c.key == choice.key)
|
||||
{
|
||||
choices.push(choice.clone());
|
||||
}
|
||||
if selected.is_none()
|
||||
&& let Some(key) = current_key.clone()
|
||||
&& !key.field.is_nil()
|
||||
{
|
||||
out = out.push(text("Current address needs review. Choose an explicit path to resolve a legacy, ambiguous or incompatible value.").size(11).color(style::MUTED));
|
||||
let retained = crate::data_values::FieldPathChoice {
|
||||
label: format!(
|
||||
"Current address · {} / {}",
|
||||
key.path
|
||||
.iter()
|
||||
.map(|id| project.named(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" → "),
|
||||
project.named(key.field)
|
||||
),
|
||||
key,
|
||||
};
|
||||
selected = Some(retained.clone());
|
||||
choices.push(retained);
|
||||
}
|
||||
let owned = path.to_owned();
|
||||
out = out.push(
|
||||
pick_list(choices, selected, move |choice| {
|
||||
Message::FieldValuePath(owned.clone(), choice.key)
|
||||
})
|
||||
.placeholder("Choose a carried field path…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let (Some((exchange, phase)), Some(current)) = (context, current) {
|
||||
let key = crate::data_values::FieldPathKey {
|
||||
path: current.path,
|
||||
field: current.field,
|
||||
};
|
||||
if crate::data_values::resolve(project, exchange, phase, &key).is_some() {
|
||||
if let Some(parent) = crate::data_values::parent(&key) {
|
||||
out = out.push(
|
||||
button(text("Up to enclosing field").size(11))
|
||||
.style(style::nav)
|
||||
.on_press(Message::FieldValuePath(path.into(), parent)),
|
||||
);
|
||||
}
|
||||
match crate::data_values::children(project, exchange, phase, &key) {
|
||||
Ok(children) if !children.is_empty() => {
|
||||
let path = path.to_owned();
|
||||
out = out.push(
|
||||
pick_list(
|
||||
children,
|
||||
None::<crate::data_values::FieldPathChoice>,
|
||||
move |choice| Message::FieldValuePath(path.clone(), choice.key),
|
||||
)
|
||||
.placeholder("Choose a field inside this record…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
Err(error) => out = out.push(text(error).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()
|
||||
}
|
||||
|
||||
#[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 _ = 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 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user