Compose ordinary source defaults with the reviewed Text workspace

This commit is contained in:
2026-09-13 06:33:04 -07:00
parent 136ed6e586
commit d7c065991d
14 changed files with 3366 additions and 50 deletions
+49 -15
View File
@@ -3,6 +3,8 @@
mod semantic_source;
#[path = "migration_v2/source.rs"]
mod v2_source;
pub(crate) use crate::source_defaults::Mode as DeclarationFormat;
use crate::source_defaults::{self, Context as DefaultContext};
use crate::*;
use serde_json::{Map, Value};
use std::{collections::BTreeMap, fmt};
@@ -645,25 +647,34 @@ fn offset_for(source: &str, line: usize, column: usize) -> usize {
source.len()
}
/// Canonical output uses persistent aliases and explicit identities. Typed
/// nested records are ordinary language fields; JSON documents are not embedded.
/// Canonical output uses persistent aliases and explicit identities, omitting
/// only values supplied by source defaults. Typed nested records are ordinary
/// language fields; JSON documents are not embedded.
pub fn format(project: &Project) -> String {
let mut out = std::format!(
"arcadia {SCHEMA_VERSION};\nproject {} {} id \"{}\" {{\n description {};\n\n",
"arcadia {SCHEMA_VERSION};\nproject {} {} id \"{}\" {{\n",
project.alias,
quote(&project.name),
project.id,
quote(&project.description)
);
for (_, declaration) in format_declarations(project) {
if !project.description.is_empty() {
out.push_str(&std::format!(
" description {};\n",
quote(&project.description)
));
}
out.push('\n');
for (_, declaration) in format_declarations(project, DeclarationFormat::Compact) {
out.push_str(&declaration);
}
out.push_str("}\n");
out
}
/// The same typed declaration writer is used by flat and modular source output.
pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
/// Shared typed writer for canonical output and lossless editing. Complete edit
/// comparisons retain defaults; emitted declarations and value fragments apply
/// only the omission rules valid in their selected source context.
pub(crate) fn format_declarations(project: &Project, mode: DeclarationFormat) -> Vec<(Id, String)> {
let aliases = project
.identities()
.into_iter()
@@ -675,6 +686,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
let mut fields = payload(&serde_json::to_value(&e.body).unwrap());
fields.insert("description".into(), Value::String(e.description.clone()));
fields.insert("parent".into(), serde_json::to_value(e.parent).unwrap());
let fields = source_defaults::fields(DefaultContext::Element(e.kind()), &fields, mode);
write_declaration(
&mut out,
&element_word(e.kind(), e.layer()),
@@ -690,6 +702,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
let mut out = String::new();
let mut fields = payload(&serde_json::to_value(&r.body).unwrap());
fields.insert("description".into(), Value::String(r.description.clone()));
let fields = source_defaults::fields(DefaultContext::Relation(r.kind()), &fields, mode);
write_declaration(
&mut out,
&relation_word(r.kind()),
@@ -705,6 +718,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
let mut out = String::new();
let mut fields = record_fields(d);
fields.remove("nodes");
let fields = source_defaults::fields(DefaultContext::Diagram, &fields, mode);
write_declaration(
&mut out,
"diagram",
@@ -719,6 +733,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
let mut out = String::new();
let mut fields = record_fields(n);
fields.insert("diagram".into(), Value::String(d.id.to_string()));
let fields = source_defaults::fields(DefaultContext::Node, &fields, mode);
write_declaration(&mut out, "node", &n.alias, None, n.id, &fields, &aliases);
declarations.push((n.id, out));
}
@@ -731,7 +746,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
&a.alias,
Some(&a.name),
a.id,
&record_fields(a),
&source_defaults::fields(DefaultContext::Artifact, &record_fields(a), mode),
&aliases,
);
declarations.push((a.id, out));
@@ -744,7 +759,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
&t.alias,
Some(&t.name),
t.id,
&record_fields(t),
&source_defaults::fields(DefaultContext::Trace, &record_fields(t), mode),
&aliases,
);
declarations.push((t.id, out));
@@ -1742,7 +1757,9 @@ pub(crate) fn resolve_declarations(
*viewpoint = ScenarioViewpoint::MixedLegacy;
}
let mut body = serde_json::to_value(initial).unwrap();
let defaults = if legacy {
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Element(kind))
} else if legacy {
crate::persistence::v3::legacy_defaults(kind)
.and_then(|value| value.as_object().cloned())
.unwrap_or_else(|| payload(&body))
@@ -1799,7 +1816,12 @@ pub(crate) fn resolve_declarations(
let mut body =
serde_json::to_value(RelationBody::new(kind, Id::nil(), Id::nil()))
.unwrap();
let merged = merge_fields(payload(&body), fields).map_err(&conversion_error)?;
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Relation(kind))
} else {
payload(&body)
};
let merged = merge_fields(defaults, fields).map_err(&conversion_error)?;
replace_payload(&mut body, merged);
let body = serde_json::from_value(body)
.map_err(|e| conversion_error(e.to_string()))?;
@@ -1824,7 +1846,10 @@ pub(crate) fn resolve_declarations(
}
let defaults =
serde_json::json!({"description":"","layer":"Oa","projection":"Overview","routes":[],"view_options":crate::view_options::ViewOptions::default()});
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Diagram)
} else { defaults.as_object().unwrap().clone() };
let mut record = merge_fields(defaults, fields)
.map_err(&conversion_error)?;
header(&mut record, d, true);
record.insert("nodes".into(), serde_json::json!({}));
@@ -1839,7 +1864,10 @@ pub(crate) fn resolve_declarations(
return Err(conversion_error("Missing required field 'element'.".into()));
}
let defaults = serde_json::json!({"element":null,"position":{"x":0.0,"y":0.0},"size":{"width":224.0,"height":94.0},"collapsed":false,"port_anchors":[]});
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Node)
} else { defaults.as_object().unwrap().clone() };
let mut record = merge_fields(defaults, fields)
.map_err(&conversion_error)?;
header(&mut record, d, false);
let node: NodePlacement = serde_json::from_value(Value::Object(record))
@@ -1851,7 +1879,10 @@ pub(crate) fn resolve_declarations(
return Err(conversion_error("Missing required field 'owner'.".into()));
}
let defaults = serde_json::json!({"description":"","owner":null,"relation":"References","uri":"","revision":null,"fragment":null});
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Artifact)
} else { defaults.as_object().unwrap().clone() };
let mut record = merge_fields(defaults, fields)
.map_err(&conversion_error)?;
header(&mut record, d, true);
let artifact = serde_json::from_value(Value::Object(record))
@@ -1867,7 +1898,10 @@ pub(crate) fn resolve_declarations(
}
}
let defaults = serde_json::json!({"description":"","oa":null,"sa":null,"kind":"Refinement","baseline":null});
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
let defaults = if project.schema_version >= 7 {
source_defaults::parser_fields(DefaultContext::Trace)
} else { defaults.as_object().unwrap().clone() };
let mut record = merge_fields(defaults, fields)
.map_err(&conversion_error)?;
header(&mut record, d, true);
let trace = if project.schema_version < 5 {
+1
View File
@@ -11,6 +11,7 @@ pub mod references;
pub mod routing;
pub mod semantic;
pub mod session;
mod source_defaults;
pub mod source_files;
pub mod source_project;
pub mod topology_context;
+382
View File
@@ -0,0 +1,382 @@
//! 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 => &[],
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::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"),
}
}
#[cfg(test)]
#[path = "source_defaults_tests.rs"]
mod tests;
+718
View File
@@ -0,0 +1,718 @@
//! Independent literal oracles for the production source-default policy.
use super::{self as policy, Context, Mode};
use crate::{Kind, RelationKind};
use serde_json::{Value, json};
#[test]
fn complete_comparison_keeps_populated_serializer_skip_values() {
for (context, input) in [
(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"operation":{"allocation":"allocation-id"},"values":[]}}}),
),
(
Context::Node,
json!({"port_anchors":[{"port":"port-id","side":"Left","offset":0.0}]}),
),
(
Context::Trace,
json!({"baseline":{"semantic":{"historical":"payload"}}}),
),
] {
assert_eq!(rendered(context, &input, Mode::CompleteForEdit), input);
}
}
#[test]
fn compact_value_fragments_keep_selected_constructor_requirements() {
for (context, input, expected) in [
(
Context::Element(Kind::DataType),
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null}},"default":null,"unit":""}),
json!({"definition":{"String":{"minimum_length":0}},"default":null,"unit":""}),
),
(
Context::Element(Kind::Event),
json!({"body":{"Signal":{"items":[]}}}),
json!({"body":{"Signal":{"items":[]}}}),
),
(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"operation":{"allocation":null},"values":[]}},"precondition":""}),
json!({"body":{"Exchange":{"operation":{}}},"precondition":""}),
),
(
Context::Element(Kind::Occurrence),
json!({"body":{"ScenarioUse":{"scenario":null,"bindings":[]}}}),
json!({"body":{"ScenarioUse":{"bindings":[]}}}),
),
] {
assert_eq!(rendered(context, &input, Mode::CompactValues), expected);
}
}
#[test]
fn source_scenario_default_is_independent_of_graphical_creation() {
use crate::{ElementBody, Layer, ScenarioViewpoint, SharedBody, language};
let project = language::parse(
"arcadia 7; project p \"P\" { oa_scenario o \"O\" {} sa_scenario s \"S\" {} }",
)
.unwrap();
for element in project.elements.values() {
assert!(matches!(
element.body.shared(),
Some(SharedBody::Scenario {
viewpoint: ScenarioViewpoint::MixedLegacy,
..
})
));
assert!(!matches!(
ElementBody::new(Kind::Scenario, element.layer())
.unwrap()
.shared(),
Some(SharedBody::Scenario {
viewpoint: ScenarioViewpoint::MixedLegacy,
..
})
));
}
let source = language::format(&project);
assert!(!source.contains("description"));
assert!(!source.contains("parent"));
assert!(!source.contains("viewpoint"));
assert_eq!(language::parse(&source).unwrap(), project);
let mut typed = project;
for element in typed.elements.values_mut() {
let viewpoint = if element.layer() == Layer::Oa {
ScenarioViewpoint::OperationalActivity
} else {
ScenarioViewpoint::Functional
};
*element.body.shared_mut().unwrap() = SharedBody::Scenario {
viewpoint,
context: None,
precondition: String::new(),
postcondition: String::new(),
steps: vec![],
};
}
let source = language::format(&typed);
assert!(source.contains("viewpoint OperationalActivity;"));
assert!(source.contains("viewpoint Functional;"));
assert_eq!(language::parse(&source).unwrap(), typed);
}
#[test]
fn shared_parser_defaults_preserve_required_fields_and_kind_checks() {
for (declaration, message) in [
(
"oa_port p0 \"Port\" {}",
"Missing required field 'contract'",
),
(
"allocation r \"R\" { source @owner; }",
"Missing required field 'target'",
),
(
"functional_exchange r \"R\" { source @owner; target @work; }",
"Missing required field 'source_port'",
),
(
"contextual_link r \"R\" {}",
"Missing required field 'owner'",
),
("node n { diagram @d; }", "Missing required field 'element'"),
("artifact a \"A\" {}", "Missing required field 'owner'"),
("trace t \"T\" {}", "Missing required field 'oa'"),
(
"oa_state s \"S\" { kind Initial; }",
"Declaration kind disagrees",
),
(
"oa_activity a \"A\" { unknown true; }",
"Unknown field 'unknown'",
),
(
"oa_data_type t \"T\" { definition { String {}; }; }",
"minimum_length",
),
(
"oa_occurrence o \"O\" { body { ScenarioUse {}; }; }",
"bindings",
),
] {
let source = format!(
"arcadia 7; project p \"P\" {{ oa_entity owner \"Owner\" {{}} oa_activity work \"Work\" {{}} diagram d \"D\" {{}} {declaration} }}"
);
let errors = crate::language::parse(&source).unwrap_err();
assert!(
errors.iter().any(|error| error.message.contains(message)),
"{declaration}: {errors:?}"
);
}
}
fn rendered(context: Context, input: &Value, mode: Mode) -> Value {
Value::Object(policy::fields(context, input.as_object().unwrap(), mode))
}
fn compact(context: Context, input: Value, expected: Value) {
let original = input.clone();
let result = rendered(context, &input, Mode::Compact);
assert_eq!(result, expected, "context {context:?}");
assert_eq!(input, original, "the input remains immutable");
assert_eq!(
rendered(context, &result, Mode::Compact),
result,
"compact policy is idempotent"
);
}
#[test]
fn complete_comparison_restores_only_the_three_known_skips() {
let exchange = json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}}});
let mut complete = exchange.clone();
complete["body"]["Exchange"]["operation"] = Value::Null;
assert_eq!(
rendered(
Context::Element(Kind::Occurrence),
&exchange,
Mode::CompleteForEdit
),
complete
);
assert_eq!(
rendered(
Context::Node,
&json!({"element":"e","diagram":"d"}),
Mode::CompleteForEdit
),
json!({"element":"e","diagram":"d","port_anchors":[]})
);
assert_eq!(
rendered(
Context::Trace,
&json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"","description":""}}}),
Mode::CompleteForEdit
),
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"","description":""},"semantic":null}})
);
for input in [
json!({"body":{"Action":{"action":null,"lifeline":null}}}),
json!({"body":{"State":{"state":null,"lifeline":null}}}),
] {
assert_eq!(
rendered(
Context::Element(Kind::Occurrence),
&input,
Mode::CompleteForEdit
),
input
);
}
for input in [
json!({"baseline":null}),
json!({"baseline":{"semantic":{"historical":[null,[],false,0,""]},"oa":{"name":"n","description":""},"sa":{"name":"s","description":""}}}),
] {
assert_eq!(
rendered(Context::Trace, &input, Mode::CompleteForEdit),
input
);
}
}
#[test]
fn every_element_omits_only_empty_common_defaults() {
for kind in Kind::ALL {
// Future/unknown payloads are preservation sentinels, not admitted source.
compact(
Context::Element(kind),
json!({"description":"","parent":null,"future":{"empty":[],"absent":null,"zero":0,"flag":false}}),
json!({"future":{"empty":[],"absent":null,"zero":0,"flag":false}}),
);
compact(
Context::Element(kind),
json!({"description":" \n","parent":"owner"}),
json!({"description":" \n","parent":"owner"}),
);
}
}
#[test]
fn ordinary_element_defaults_are_context_specific() {
for kind in [Kind::Process, Kind::FunctionalChain] {
compact(Context::Element(kind), json!({"steps":[]}), json!({}));
compact(
Context::Element(kind),
json!({"steps":["second","first"]}),
json!({"steps":["second","first"]}),
);
}
compact(
Context::Element(Kind::ExchangeItem),
json!({"kind":"Information","mechanism":"Unspecified","supertype":null}),
json!({}),
);
compact(
Context::Element(Kind::ExchangeItem),
json!({"kind":"Energy","mechanism":"Flow","supertype":"base"}),
json!({"kind":"Energy","mechanism":"Flow","supertype":"base"}),
);
compact(
Context::Element(Kind::ExchangeCategory),
json!({"family":"Work","exchanges":[]}),
json!({}),
);
compact(
Context::Element(Kind::ExchangeCategory),
json!({"family":"Participant","exchanges":["x"]}),
json!({"family":"Participant","exchanges":["x"]}),
);
compact(
Context::Element(Kind::Interface),
json!({"supertype":null}),
json!({}),
);
compact(
Context::Element(Kind::Lifeline),
json!({"represented":null}),
json!({}),
);
compact(
Context::Element(Kind::Constraint),
json!({"expression":"","constrained":[]}),
json!({}),
);
}
#[test]
fn scenario_source_default_never_uses_the_graphical_viewpoint_default() {
compact(
Context::Element(Kind::Scenario),
json!({"viewpoint":"MixedLegacy","context":null,"precondition":"","postcondition":"","steps":[]}),
json!({}),
);
for viewpoint in [
"OperationalActivity",
"Functional",
"ParticipantInteraction",
"BoundaryInterface",
] {
compact(
Context::Element(Kind::Scenario),
json!({"viewpoint":viewpoint,"context":"scope","precondition":"ready","postcondition":"","steps":["two","one"]}),
json!({"viewpoint":viewpoint,"context":"scope","precondition":"ready","steps":["two","one"]}),
);
}
}
#[test]
fn each_state_header_supplies_its_own_kind() {
for (kind, name) in [
(Kind::State, "State"),
(Kind::Mode, "Mode"),
(Kind::Initial, "Initial"),
(Kind::Final, "Final"),
(Kind::Choice, "Choice"),
(Kind::Junction, "Junction"),
(Kind::Fork, "Fork"),
(Kind::Join, "Join"),
(Kind::ShallowHistory, "ShallowHistory"),
(Kind::DeepHistory, "DeepHistory"),
(Kind::EntryPoint, "EntryPoint"),
(Kind::ExitPoint, "ExitPoint"),
(Kind::Terminate, "Terminate"),
] {
compact(
Context::Element(kind),
json!({"kind":name,"entry":"","exit":"","do_activity":""}),
json!({}),
);
}
// Invalid mismatches must not be silently normalized by the writer policy.
compact(
Context::Element(Kind::Mode),
json!({"kind":"State","entry":"work"}),
json!({"kind":"State","entry":"work"}),
);
}
#[test]
fn data_field_unbounded_and_present_empty_values_are_not_defaults() {
compact(
Context::Element(Kind::DataField),
json!({"type_ref":"String","minimum":1,"maximum":1,"unit":"","direction":"Unspecified","storage":"Value","default":null}),
json!({}),
);
compact(
Context::Element(Kind::DataField),
json!({"type_ref":{"Named":"record"},"minimum":0,"maximum":null,"unit":"m","direction":"Out","storage":"Reference","default":[]}),
json!({"type_ref":{"Named":"record"},"minimum":0,"maximum":null,"unit":"m","direction":"Out","storage":"Reference","default":[]}),
);
for value in [
json!({"Boolean":false}),
json!({"Integer":0}),
json!({"Real":0.0}),
json!({"String":""}),
] {
compact(
Context::Element(Kind::DataField),
json!({"default":[value.clone()]}),
json!({"default":[value]}),
);
}
}
#[test]
fn data_definition_whole_default_and_selected_required_fields_differ() {
compact(
Context::Element(Kind::DataType),
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null}},"default":null,"unit":""}),
json!({}),
);
for (input, expected) in [
(
json!({"Integer":{"minimum":null,"maximum":null}}),
json!({"Integer":{}}),
),
(
json!({"Real":{"minimum":null,"maximum":2.5}}),
json!({"Real":{"maximum":2.5}}),
),
(
json!({"String":{"minimum_length":4,"maximum_length":null}}),
json!({"String":{"minimum_length":4}}),
),
(json!({"Record":{"supertype":null}}), json!({"Record":{}})),
(
json!({"Enumeration":{"literals":[]}}),
json!({"Enumeration":{"literals":[]}}),
),
] {
compact(
Context::Element(Kind::DataType),
json!({"definition":input}),
json!({"definition":expected}),
);
}
compact(
Context::Element(Kind::DataType),
json!({"definition":"Boolean","default":{"Boolean":false}}),
json!({"definition":"Boolean","default":{"Boolean":false}}),
);
}
#[test]
fn port_contract_and_its_required_payload_stay_explicit() {
compact(
Context::Element(Kind::Port),
json!({"contract":"Legacy","legacy":{"direction":"Out","interface":null}}),
json!({"contract":"Legacy","legacy":{"direction":"Out"}}),
);
for contract in [
json!({"FunctionInput":{"incoming_items":[]}}),
json!({"FunctionOutput":{"outgoing_items":[]}}),
json!({"Component":{"kind":"Standard","orientation":"Unset"}}),
] {
compact(
Context::Element(Kind::Port),
json!({"contract":contract.clone(),"legacy":null}),
json!({"contract":contract}),
);
}
}
#[test]
fn occurrence_whole_default_is_distinct_from_selected_constructor_payload() {
compact(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}},"precondition":"","postcondition":""}),
json!({}),
);
for (input, expected) in [
(
json!({"Action":{"action":null,"lifeline":null}}),
json!({"Action":{}}),
),
(
json!({"State":{"state":null,"lifeline":null}}),
json!({"State":{}}),
),
(
json!({"SequenceReference":{"sequence":null}}),
json!({"SequenceReference":{}}),
),
(
json!({"ScenarioUse":{"scenario":null,"bindings":[]}}),
json!({"ScenarioUse":{"bindings":[]}}),
),
(
json!({"Branch":{"guard":"","occurrences":[]}}),
json!({"Branch":{"guard":"","occurrences":[]}}),
),
(
json!({"Fragment":{"operator":"Parallel","branches":[]}}),
json!({"Fragment":{"operator":"Parallel","branches":[]}}),
),
(
json!({"Control":{"kind":"Choice"}}),
json!({"Control":{"kind":"Choice"}}),
),
] {
compact(
Context::Element(Kind::Occurrence),
json!({"body":input}),
json!({"body":expected}),
);
}
}
#[test]
fn unresolved_operation_and_required_message_value_are_preserved() {
compact(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[{"field":"f","path":[],"expression":""}],"operation":{"allocation":null}}}}),
json!({"body":{"Exchange":{"values":[{"field":"f","expression":""}],"operation":{}}}}),
);
compact(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"exchange":"flow","sender":"a","receiver":"b","message_kind":{"Timer":{"expression":""}},"reply_to":"request","values":[],"operation":null}}}),
json!({"body":{"Exchange":{"exchange":"flow","sender":"a","receiver":"b","message_kind":{"Timer":{"expression":""}},"reply_to":"request"}}}),
);
}
#[test]
fn event_body_whole_default_does_not_prune_selected_required_fields() {
compact(
Context::Element(Kind::Event),
json!({"body":{"Signal":{"items":[]}}}),
json!({}),
);
for body in [
json!({"Signal":{"items":["i"]}}),
json!({"Time":{"kind":"After","expression":""}}),
json!({"Change":{"expression":""}}),
] {
compact(
Context::Element(Kind::Event),
json!({"body":body.clone()}),
json!({"body":body}),
);
}
}
#[test]
fn relation_required_endpoints_and_contextual_payloads_remain() {
for kind in RelationKind::ALL {
compact(
Context::Relation(kind),
json!({"description":"","source":"s","target":"t","future":[]}),
json!({"source":"s","target":"t","future":[]}),
);
}
compact(
Context::Relation(RelationKind::FunctionalExchange),
json!({"source":"s","target":"t","source_port":"sp","target_port":"tp","items":[]}),
json!({"source":"s","target":"t","source_port":"sp","target_port":"tp"}),
);
let contextual = json!({"owner":"o","source":{"references":[],"occurrence":"s"},"target":{"references":[],"occurrence":"t"},"purpose":{"Precedence":{"guard":""}}});
compact(
Context::Relation(RelationKind::ContextualLink),
contextual.clone(),
contextual,
);
}
#[test]
fn relation_defaults_preserve_contextual_orientation_and_multiplicity() {
for (kind, orientation) in [
(RelationKind::BoundaryExchange, true),
(RelationKind::CommunicationMean, false),
] {
compact(
Context::Relation(kind),
json!({"oriented":orientation,"items":[]}),
json!({}),
);
compact(
Context::Relation(kind),
json!({"oriented":!orientation}),
json!({"oriented":!orientation}),
);
}
compact(
Context::Relation(RelationKind::DataAssociation),
json!({"source_role":"","target_role":"","source_minimum":0,"target_minimum":0,"source_maximum":null,"target_maximum":null,"source_navigable":false,"target_navigable":false}),
json!({}),
);
compact(
Context::Relation(RelationKind::DataAssociation),
json!({"source_maximum":0,"target_maximum":1,"source_navigable":true}),
json!({"source_maximum":0,"target_maximum":1,"source_navigable":true}),
);
compact(
Context::Relation(RelationKind::InterfaceItemAllocation),
json!({"order":0,"transmission":"Unspecified","acquisition":"Unspecified"}),
json!({}),
);
compact(
Context::Relation(RelationKind::CapabilityExtend),
json!({"condition":""}),
json!({}),
);
compact(
Context::Relation(RelationKind::SequenceLink),
json!({"guard":""}),
json!({}),
);
compact(
Context::Relation(RelationKind::StateAction),
json!({"phase":"Do"}),
json!({}),
);
compact(
Context::Relation(RelationKind::StateTransition),
json!({"transition_kind":"External","trigger":"","guard":"","effect":"","triggers":[],"effects":[]}),
json!({}),
);
compact(
Context::Relation(RelationKind::StateTransition),
json!({"transition_kind":"Internal","triggers":["t"],"effects":["e"]}),
json!({"transition_kind":"Internal","triggers":["t"],"effects":["e"]}),
);
}
#[test]
fn diagram_options_remove_only_unrestricted_defaults() {
let defaults = json!({"hidden_edges":[],"relation_kinds":"All","work_categories":"All","participant_categories":"All","coverage":"All","work_contexts":"All","exchange_labels":"Name"});
compact(
Context::Diagram,
json!({"description":"","layer":"Oa","projection":"Overview","routes":[],"view_options":defaults}),
json!({}),
);
compact(
Context::Diagram,
json!({"layer":"Sa","projection":{"StateMachine":{"machine":"m"}},"view_options":{"hidden_edges":[],"relation_kinds":{"Only":[]},"work_categories":{"Any":[]},"participant_categories":"All","coverage":"All","work_contexts":{"Any":[]},"exchange_labels":"Name"}}),
json!({"layer":"Sa","projection":{"StateMachine":{"machine":"m"}},"view_options":{"relation_kinds":{"Only":[]},"work_categories":{"Any":[]},"work_contexts":{"Any":[]}}}),
);
compact(
Context::Diagram,
json!({"routes":[{"edge":"e","waypoints":[],"label":null}]}),
json!({"routes":[{"edge":"e"}]}),
);
}
#[test]
fn node_whole_defaults_preserve_negative_zero_and_required_coordinates() {
compact(
Context::Node,
json!({"diagram":"d","element":"e","position":{"x":0.0,"y":0.0},"size":{"width":224.0,"height":94.0},"collapsed":false}),
json!({"diagram":"d","element":"e"}),
);
for position in [
json!({"x":-0.0,"y":0.0}),
json!({"x":0.0,"y":-0.0}),
json!({"x":1.0,"y":0.0}),
] {
let result = rendered(
Context::Node,
&json!({"position":position.clone()}),
Mode::Compact,
);
assert_eq!(result, json!({"position":position.clone()}));
for coordinate in ["x", "y"] {
assert_eq!(
result["position"][coordinate].as_f64().unwrap().to_bits(),
position[coordinate].as_f64().unwrap().to_bits(),
"coordinate sign is part of fidelity"
);
}
}
compact(
Context::Node,
json!({"size":{"width":300.0,"height":94.0},"port_anchors":[{"port":"p","side":"Left","offset":0.0}]}),
json!({"size":{"width":300.0,"height":94.0},"port_anchors":[{"port":"p","side":"Left","offset":0.0}]}),
);
}
#[test]
fn artifact_present_empty_strings_and_trace_history_stay_distinct() {
compact(
Context::Artifact,
json!({"owner":"o","description":"","relation":"References","uri":"","revision":null,"fragment":null}),
json!({"owner":"o"}),
);
compact(
Context::Artifact,
json!({"owner":"o","relation":"Implements","revision":"","fragment":""}),
json!({"owner":"o","relation":"Implements","revision":"","fragment":""}),
);
compact(
Context::Trace,
json!({"oa":"o","sa":"s","description":"","kind":"Refinement","baseline":null}),
json!({"oa":"o","sa":"s"}),
);
compact(
Context::Trace,
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"s","description":""}}}),
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"s","description":""}}}),
);
let baseline = json!({"oa":{"name":"o","description":""},"sa":{"name":"s","description":""},"semantic":{"facets":[{"Value":null},{"Value":[]},{"Value":false},{"Value":0},{"Value":""}]}});
compact(
Context::Trace,
json!({"kind":"Manual","baseline":baseline.clone()}),
json!({"kind":"Manual","baseline":baseline}),
);
}
#[test]
fn nested_future_fields_prevent_whole_default_removal() {
// These sentinels exercise preservation, not current parser admission of
// unknown fields. Only known slots may lose their redundant defaults.
compact(
Context::Element(Kind::DataType),
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null,"future":[]}}}),
json!({"definition":{"String":{"minimum_length":0,"future":[]}}}),
);
compact(
Context::Element(Kind::Occurrence),
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[],"operation":null,"future":false}}}),
json!({"body":{"Exchange":{"future":false}}}),
);
compact(
Context::Diagram,
json!({"view_options":{"hidden_edges":[],"relation_kinds":"All","work_categories":"All","participant_categories":"All","coverage":"All","work_contexts":"All","exchange_labels":"Name","future":null}}),
json!({"view_options":{"future":null}}),
);
}
#[test]
fn unknown_and_multikey_constructor_wrappers_are_not_reinterpreted() {
for definition in [
json!({"Future":{"minimum":null,"maximum":null}}),
json!({"String":{"minimum_length":0,"maximum_length":null},"Future":{}}),
] {
compact(
Context::Element(Kind::DataType),
json!({"definition":definition.clone()}),
json!({"definition":definition}),
);
}
for body in [
json!({"Future":{"operation":null,"values":[]}}),
json!({"Exchange":{"exchange":null,"values":[]},"Future":{}}),
] {
let fields = json!({"body":body});
assert_eq!(
rendered(
Context::Element(Kind::Occurrence),
&fields,
Mode::CompleteForEdit
),
fields,
"do not invent a selected Exchange constructor in an unknown wrapper",
);
compact(Context::Element(Kind::Occurrence), fields.clone(), fields);
}
}
+131 -20
View File
@@ -726,7 +726,8 @@ fn apply_edits(bundle: &mut SourceBundle, edits: Edits) -> Result<(), Vec<Source
pub fn format_bundle(project: &Project) -> SourceBundle {
let ownership = default_ownership(project);
let aliases: BTreeMap<_, _> = project.identities().into_iter().collect();
let mut declarations = language::format_declarations(project);
let mut declarations =
language::format_declarations(project, language::DeclarationFormat::Compact);
declarations.sort_by_key(|(id, _)| declaration_order(project, *id, &aliases));
let mut files: BTreeMap<LogicalPath, String> = BTreeMap::new();
for (id, declaration) in declarations {
@@ -735,11 +736,13 @@ pub fn format_bundle(project: &Project) -> SourceBundle {
.or_default()
.push_str(&unindent(&declaration));
}
let mut entry = format!(
"arcadia {SCHEMA_VERSION};\n{}{{\n description {};\n",
project_header(project),
quote(&project.description)
);
let mut entry = format!("arcadia {SCHEMA_VERSION};\n{}{{\n", project_header(project));
if !project.description.is_empty() {
entry.push_str(&format!(
" description {};\n",
quote(&project.description)
));
}
for path in files.keys() {
entry.push_str(&format!(" include {};\n", quote(path)));
}
@@ -850,8 +853,25 @@ pub fn update_bundle_with_identity_map(
)]);
}
let origins: BTreeMap<_, _> = identities.iter().map(|(old, new)| (*new, *old)).collect();
let before: BTreeMap<_, _> = language::format_declarations(old).into_iter().collect();
let after: BTreeMap<_, _> = language::format_declarations(project).into_iter().collect();
// Comparison must include defaults even when persistence serialization or
// compact output omits them. Otherwise an unrelated payload edit erases
// unchanged explicit fields from the authored document.
let before: BTreeMap<_, _> =
language::format_declarations(old, language::DeclarationFormat::CompleteForEdit)
.into_iter()
.collect();
let after: BTreeMap<_, _> =
language::format_declarations(project, language::DeclarationFormat::CompleteForEdit)
.into_iter()
.collect();
let compact: BTreeMap<_, _> =
language::format_declarations(project, language::DeclarationFormat::Compact)
.into_iter()
.collect();
let compact_values: BTreeMap<_, _> =
language::format_declarations(project, language::DeclarationFormat::CompactValues)
.into_iter()
.collect();
let old_aliases: BTreeMap<_, _> = old
.identities()
.into_iter()
@@ -907,6 +927,27 @@ pub fn update_bundle_with_identity_map(
};
let mut previous = parse(old_text)?;
let mut next = parse(new_text)?;
let next_id = identities[&id];
let value_text = &compact_values[&next_id];
let insertion = InsertionSyntax::new(
&next.fields,
&parse(value_text)?.fields,
value_text,
&parse(&compact[&next_id])?.fields,
);
let old_kind = previous.header["kind"];
let new_kind = next.header["kind"];
if old_text[old_kind.start..old_kind.end] != new_text[new_kind.start..new_kind.end]
&& let language::EditKind::Record(actual_fields) = &actual.fields.kind
&& let language::EditKind::Record(previous_fields) = &mut previous.fields.kind
{
// An implicit value is supplied by its declaration context.
// After a kind change, an unchanged value may need explicit
// syntax (e.g. BoundaryExchange true -> CommunicationMean true).
// Reconsider only absent fields; authored explicit values still
// compare against their complete old values and keep trivia.
previous_fields.retain(|key, _| actual_fields.contains_key(key));
}
if actual.lexical_owner.is_some()
&& let language::EditKind::Record(fields) = &actual.fields.kind
&& !fields.contains_key("parent")
@@ -923,6 +964,7 @@ pub fn update_bundle_with_identity_map(
source,
before: old_text,
after: new_text,
insertion: Some(&insertion),
old_aliases: &old_aliases,
new_aliases: &new_aliases,
legacy_value_paths: false,
@@ -953,6 +995,7 @@ pub fn update_bundle_with_identity_map(
source,
before: &before,
after: &after,
insertion: None,
old_aliases: &old_aliases,
new_aliases: &new_aliases,
legacy_value_paths: false,
@@ -1011,7 +1054,7 @@ pub fn update_bundle_with_identity_map(
project,
identities,
&original_order,
&after,
&compact,
limits,
)?;
let result = compile_bundle(&bundle, limits)?;
@@ -1119,10 +1162,73 @@ fn inserted_value(text: &str, indent: &str, source: &str) -> String {
output
}
// Value fragments retain required payload fields even when the enclosing
// declaration field is itself omittable. For example, a selected String type
// needs minimum_length; an Event Signal needs items. The complete tree remains
// the comparison authority; this parallel tree is only an insertion template.
struct InsertionSyntax<'a> {
source: &'a str,
values: BTreeMap<usize, Span>,
omitted_fields: BTreeSet<usize>,
}
impl<'a> InsertionSyntax<'a> {
fn new(
complete: &language::EditValue,
compact_values: &language::EditValue,
source: &'a str,
compact: &language::EditValue,
) -> Self {
let mut result = Self {
source,
values: BTreeMap::new(),
omitted_fields: BTreeSet::new(),
};
result.collect(complete, compact_values);
if let (language::EditKind::Record(all), language::EditKind::Record(kept)) =
(&complete.kind, &compact.kind)
{
for (key, field) in all {
if !kept.contains_key(key) {
result.omitted_fields.insert(field.value.span.start);
}
}
}
result
}
fn collect(&mut self, complete: &language::EditValue, compact: &language::EditValue) {
// Borrow the existing compact text once. Copying each containing record
// here would duplicate a large nested payload at every depth.
self.values.insert(complete.span.start, compact.span);
match (&complete.kind, &compact.kind) {
(language::EditKind::Record(all), language::EditKind::Record(kept)) => {
for (key, field) in all {
if let Some(next) = kept.get(key) {
self.collect(&field.value, &next.value);
} else {
self.omitted_fields.insert(field.value.span.start);
}
}
}
(language::EditKind::List(all), language::EditKind::List(kept)) => {
assert_eq!(
all.len(),
kept.len(),
"compaction preserves array membership"
);
for (field, next) in all.iter().zip(kept) {
self.collect(field, next);
}
}
_ => {}
}
}
}
struct SyntaxUpdate<'a> {
source: &'a str,
before: &'a str,
after: &'a str,
insertion: Option<&'a InsertionSyntax<'a>>,
old_aliases: &'a BTreeMap<String, String>,
new_aliases: &'a BTreeMap<String, String>,
// Source3 migration only adds path qualification; the existing field identity
@@ -1131,6 +1237,16 @@ struct SyntaxUpdate<'a> {
identity_origins: Option<&'a BTreeMap<Id, Id>>,
}
impl SyntaxUpdate<'_> {
fn inserted(&self, value: &language::EditValue) -> &str {
self.insertion
.and_then(|syntax| {
syntax
.values
.get(&value.span.start)
.map(|span| &syntax.source[span.start..span.end])
})
.unwrap_or(&self.after[value.span.start..value.span.end])
}
fn header(
&self,
actual: &BTreeMap<&str, Span>,
@@ -1243,7 +1359,7 @@ impl SyntaxUpdate<'_> {
_ => {
erase_syntax(self.source, actual.span, edits);
let text = inserted_value(
&self.after[after.span.start..after.span.end],
self.inserted(after),
indent_at(self.source, actual.span.start),
self.source,
);
@@ -1308,6 +1424,9 @@ impl SyntaxUpdate<'_> {
.iter()
.filter(|(key, field)| {
!actual.contains_key(*key)
&& self.insertion.is_none_or(|syntax| {
!syntax.omitted_fields.contains(&field.value.span.start)
})
&& before
.and_then(|fields| fields.get(*key))
.is_none_or(|old| !self.same(&old.value, &field.value))
@@ -1326,11 +1445,7 @@ impl SyntaxUpdate<'_> {
.unwrap_or_else(|| format!("{} ", indent_at(self.source, span.start)));
let mut text = String::new();
for (key, field) in added {
let value = inserted_value(
&self.after[field.value.span.start..field.value.span.end],
&indent,
self.source,
);
let value = inserted_value(self.inserted(&field.value), &indent, self.source);
if multiline {
text.push_str(&indent);
text.push_str(&format!("{key} {value};{}", newline(self.source)));
@@ -1450,11 +1565,7 @@ impl SyntaxUpdate<'_> {
} else {
text.push(' ');
}
text.push_str(&inserted_value(
&self.after[next.span.start..next.span.end],
&indent,
self.source,
));
text.push_str(&inserted_value(self.inserted(next), &indent, self.source));
suffix = "";
}
if i + 1 < after.len() {
@@ -11,7 +11,10 @@ pub(super) fn upgrade(
.as_ref()
.expect("an explicit source migration report");
let project = &current.decoded.project;
let canonical: BTreeMap<_, _> = language::format_declarations(project).into_iter().collect();
let canonical: BTreeMap<_, _> =
language::format_declarations(project, language::DeclarationFormat::CompleteForEdit)
.into_iter()
.collect();
let changed: BTreeSet<_> = report
.semantic_defaults
.iter()
@@ -59,6 +62,7 @@ pub(super) fn upgrade(
source,
before: source,
after,
insertion: None,
old_aliases: &aliases,
new_aliases: &aliases,
legacy_value_paths: true,