406 lines
16 KiB
Rust
406 lines
16 KiB
Rust
//! 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<String, Value> {
|
|
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<String, Value>,
|
|
mode: Mode,
|
|
) -> Map<String, Value> {
|
|
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<String, Value>) {
|
|
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<String, Value> {
|
|
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<String, Value> {
|
|
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<String, Value> {
|
|
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<String, Value>) {
|
|
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<String, Value>) {
|
|
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<String, Value>) {
|
|
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<String, Value>,
|
|
key: &str,
|
|
tag: &str,
|
|
) -> Option<&'a mut Map<String, Value>> {
|
|
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<String, Value>, 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<String, Value>, defaults: &Map<String, Value>) {
|
|
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<String, Value> {
|
|
match value {
|
|
Value::Object(fields) => fields,
|
|
_ => unreachable!("literal source default is an object"),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "source_defaults_tests.rs"]
|
|
mod tests;
|