Add typed data contract editing and bounded dictionary views
This commit is contained in:
+127
-13
@@ -14,6 +14,7 @@ use iced::{
|
||||
use oa_core::*;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
mod data_fields;
|
||||
mod port_fields;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -218,7 +219,7 @@ impl Draft {
|
||||
} else if path.ends_with("bindings") {
|
||||
json!({"referenced": Id::nil(), "local": Id::nil()})
|
||||
} else if path.ends_with("values") {
|
||||
json!({"field": Id::nil(), "expression": ""})
|
||||
json!({"path": [], "field": Id::nil(), "expression": ""})
|
||||
} else if path.ends_with("routes") {
|
||||
json!({"edge": Id::nil(), "waypoints": [], "label": null})
|
||||
} else if path.ends_with("waypoints") {
|
||||
@@ -242,6 +243,33 @@ impl Draft {
|
||||
self.rebuild();
|
||||
Ok(())
|
||||
}
|
||||
pub fn append(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
let mut candidate = self.materialize()?;
|
||||
candidate
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("List no longer exists.")?
|
||||
.push(value);
|
||||
self.value = candidate;
|
||||
self.rebuild();
|
||||
Ok(())
|
||||
}
|
||||
pub fn value_path(
|
||||
&mut self,
|
||||
path: &str,
|
||||
key: crate::data_values::FieldPathKey,
|
||||
) -> Result<(), String> {
|
||||
let mut candidate = self.materialize()?;
|
||||
let row = candidate
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or("Message value no longer exists.")?;
|
||||
row.insert("path".into(), json!(key.path));
|
||||
row.insert("field".into(), json!(key.field));
|
||||
self.value = candidate;
|
||||
self.rebuild();
|
||||
Ok(())
|
||||
}
|
||||
pub fn command(&self) -> Result<Command, String> {
|
||||
let mut value = self.materialize()?;
|
||||
let decode = |e: serde_json::Error| e.to_string();
|
||||
@@ -411,6 +439,7 @@ fn reference(path: &str) -> bool {
|
||||
| "Named"
|
||||
| "diagram"
|
||||
| "machine"
|
||||
| "supertype"
|
||||
) || matches!(
|
||||
parent,
|
||||
"items"
|
||||
@@ -440,6 +469,7 @@ fn optional_reference(path: &str) -> bool {
|
||||
| "reply_to"
|
||||
| "lifeline"
|
||||
| "state"
|
||||
| "supertype"
|
||||
)
|
||||
}
|
||||
fn multiline(path: &str) -> bool {
|
||||
@@ -462,9 +492,11 @@ fn free_string(path: &str) -> bool {
|
||||
multiline(path)
|
||||
|| matches!(
|
||||
path.rsplit('/').next(),
|
||||
Some("name" | "uri" | "revision" | "fragment" | "unit")
|
||||
Some("name" | "uri" | "revision" | "fragment" | "unit" | "source_role" | "target_role")
|
||||
)
|
||||
|| path.split('/').rev().nth(1) == Some("literals")
|
||||
|| (path.contains("/default/")
|
||||
&& matches!(path.rsplit('/').next(), Some("String" | "Enumeration")))
|
||||
}
|
||||
|
||||
// These are choices for the closed model, not a second validation boundary.
|
||||
@@ -662,21 +694,37 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
.collect();
|
||||
}
|
||||
if field == "field" {
|
||||
let address_path = path
|
||||
.strip_suffix("/field")
|
||||
.and_then(|row| value.pointer(&format!("{row}/path")))
|
||||
.and_then(|v| serde_json::from_value::<Vec<Id>>(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
return element
|
||||
.as_ref()
|
||||
.and_then(|e| match e.shared()? {
|
||||
SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(id), ..
|
||||
exchange: Some(id),
|
||||
message_kind,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => project.relations.get(id),
|
||||
} => project
|
||||
.relations
|
||||
.get(id)
|
||||
.and_then(|r| crate::data_values::choices(project, r, message_kind).ok()),
|
||||
_ => None,
|
||||
})
|
||||
.map(|r| validation::carried_fields(project, r).into_iter().collect())
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|choice| choice.key.path == address_path)
|
||||
.map(|choice| choice.key.field)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
}
|
||||
|
||||
let scenario = element
|
||||
.as_ref()
|
||||
.and_then(|e| e.parent)
|
||||
@@ -707,6 +755,40 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
return false;
|
||||
}
|
||||
match field {
|
||||
"supertype" => element.as_ref().is_some_and(|owner| {
|
||||
if candidate.id == owner.id
|
||||
|| candidate.kind() != owner.kind()
|
||||
|| (candidate.kind() == Kind::DataType
|
||||
&& !data::is_record(project, candidate.id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let (
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind, mechanism, ..
|
||||
}),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: base_kind,
|
||||
mechanism: base_mechanism,
|
||||
..
|
||||
}),
|
||||
) = (owner.shared(), candidate.shared())
|
||||
&& (kind != base_kind
|
||||
|| (*base_mechanism != ExchangeMechanism::Unspecified
|
||||
&& mechanism != base_mechanism))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mut next = Some(candidate.id);
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = next {
|
||||
if id == owner.id || !seen.insert(id) {
|
||||
return false;
|
||||
}
|
||||
next = project.elements.get(&id).and_then(data::supertype);
|
||||
}
|
||||
true
|
||||
}),
|
||||
"machine" => candidate.kind() == Kind::StateMachine,
|
||||
"parent" => element.as_ref().is_some_and(|child| {
|
||||
if candidate.id == draft.original
|
||||
@@ -1058,6 +1140,20 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
let words: &[&str] = match field {
|
||||
"side" if path.starts_with("/port_anchors/") => &["Left", "Right", "Top", "Bottom"],
|
||||
"layer" => &["Oa", "Sa"],
|
||||
"mechanism" => &["Unspecified", "Flow", "Event", "Operation", "SharedData"],
|
||||
"storage" => &["Value", "Reference"],
|
||||
"direction" if path.contains("DataField") => {
|
||||
&["Unspecified", "In", "Out", "InOut", "Return"]
|
||||
}
|
||||
"transmission" => &[
|
||||
"Unspecified",
|
||||
"Synchronous",
|
||||
"Asynchronous",
|
||||
"Unicast",
|
||||
"Multicast",
|
||||
"Broadcast",
|
||||
],
|
||||
"acquisition" => &["Unspecified", "Read", "Accept"],
|
||||
"operator" => &["Alternative", "Parallel", "Loop", "Optional"],
|
||||
"viewpoint" if path.contains("/Oa/") => &[
|
||||
"OperationalActivity",
|
||||
@@ -1066,7 +1162,7 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
],
|
||||
"viewpoint" => &["Functional", "BoundaryInterface", "MixedLegacy"],
|
||||
"phase" => &["Entry", "Do", "Exit"],
|
||||
"kind" if path.contains("ExchangeItem") => &["Information", "Material", "Energy", "Event"],
|
||||
"kind" if path.contains("ExchangeItem") => &["Information", "Material", "Energy"],
|
||||
"kind" if path.contains("Control") => &["Choice", "Merge", "Fork", "Join", "Loop"],
|
||||
"kind" if path.contains("State") => &[
|
||||
"State",
|
||||
@@ -1132,11 +1228,20 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
if field == "definition" {
|
||||
return vec![
|
||||
("Boolean".into(), json!("Boolean")),
|
||||
("Integer".into(), json!("Integer")),
|
||||
("Real".into(), json!("Real")),
|
||||
("Text".into(), json!("String")),
|
||||
(
|
||||
"Integer".into(),
|
||||
json!({"Integer":{"minimum":null,"maximum":null}}),
|
||||
),
|
||||
(
|
||||
"Real".into(),
|
||||
json!({"Real":{"minimum":null,"maximum":null}}),
|
||||
),
|
||||
(
|
||||
"Text".into(),
|
||||
json!({"String":{"minimum_length":0,"maximum_length":null}}),
|
||||
),
|
||||
("Enumeration".into(), json!({"Enumeration":{"literals":[]}})),
|
||||
("Record".into(), json!("Record")),
|
||||
("Record".into(), json!({"Record":{"supertype":null}})),
|
||||
];
|
||||
}
|
||||
if field == "type_ref" {
|
||||
@@ -1273,6 +1378,9 @@ fn property<'a>(
|
||||
label: &str,
|
||||
skip_variant: bool,
|
||||
) -> View<'a, Message> {
|
||||
if let Some(view) = data_fields::property(draft, project, value, path) {
|
||||
return view;
|
||||
}
|
||||
let label_view = || text(label.to_owned()).size(11).color(style::MUTED);
|
||||
if path.ends_with("/Port/legacy") {
|
||||
let mut content = column![].spacing(4);
|
||||
@@ -2075,6 +2183,8 @@ mod tests {
|
||||
Layer::Oa,
|
||||
SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Energy,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
},
|
||||
);
|
||||
let mut draft = Draft::from_project(&p, item).unwrap();
|
||||
@@ -2091,8 +2201,12 @@ mod tests {
|
||||
let interface = p
|
||||
.add_element(Kind::Interface, Layer::Oa, "Power contract", None)
|
||||
.unwrap();
|
||||
p.elements.get_mut(&interface).unwrap().body =
|
||||
ElementBody::shared_in(Layer::Oa, SharedBody::Interface { items: vec![item] });
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Allocated item",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
assert!(execute_batch(&p, &[draft.command().unwrap()]).is_err());
|
||||
assert!(draft.dirty());
|
||||
assert_eq!(p.elements[&item].layer(), Layer::Oa);
|
||||
|
||||
Reference in New Issue
Block a user