Migrate frozen data contracts and authored source to schema 4
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Schema 3 to 4 reader
|
||||
|
||||
Schema 3 is a real compatibility input. Its shapes were frozen at `5ce3eef`.
|
||||
The small DTOs in `mod.rs` own the changed data payloads and old message values;
|
||||
closed element/relation constructor lists prevent new vocabulary from entering
|
||||
the old reader. Unchanged ports, diagram placements/routes, artifacts and traces
|
||||
reuse their typed records. The intermediate JSON bridge rejects duplicate keys.
|
||||
|
||||
Nature, bounded types, field defaults/direction/storage, and single supertypes
|
||||
receive explicit documented defaults. Interface lists become identified ordered
|
||||
allocations with UUID v5 namespace equal to the project UUID and name
|
||||
`v2-interface-item:<interface-uuid>:<item-uuid>`. The name remains the same for
|
||||
V2 and V3 inputs so the migration route cannot change the generated identity.
|
||||
Existing IDs never move; deterministic collisions fail with a located diagnostic.
|
||||
Aliases use the normal deterministic collision suffix. Unique legacy message
|
||||
paths are qualified; ambiguous annotations retain their expression and warning.
|
||||
|
||||
V1 first normalizes to the frozen V2 DTO. V2 port/endpoint normalization emits
|
||||
an explicit schema3 candidate and then runs this reader before current decoding.
|
||||
Generated object, record and reference budgets cover the combined stages.
|
||||
|
||||
`language::resolve_declarations` shares lexical/AST parsing, including nesting,
|
||||
but chooses frozen source3 payload defaults and rejects new constructors. Both
|
||||
single-file parsing and `source_project::compile_bundle` take this explicit path.
|
||||
The bundle compiler first stabilizes omitted IDs, then performs targeted token
|
||||
edits in `source_project/source_migration.rs`. Original files, includes, lexical
|
||||
ownership and comments survive. New allocation declarations stay in the source
|
||||
interface's module. The returned source and index describe schema4 bytes; loading
|
||||
does not rewrite original disk files. Canonical output and second-save stability
|
||||
are schema4. No whole-project formatting fallback handles authored source3.
|
||||
|
||||
Frozen source/JSON fixtures live under `tests/fixtures/v3-data-contract.*`; the
|
||||
original V2 golden files remain unchanged.
|
||||
@@ -0,0 +1,630 @@
|
||||
//! Explicit schema-3 compatibility. Only changed payloads are frozen here;
|
||||
//! unchanged typed records are shared after a closed old-constructor check.
|
||||
use crate::language::ParseLimits;
|
||||
use crate::persistence::{DecodeResult, FileError, MigrationReport};
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Project3 {
|
||||
pub schema_version: u32,
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub elements: BTreeMap<Id, Element3>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub relations: BTreeMap<Id, Relation3>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub diagrams: BTreeMap<Id, Diagram>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub artifact_links: BTreeMap<Id, ArtifactLink>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub traces: BTreeMap<Id, Trace>,
|
||||
}
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Element3 {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parent: Option<Id>,
|
||||
#[serde(deserialize_with = "strict_value")]
|
||||
pub body: Value,
|
||||
}
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Relation3 {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(deserialize_with = "strict_value")]
|
||||
pub body: Value,
|
||||
}
|
||||
|
||||
// A Value bridge must retain the strict duplicate-field behavior of typed DTOs.
|
||||
fn strict_value<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Value, D::Error> {
|
||||
struct Strict;
|
||||
impl<'de> serde::de::Visitor<'de> for Strict {
|
||||
type Value = Value;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("a value with unique object fields")
|
||||
}
|
||||
fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Value, E> {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(Value::Number)
|
||||
.ok_or_else(|| E::custom("Non-finite number"))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn visit_none<E: serde::de::Error>(self) -> Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<Value, A::Error> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(v) = a.next_element::<StrictValue>()? {
|
||||
out.push(v.0);
|
||||
}
|
||||
Ok(Value::Array(out))
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut a: A) -> Result<Value, A::Error> {
|
||||
let mut out = serde_json::Map::new();
|
||||
while let Some((k, v)) = a.next_entry::<String, StrictValue>()? {
|
||||
if out.insert(k.clone(), v.0).is_some() {
|
||||
return Err(serde::de::Error::custom(format!("Duplicate field '{k}'")));
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
}
|
||||
}
|
||||
d.deserialize_any(Strict)
|
||||
}
|
||||
struct StrictValue(Value);
|
||||
impl<'de> Deserialize<'de> for StrictValue {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
strict_value(d).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum ItemKind3 {
|
||||
Information,
|
||||
Material,
|
||||
Energy,
|
||||
Event,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum Definition3 {
|
||||
Boolean,
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration { literals: Vec<String> },
|
||||
Record,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Item3 {
|
||||
kind: ItemKind3,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Type3 {
|
||||
definition: Definition3,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Field3 {
|
||||
type_ref: TypeRef,
|
||||
minimum: u32,
|
||||
maximum: Option<u32>,
|
||||
unit: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Interface3 {
|
||||
items: Vec<Id>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct MessageValue3 {
|
||||
field: Id,
|
||||
expression: String,
|
||||
}
|
||||
|
||||
pub(crate) fn relation_allowed(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"Allocation"
|
||||
| "PortAllocation"
|
||||
| "Interaction"
|
||||
| "CapabilityInvolvement"
|
||||
| "ChainInvolvement"
|
||||
| "RoleRealization"
|
||||
| "RoleAssignment"
|
||||
| "FunctionalExchange"
|
||||
| "BoundaryExchange"
|
||||
| "MissionCapability"
|
||||
| "CapabilityInclude"
|
||||
| "CapabilityExtend"
|
||||
| "CapabilityGeneralization"
|
||||
| "MissionInvolvement"
|
||||
| "SequenceLink"
|
||||
| "StateTransition"
|
||||
| "Availability"
|
||||
| "ExchangeAllocation"
|
||||
| "InterfaceProvision"
|
||||
| "InterfaceRequirement"
|
||||
| "StateAction"
|
||||
)
|
||||
}
|
||||
pub(crate) fn element_allowed(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"Entity"
|
||||
| "Actor"
|
||||
| "Role"
|
||||
| "Activity"
|
||||
| "Capability"
|
||||
| "OperationalMission"
|
||||
| "Process"
|
||||
| "System"
|
||||
| "SystemActor"
|
||||
| "Function"
|
||||
| "SystemCapability"
|
||||
| "Mission"
|
||||
| "FunctionalChain"
|
||||
| "ExchangeItem"
|
||||
| "DataType"
|
||||
| "DataField"
|
||||
| "Interface"
|
||||
| "Port"
|
||||
| "Scenario"
|
||||
| "Lifeline"
|
||||
| "Occurrence"
|
||||
| "StateMachine"
|
||||
| "Region"
|
||||
| "State"
|
||||
| "Mode"
|
||||
| "Initial"
|
||||
| "Final"
|
||||
| "Choice"
|
||||
| "Junction"
|
||||
| "Fork"
|
||||
| "Join"
|
||||
| "ShallowHistory"
|
||||
| "DeepHistory"
|
||||
| "EntryPoint"
|
||||
| "ExitPoint"
|
||||
| "Terminate"
|
||||
| "Event"
|
||||
| "Constraint"
|
||||
)
|
||||
}
|
||||
pub(crate) fn legacy_defaults(kind: Kind) -> Option<Value> {
|
||||
Some(match kind {
|
||||
Kind::ExchangeItem => json!({"kind":"Information"}),
|
||||
Kind::DataType => json!({"definition":"String"}),
|
||||
Kind::DataField => json!({"type_ref":"String","minimum":1,"maximum":1,"unit":""}),
|
||||
Kind::Interface => json!({"items":[]}),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn at(id: Id, field: &str, code: &str, message: impl Into<String>) -> FileError {
|
||||
let message = message.into();
|
||||
FileError::Model {
|
||||
message: format!("V3 {code} at {id}.{field}: {message}"),
|
||||
issues: vec![validation::Issue {
|
||||
severity: validation::Severity::Error,
|
||||
element: Some(id),
|
||||
code: code.into(),
|
||||
field: Some(field.into()),
|
||||
message,
|
||||
}],
|
||||
}
|
||||
}
|
||||
fn singleton(value: &Value) -> Option<(&str, &Value)> {
|
||||
let object = value.as_object()?;
|
||||
(object.len() == 1)
|
||||
.then(|| object.iter().next().unwrap())
|
||||
.map(|(k, v)| (k.as_str(), v))
|
||||
}
|
||||
fn old_payload<T: serde::de::DeserializeOwned>(
|
||||
id: Id,
|
||||
field: &str,
|
||||
value: Value,
|
||||
) -> Result<T, FileError> {
|
||||
serde_json::from_value(value).map_err(|error| at(id, field, "legacy_shape", error.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn migrate(old: Project3, limits: ParseLimits) -> Result<DecodeResult, FileError> {
|
||||
if old.schema_version != 3 {
|
||||
return Err(FileError::Invalid("Expected schema_version 3.".into()));
|
||||
}
|
||||
let graph = old.elements.len().saturating_add(old.relations.len());
|
||||
let records = graph
|
||||
.saturating_add(old.diagrams.len())
|
||||
.saturating_add(old.diagrams.values().map(|d| d.nodes.len()).sum::<usize>())
|
||||
.saturating_add(old.traces.len())
|
||||
.saturating_add(old.artifact_links.len())
|
||||
.saturating_add(1);
|
||||
limits
|
||||
.check_counts(graph, records, 0)
|
||||
.map_err(FileError::Limit)?;
|
||||
let mut report = MigrationReport::data_upgrade(3);
|
||||
let mut value = serde_json::to_value(&old)?;
|
||||
let mut allocations = Vec::new();
|
||||
for (key, e) in &old.elements {
|
||||
if *key != e.id {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"id",
|
||||
"identity",
|
||||
"Element map key differs from its identity.",
|
||||
));
|
||||
}
|
||||
let mut body = e.body.clone();
|
||||
let (layer, inner) = singleton(&body)
|
||||
.filter(|(name, _)| matches!(*name, "Oa" | "Sa"))
|
||||
.ok_or_else(|| {
|
||||
at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Expected one OA or SA constructor.",
|
||||
)
|
||||
})?;
|
||||
let layer = layer.to_owned();
|
||||
let outer = inner
|
||||
.as_str()
|
||||
.or_else(|| singleton(inner).map(|(name, _)| name));
|
||||
if !outer.is_some_and(|name| {
|
||||
if layer == "Oa" {
|
||||
matches!(
|
||||
name,
|
||||
"Entity"
|
||||
| "Actor"
|
||||
| "Role"
|
||||
| "Activity"
|
||||
| "Capability"
|
||||
| "Mission"
|
||||
| "Process"
|
||||
| "Shared"
|
||||
)
|
||||
} else {
|
||||
matches!(
|
||||
name,
|
||||
"System"
|
||||
| "Actor"
|
||||
| "Function"
|
||||
| "Capability"
|
||||
| "Mission"
|
||||
| "FunctionalChain"
|
||||
| "Shared"
|
||||
)
|
||||
}
|
||||
}) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Unknown schema3 layer constructor.",
|
||||
));
|
||||
}
|
||||
if let Some(("Shared", shared)) = singleton(inner) {
|
||||
let name = shared
|
||||
.as_str()
|
||||
.or_else(|| singleton(shared).map(|(name, _)| name))
|
||||
.ok_or_else(|| {
|
||||
at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Expected one schema3 shared constructor.",
|
||||
)
|
||||
})?
|
||||
.to_owned();
|
||||
if !matches!(
|
||||
name.as_str(),
|
||||
"ExchangeItem"
|
||||
| "DataType"
|
||||
| "DataField"
|
||||
| "Interface"
|
||||
| "Port"
|
||||
| "Scenario"
|
||||
| "Lifeline"
|
||||
| "Occurrence"
|
||||
| "StateMachine"
|
||||
| "Event"
|
||||
| "Region"
|
||||
| "State"
|
||||
| "Constraint"
|
||||
) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
format!("Unknown schema3 shared constructor '{name}'."),
|
||||
));
|
||||
}
|
||||
let payload = shared.get(&name).cloned().unwrap_or(Value::Null);
|
||||
let changed = match name.as_str() {
|
||||
"ExchangeItem" => {
|
||||
let old: Item3 = old_payload(e.id, "body.kind", payload)?;
|
||||
let (kind, mechanism) = match old.kind {
|
||||
ItemKind3::Information => ("Information", "Unspecified"),
|
||||
ItemKind3::Material => ("Material", "Unspecified"),
|
||||
ItemKind3::Energy => ("Energy", "Unspecified"),
|
||||
ItemKind3::Event => ("Information", "Event"),
|
||||
};
|
||||
Some(json!({"kind":kind,"mechanism":mechanism,"supertype":null}))
|
||||
}
|
||||
"DataType" => {
|
||||
let old: Type3 = old_payload(e.id, "body.definition", payload)?;
|
||||
let definition = match old.definition {
|
||||
Definition3::Boolean => json!("Boolean"),
|
||||
Definition3::Integer => json!({"Integer":{"minimum":null,"maximum":null}}),
|
||||
Definition3::Real => json!({"Real":{"minimum":null,"maximum":null}}),
|
||||
Definition3::String => {
|
||||
json!({"String":{"minimum_length":0,"maximum_length":null}})
|
||||
}
|
||||
Definition3::Enumeration { literals } => {
|
||||
json!({"Enumeration":{"literals":literals}})
|
||||
}
|
||||
Definition3::Record => json!({"Record":{"supertype":null}}),
|
||||
};
|
||||
Some(json!({"definition":definition,"default":null,"unit":""}))
|
||||
}
|
||||
"DataField" => {
|
||||
let old: Field3 = old_payload(e.id, "body", payload)?;
|
||||
Some(
|
||||
json!({"type_ref":old.type_ref,"minimum":old.minimum,"maximum":old.maximum,"unit":old.unit,"direction":"Unspecified","storage":"Value","default":null}),
|
||||
)
|
||||
}
|
||||
"Interface" => {
|
||||
let old: Interface3 = old_payload(e.id, "items", payload)?;
|
||||
let mut seen = BTreeSet::new();
|
||||
for (order, item) in old.items.into_iter().enumerate() {
|
||||
if !seen.insert(item) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"items",
|
||||
"duplicate_reference",
|
||||
"Interface items must be unique.",
|
||||
));
|
||||
}
|
||||
allocations.push((e.id, item, order));
|
||||
}
|
||||
Some(json!({"supertype":null}))
|
||||
}
|
||||
"Occurrence" => {
|
||||
let mut payload = payload;
|
||||
if let Some(values) = payload.pointer_mut("/body/Exchange/values") {
|
||||
let old: Vec<MessageValue3> =
|
||||
old_payload(e.id, "body.values", values.clone())?;
|
||||
*values=Value::Array(old.into_iter().map(|v|json!({"field":v.field,"expression":v.expression,"path":[]})).collect());
|
||||
}
|
||||
Some(payload)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(changed) = changed {
|
||||
let description = match name.as_str() {
|
||||
"ExchangeItem" if changed["mechanism"] == "Event" => {
|
||||
"Legacy Event nature becomes Information with Event mechanism; supertype is absent."
|
||||
}
|
||||
"ExchangeItem" => {
|
||||
"Nature is retained; mechanism is Unspecified and supertype is absent."
|
||||
}
|
||||
"DataType" => {
|
||||
"Existing type is retained with unbounded numeric/string limits, no scalar default or unit, and no Record supertype."
|
||||
}
|
||||
"DataField" => {
|
||||
"Type, ownership, multiplicity and unit are retained; direction is Unspecified, storage is Value and default is absent."
|
||||
}
|
||||
"Interface" => {
|
||||
"Items become identified ordered allocations with Unspecified protocols; supertype is absent."
|
||||
}
|
||||
_ => {
|
||||
"Value expressions are retained; unique enclosing field paths are resolved and ambiguous legacy paths remain unresolved."
|
||||
}
|
||||
};
|
||||
body[&layer]["Shared"][&name] = changed;
|
||||
report
|
||||
.semantic_defaults
|
||||
.push(crate::persistence::SemanticDefault {
|
||||
object: e.id,
|
||||
field: "body".into(),
|
||||
description: description.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Current deserialization checks the reused, unchanged outer/typed parts.
|
||||
let _: ElementBody = serde_json::from_value(body.clone())
|
||||
.map_err(|error| at(e.id, "body", "legacy_shape", error.to_string()))?;
|
||||
value["elements"][key.to_string()]["body"] = body;
|
||||
}
|
||||
for (key, r) in &old.relations {
|
||||
if *key != r.id {
|
||||
return Err(at(
|
||||
r.id,
|
||||
"id",
|
||||
"identity",
|
||||
"Relation map key differs from its identity.",
|
||||
));
|
||||
}
|
||||
if !singleton(&r.body).is_some_and(|(name, _)| relation_allowed(name)) {
|
||||
return Err(at(
|
||||
r.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Relation constructor was not part of schema3.",
|
||||
));
|
||||
}
|
||||
}
|
||||
value["schema_version"] = SCHEMA_VERSION.into();
|
||||
let mut project: Project = serde_json::from_value(value)?;
|
||||
let mut references = project
|
||||
.elements
|
||||
.values()
|
||||
.map(|e| e.steps().map_or(0, Vec::len))
|
||||
.sum::<usize>()
|
||||
.saturating_add(allocations.len().saturating_mul(2));
|
||||
limits
|
||||
.check_counts(
|
||||
project
|
||||
.elements
|
||||
.len()
|
||||
.saturating_add(project.relations.len())
|
||||
.saturating_add(allocations.len()),
|
||||
project.identities().len().saturating_add(allocations.len()),
|
||||
references,
|
||||
)
|
||||
.map_err(FileError::Limit)?;
|
||||
let mut ids: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||
for (source, target, order) in allocations {
|
||||
let id = Id::new_v5(
|
||||
&project.id,
|
||||
format!("v2-interface-item:{source}:{target}").as_bytes(),
|
||||
);
|
||||
if !ids.insert(id) {
|
||||
return Err(at(
|
||||
source,
|
||||
"items",
|
||||
"identity_collision",
|
||||
format!(
|
||||
"Deterministic interface allocation {id} already belongs to another object."
|
||||
),
|
||||
));
|
||||
}
|
||||
let name = format!("{} · {}", project.named(source), project.named(target));
|
||||
let alias = project.fresh_alias(
|
||||
&format!(
|
||||
"{}_{}_allocation",
|
||||
project
|
||||
.elements
|
||||
.get(&source)
|
||||
.map_or("interface", |e| e.alias.as_str()),
|
||||
project
|
||||
.elements
|
||||
.get(&target)
|
||||
.map_or("item", |e| e.alias.as_str())
|
||||
),
|
||||
"interface_item_allocation",
|
||||
id,
|
||||
);
|
||||
project.relations.insert(
|
||||
id,
|
||||
Relation {
|
||||
id,
|
||||
alias: alias.clone(),
|
||||
name,
|
||||
description: String::new(),
|
||||
body: RelationBody::InterfaceItemAllocation {
|
||||
source,
|
||||
target,
|
||||
order: order.try_into().map_err(|_| {
|
||||
at(source, "items", "model_limit", "Too many interface items.")
|
||||
})?,
|
||||
transmission: TransmissionProtocol::Unspecified,
|
||||
acquisition: AcquisitionProtocol::Unspecified,
|
||||
},
|
||||
},
|
||||
);
|
||||
report.generated_interface_allocations.push(
|
||||
crate::persistence::GeneratedInterfaceAllocation {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
alias,
|
||||
order: order as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
let annotations: Vec<_> = project
|
||||
.elements
|
||||
.values()
|
||||
.filter_map(|e| match e.shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
values,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) => Some((e.id, *exchange, values.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (owner, exchange, values) in annotations {
|
||||
let Some(relation) = project.relations.get(&exchange) else {
|
||||
continue;
|
||||
};
|
||||
for (index, value) in values.into_iter().enumerate() {
|
||||
let resolution =
|
||||
crate::data::resolve_legacy_field_path(&project, relation, value.field)
|
||||
.map_err(|message| at(owner, "body.values", "model_limit", message))?;
|
||||
if let crate::data::LegacyFieldPathResolution::Unique(path) = resolution {
|
||||
if !path.is_empty() {
|
||||
references = references.saturating_add(path.len());
|
||||
limits
|
||||
.check_counts(
|
||||
project
|
||||
.elements
|
||||
.len()
|
||||
.saturating_add(project.relations.len()),
|
||||
project.identities().len(),
|
||||
references,
|
||||
)
|
||||
.map_err(FileError::Limit)?;
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project
|
||||
.elements
|
||||
.get_mut(&owner)
|
||||
.and_then(Element::shared_mut)
|
||||
{
|
||||
values[index].path = path;
|
||||
}
|
||||
}
|
||||
} else if matches!(
|
||||
resolution,
|
||||
crate::data::LegacyFieldPathResolution::Ambiguous
|
||||
) {
|
||||
report.unresolved_message_values.push(crate::persistence::UnresolvedMessageValue { occurrence:owner,field:value.field,reason:"Several carried paths reach this legacy field; choose an explicit argument path.".into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
limits.check_model(&project).map_err(FileError::Limit)?;
|
||||
super::validate_project(&project)?;
|
||||
Ok(DecodeResult {
|
||||
project,
|
||||
migration: Some(report),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user