//! Ordinary source defaults shared by current parsing and declaration rendering. //! //! These are source omission defaults, not graphical creation defaults. The //! policy operates on complete typed declaration maps. Required named fields //! and selected constructor payloads remain distinct from omission defaults. //! Historical versions use their existing frozen parser paths. Semantic baseline //! history is never traversed or normalized here. use crate::{Kind, RelationKind}; use serde_json::{Map, Value, json}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Context { Element(Kind), Relation(RelationKind), Diagram, Node, Artifact, Trace, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Mode { CompleteForEdit, Compact, /// Internal lossless-edit fragments: retain every top-level value while /// compacting its selected constructor's optional nested fields. CompactValues, } /// Top-level field inventory for the current ordinary reader. Callers must /// check required fields before merging; null entries only admit their names. /// This never supplies defaults inside a selected constructor. pub(crate) fn parser_fields(context: Context) -> Map { let mut fields = declaration_defaults(context); let required: &[&str] = match context { Context::Element(kind) => { fields.remove("description"); fields.remove("parent"); if kind == Kind::Port { &["contract"] } else { &[] } } Context::Relation(kind) => { fields.remove("description"); match kind { RelationKind::ContextualLink => &["owner", "source", "target", "purpose"], RelationKind::FunctionalExchange => { &["source", "target", "source_port", "target_port"] } _ => &["source", "target"], } } Context::Diagram => { // Admit the optional payload without restoring a synthetic null in // CompleteForEdit: absence there distinguishes a real layout clear // from an unrelated edit beside an authored explicit `layout none`. fields.insert("layout".into(), Value::Null); &[] } Context::Node => &["element"], Context::Artifact => &["owner"], Context::Trace => &["oa", "sa"], }; for name in required { fields.insert((*name).into(), Value::Null); } fields } pub(crate) fn fields( context: Context, input: &Map, mode: Mode, ) -> Map { let mut result = input.clone(); restore_serialization_skips(context, &mut result); if mode != Mode::CompleteForEdit { compact_nested(context, &mut result); } if mode == Mode::Compact { // Compact a composite and its declared default by the same nested rules // before whole-field comparison. This also makes compact input stable: // e.g. String{minimum_length:0} still denotes the default definition. let mut defaults = declaration_defaults(context); compact_nested(context, &mut defaults); omit_defaults(&mut result, &defaults); } result } fn restore_serialization_skips(context: Context, fields: &mut Map) { match context { Context::Element(Kind::Occurrence) => { if let Some(exchange) = constructor_payload(fields, "body", "Exchange") { exchange .entry("operation".to_owned()) .or_insert(Value::Null); } } Context::Diagram => { if let Some(routes) = fields.get_mut("routes").and_then(Value::as_array_mut) { for route in routes.iter_mut().filter_map(Value::as_object_mut) { route .entry("source_attachment".to_owned()) .or_insert(Value::Null); route .entry("target_attachment".to_owned()) .or_insert(Value::Null); } } } Context::Node => { fields.entry("port_anchors".to_owned()).or_insert(json!([])); } Context::Trace => { if let Some(baseline) = fields.get_mut("baseline").and_then(Value::as_object_mut) { baseline.entry("semantic".to_owned()).or_insert(Value::Null); } } _ => {} } } fn declaration_defaults(context: Context) -> Map { let mut defaults = match context { Context::Element(kind) => element_defaults(kind), Context::Relation(kind) => relation_defaults(kind), Context::Diagram => object(json!({ "description": "", "layer": "Oa", "projection": "Overview", "routes": [], "view_options": { "hidden_edges": [], "relation_kinds": "All", "work_categories": "All", "participant_categories": "All", "coverage": "All", "work_contexts": "All", "exchange_labels": "Name" } })), Context::Node => object(json!({ "position": {"x": 0.0, "y": 0.0}, "size": {"width": 224.0, "height": 94.0}, "collapsed": false, "port_anchors": [] })), Context::Artifact => object(json!({ "description": "", "relation": "References", "uri": "", "revision": null, "fragment": null })), Context::Trace => object(json!({ "description": "", "kind": "Refinement", "baseline": null })), }; match context { Context::Element(_) => { defaults.insert("description".into(), json!("")); defaults.insert("parent".into(), Value::Null); } Context::Relation(_) => { defaults.insert("description".into(), json!("")); } _ => {} } defaults } fn element_defaults(kind: Kind) -> Map { if let Some(state_kind) = kind.state_kind() { // The declaration selects this discriminator; no GUI factory is used. return object(json!({ "kind": state_kind, "entry": "", "exit": "", "do_activity": "" })); } match kind { Kind::Process | Kind::FunctionalChain => object(json!({"steps": []})), Kind::ExchangeItem => object(json!({ "kind": "Information", "mechanism": "Unspecified", "supertype": null })), Kind::ExchangeCategory => object(json!({"family": "Work", "exchanges": []})), Kind::DataType => object(json!({ "definition": {"String": {"minimum_length": 0, "maximum_length": null}}, "default": null, "unit": "" })), Kind::DataField => object(json!({ "type_ref": "String", "minimum": 1, "maximum": 1, "unit": "", "direction": "Unspecified", "storage": "Value", "default": null })), Kind::Interface => object(json!({"supertype": null})), // Port.contract is required, even when its explicit value is Legacy. Kind::Port => object(json!({"legacy": null})), Kind::Scenario => object(json!({ "viewpoint": "MixedLegacy", "context": null, "precondition": "", "postcondition": "", "steps": [] })), Kind::Lifeline => object(json!({"represented": null})), Kind::Occurrence => object(json!({ "body": {"Exchange": { "exchange": null, "sender": null, "receiver": null, "message_kind": "Asynchronous", "reply_to": null, "values": [], "operation": null }}, "precondition": "", "postcondition": "" })), Kind::Event => object(json!({"body": {"Signal": {"items": []}}})), Kind::Constraint => object(json!({"expression": "", "constrained": []})), // Unit-body declarations have no additional omission defaults. A future // Kind reaches this branch without gaining speculative payload rules. _ => Map::new(), } } fn relation_defaults(kind: RelationKind) -> Map { match kind { RelationKind::InterfaceItemAllocation => object(json!({ "order": 0, "transmission": "Unspecified", "acquisition": "Unspecified" })), RelationKind::DataAssociation => object(json!({ "source_role": "", "target_role": "", "source_minimum": 0, "target_minimum": 0, "source_maximum": null, "target_maximum": null, "source_navigable": false, "target_navigable": false })), RelationKind::Interaction | RelationKind::FunctionalExchange => { object(json!({"items": []})) } RelationKind::BoundaryExchange => object(json!({"items": [], "oriented": true})), RelationKind::CommunicationMean => object(json!({"items": [], "oriented": false})), RelationKind::CapabilityExtend => object(json!({"condition": ""})), RelationKind::SequenceLink => object(json!({"guard": ""})), RelationKind::StateTransition => object(json!({ "transition_kind": "External", "trigger": "", "guard": "", "effect": "", "triggers": [], "effects": [] })), RelationKind::StateAction => object(json!({"phase": "Do"})), // source/target, FunctionalExchange ports and ContextualLink's owner, // endpoints and purpose are required, despite factory placeholder values. _ => Map::new(), } } fn compact_nested(context: Context, fields: &mut Map) { match context { Context::Element(Kind::DataType) => compact_data_definition(fields), Context::Element(Kind::Port) => { if let Some(legacy) = fields.get_mut("legacy").and_then(Value::as_object_mut) { omit_nulls(legacy, &["interface"]); } // Every selected PortContract payload field remains required. } Context::Element(Kind::Occurrence) => compact_occurrence(fields), Context::Diagram => { if let Some(options) = fields .get_mut("view_options") .and_then(Value::as_object_mut) { omit_defaults( options, &object(json!({ "hidden_edges": [], "relation_kinds": "All", "work_categories": "All", "participant_categories": "All", "coverage": "All", "work_contexts": "All", "exchange_labels": "Name" })), ); } if let Some(routes) = fields.get_mut("routes").and_then(Value::as_array_mut) { for route in routes { if let Some(route) = route.as_object_mut() { omit_defaults( route, &object( json!({"waypoints": [], "label": null, "source_attachment": null, "target_attachment": null}), ), ); } } } // Route entries, EdgeLabel coordinates, projection machine IDs, and // Only([])/Any([]) selectors remain even when their values are empty. } Context::Trace => { if let Some(baseline) = fields.get_mut("baseline").and_then(Value::as_object_mut) { omit_nulls(baseline, &["semantic"]); } // No recursion through snapshots or semantic historical facets. } // DataValue/TypeRef payloads, EventBody fields, Node geometry/anchors, // contextual endpoints/purposes, and other required records stay intact. _ => {} } } fn compact_data_definition(fields: &mut Map) { for tag in ["Integer", "Real"] { if let Some(payload) = constructor_payload(fields, "definition", tag) { omit_nulls(payload, &["minimum", "maximum"]); } } if let Some(payload) = constructor_payload(fields, "definition", "String") { omit_nulls(payload, &["maximum_length"]); // minimum_length is required inside a selected String constructor. } if let Some(payload) = constructor_payload(fields, "definition", "Record") { omit_nulls(payload, &["supertype"]); } // Boolean has no payload; Enumeration.literals remains required even []. } fn compact_occurrence(fields: &mut Map) { if let Some(exchange) = constructor_payload(fields, "body", "Exchange") { omit_defaults( exchange, &object(json!({ "exchange": null, "sender": null, "receiver": null, "message_kind": "Asynchronous", "reply_to": null, "values": [], "operation": null })), ); if let Some(operation) = exchange.get_mut("operation").and_then(Value::as_object_mut) { omit_nulls(operation, &["allocation"]); // Keep the containing operation field even if its record becomes {}: // explicit unresolved operation intent is not an absent operation. } if let Some(values) = exchange.get_mut("values").and_then(Value::as_array_mut) { for value in values { if let Some(message_value) = value.as_object_mut() { omit_defaults(message_value, &object(json!({"path": []}))); } } } // MessageKind::Timer.expression, MessageValue.field/expression remain. } for (tag, optional) in [ ("Action", &["action", "lifeline"][..]), ("State", &["state", "lifeline"][..]), ("SequenceReference", &["sequence"][..]), ("ScenarioUse", &["scenario"][..]), ] { if let Some(payload) = constructor_payload(fields, "body", tag) { omit_nulls(payload, optional); } } // ScenarioUse.bindings, Control.kind, Fragment.operator/branches and // Branch.guard/occurrences are required, including empty strings/lists. } fn constructor_payload<'a>( fields: &'a mut Map, key: &str, tag: &str, ) -> Option<&'a mut Map> { let wrapper = fields.get_mut(key)?.as_object_mut()?; if wrapper.len() != 1 { return None; } wrapper.get_mut(tag)?.as_object_mut() } fn omit_nulls(fields: &mut Map, names: &[&str]) { for name in names { if fields.get(*name).is_some_and(Value::is_null) { fields.remove(*name); } } } fn omit_defaults(fields: &mut Map, defaults: &Map) { for (name, default) in defaults { if fields.get(name).is_some_and(|value| same(value, default)) { fields.remove(name); } } } // Comparison recurses only to compare a known field with its exact declared // default. It never traverses arbitrary data looking for values to remove. fn same(value: &Value, default: &Value) -> bool { match (value, default) { (Value::Object(a), Value::Object(b)) => { a.len() == b.len() && a.iter() .all(|(key, value)| b.get(key).is_some_and(|d| same(value, d))) } (Value::Array(a), Value::Array(b)) => { a.len() == b.len() && a.iter().zip(b).all(|(value, d)| same(value, d)) } (Value::Number(a), Value::Number(b)) => { // serde_json numeric equality alone can equate -0.0 with +0.0. // Extracted f32 coordinates are represented exactly as f64 here. a == b && match (a.as_f64(), b.as_f64()) { (Some(x), Some(y)) if x == 0.0 && y == 0.0 => x.to_bits() == y.to_bits(), _ => true, } } _ => value == default, } } fn object(value: Value) -> Map { match value { Value::Object(fields) => fields, _ => unreachable!("literal source default is an object"), } } #[cfg(test)] #[path = "source_defaults_tests.rs"] mod tests;