Migrate frozen data contracts and authored source to schema 4

This commit is contained in:
2026-09-06 03:39:02 +03:00
parent c5276b1943
commit 1230ab8a3e
21 changed files with 2006 additions and 126 deletions
+92 -13
View File
@@ -1,4 +1,4 @@
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2 migration.
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3 migration.
#[path = "migration_v2/source.rs"]
mod v2_source;
use crate::*;
@@ -340,6 +340,8 @@ fn reference_field(key: &str) -> bool {
| "referenced"
| "local"
| "field"
| "path"
| "supertype"
| "sender"
| "receiver"
| "action"
@@ -378,10 +380,14 @@ fn enum_field(key: &str) -> bool {
| "message_kind"
| "transition_kind"
| "side"
| "mechanism"
| "storage"
| "transmission"
| "acquisition"
)
}
/// Parse V3, or explicitly migrate a valid V1/V2 source candidate. Parsing never
/// Parse V4, or explicitly migrate a valid V1/V2/V3 source candidate. Parsing never
/// adopts a model and never overwrites an editor draft.
pub fn parse(source: &str) -> Result<Project, Vec<Diagnostic>> {
parse_limited(source, ParseLimits::UNLIMITED)
@@ -546,7 +552,7 @@ fn offset_for(source: &str, line: usize, column: usize) -> usize {
/// nested records are ordinary language fields; JSON documents are not embedded.
pub fn format(project: &Project) -> String {
let mut out = std::format!(
"arcadia 3;\nproject {} {} id \"{}\" {{\n description {};\n\n",
"arcadia 4;\nproject {} {} id \"{}\" {{\n description {};\n\n",
project.alias,
quote(&project.name),
project.id,
@@ -1337,12 +1343,12 @@ impl Parser<'_> {
.map(|(key, span)| ((d.id, key.clone()), span.start)),
);
}
let project =
let decoded =
resolve_declarations(header.project, &unit.declarations, &aliases, self.limits)
.map_err(|(_, error)| error)?;
Ok(ParsedDocument {
project,
migration: None,
project: decoded.project,
migration: decoded.migration,
locations,
fields,
})
@@ -1350,9 +1356,11 @@ impl Parser<'_> {
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
let mut header = if entry {
self.take("arcadia")?;
if self.text() != "3" {
return Err(self.error("Unsupported schema version: expected 'arcadia 3;'."));
if !matches!(self.text(), "3" | "4") {
return Err(self
.error("Unsupported schema version: expected 'arcadia 3;' or 'arcadia 4;'."));
}
let schema_version = if self.text() == "3" { 3 } else { 4 };
self.cursor += 1;
self.take(";")?;
let start = self.current().span.start;
@@ -1365,7 +1373,7 @@ impl Parser<'_> {
self.take("{")?;
Some(SourceHeader {
project: Project {
schema_version: SCHEMA_VERSION,
schema_version,
id,
alias,
name,
@@ -1540,7 +1548,9 @@ pub(crate) fn resolve_declarations(
declarations: &[Declaration],
aliases: &BTreeMap<String, Id>,
limits: ParseLimits,
) -> Result<Project, (usize, Error)> {
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
let legacy = project.schema_version == 3;
let mut legacy_elements = serde_json::Map::new();
let mut nodes = Vec::new();
for (index, declaration) in declarations.iter().enumerate() {
(|| -> Result<(), Error> {
@@ -1561,6 +1571,9 @@ pub(crate) fn resolve_declarations(
};
match d.kind {
DeclKind::Element(kind, layer) => {
if legacy && !crate::persistence::v3::element_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) {
return Err(conversion_error("Element constructor was not part of schema3.".into()));
}
let description =
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
let explicit_parent = d.fields.contains_key("parent");
@@ -1601,9 +1614,17 @@ pub(crate) fn resolve_declarations(
*viewpoint = ScenarioViewpoint::MixedLegacy;
}
let mut body = serde_json::to_value(initial).unwrap();
let body_fields =
merge_fields(payload(&body), fields).map_err(&conversion_error)?;
let defaults = if legacy {
crate::persistence::v3::legacy_defaults(kind)
.and_then(|value| value.as_object().cloned())
.unwrap_or_else(|| payload(&body))
} else { payload(&body) };
let body_fields = merge_fields(defaults, fields).map_err(&conversion_error)?;
replace_payload(&mut body, body_fields);
if legacy {
legacy_elements.insert(d.id.to_string(), serde_json::json!({"id":d.id,"alias":d.alias,"name":d.name,"description":description,"parent":parent,"body":body}));
return Ok(());
}
let body: ElementBody = serde_json::from_value(body)
.map_err(|e| conversion_error(e.to_string()))?;
if body.kind() != kind || body.layer() != layer {
@@ -1624,6 +1645,9 @@ pub(crate) fn resolve_declarations(
);
}
DeclKind::Relation(kind) => {
if legacy && !crate::persistence::v3::relation_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) {
return Err(conversion_error("Relation constructor was not part of schema3.".into()));
}
let description =
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
let required: &[&str] = if kind == RelationKind::FunctionalExchange {
@@ -1725,6 +1749,58 @@ pub(crate) fn resolve_declarations(
.map_err(|error| (index, error))?;
target.nodes.insert(node.id, node);
}
if legacy {
let mut value = serde_json::to_value(&project).expect("project serialization");
value["elements"] = Value::Object(legacy_elements);
let migrated = serde_json::from_value(value)
.map_err(crate::persistence::FileError::Json)
.and_then(|old| crate::persistence::v3::migrate(old, limits))
.map_err(|error| {
let issue = if let crate::persistence::FileError::Model { issues, .. } = &error {
issues.first()
} else {
None
};
let index = issue
.and_then(|issue| issue.element)
.and_then(|id| declarations.iter().position(|d| d.id == id))
.unwrap_or(usize::MAX);
let offset = declarations.get(index).map_or(0, |d| {
issue
.and_then(|issue| issue.field.as_ref())
.and_then(|field| d.field_spans.get(field))
.map_or(d.offset, |span| span.start)
});
(
index,
Error {
code: if matches!(error, crate::persistence::FileError::Limit(_)) {
"model_limit"
} else {
"migration_error"
},
offset,
message: error.to_string(),
},
)
})?;
for (index, d) in declarations.iter().enumerate() {
if let DeclKind::Element(kind, layer) = d.kind {
let body = &migrated.project.elements[&d.id].body;
if body.kind() != kind || body.layer() != layer {
return Err((
index,
Error {
code: "invalid_source",
offset: d.offset,
message: "Declaration kind disagrees with its typed body.".into(),
},
));
}
}
}
return Ok(migrated);
}
limits.check_model(&project).map_err(|message| {
(
usize::MAX,
@@ -1735,7 +1811,10 @@ pub(crate) fn resolve_declarations(
},
)
})?;
Ok(project)
Ok(crate::persistence::DecodeResult {
project,
migration: None,
})
}
fn header(fields: &mut Map<String, Value>, d: &Declaration, named: bool) {