experiment(core): implement typed source-default field policy
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
//! Isolated ordinary source-7 omission-policy experiment, not production wiring.
|
||||
//!
|
||||
//! Input is the typed declaration-field map extracted by language.rs: header
|
||||
//! identity/name/alias are outside it; element description/parent and node diagram
|
||||
//! are included. CompleteForEdit restores only persistence-serializer skips at
|
||||
//! their applicable paths. It does not hydrate arbitrary partial source records.
|
||||
//! Compact removes only this source-7 context's admitted omission defaults.
|
||||
//!
|
||||
//! Parent is a document-level reference here: non-null parents remain. A caller
|
||||
//! emitting lexical nesting must handle its lexical owner separately. Project
|
||||
//! header description and version dispatch are likewise outside this interface.
|
||||
//! Unknown fields/constructors are preserved; source8 analysis and historical
|
||||
//! semantic baseline content are not traversed by this policy.
|
||||
|
||||
use oa_core::{Kind, RelationKind};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Context {
|
||||
Element(Kind),
|
||||
Relation(RelationKind),
|
||||
Diagram,
|
||||
Node,
|
||||
Artifact,
|
||||
Trace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
CompleteForEdit,
|
||||
Compact,
|
||||
}
|
||||
|
||||
pub 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::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.
|
||||
compact_nested(context, &mut result);
|
||||
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::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})));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user