Migrate schema 2 ports through frozen readers and expose schema 3 source API
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-09-05 18:23:33 +03:00
parent 0d14438b7b
commit b9f88ce1f3
21 changed files with 7513 additions and 152 deletions
+70 -19
View File
@@ -1,4 +1,6 @@
//! Typed OA/SA language, tolerant source analysis, and explicit V1 migration.
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2 migration.
#[path = "migration_v2/source.rs"]
mod v2_source;
use crate::*;
use serde_json::{Map, Value};
use std::{collections::BTreeMap, fmt};
@@ -317,6 +319,8 @@ fn reference_field(key: &str) -> bool {
| "branches"
| "Named"
| "items"
| "incoming_items"
| "outgoing_items"
| "triggers"
| "effects"
| "interface"
@@ -351,6 +355,8 @@ fn enum_field(key: &str) -> bool {
key,
"kind"
| "direction"
| "orientation"
| "contract"
| "layer"
| "projection"
| "relation"
@@ -364,13 +370,24 @@ fn enum_field(key: &str) -> bool {
)
}
/// Parse V2, or explicitly migrate a valid V1 source candidate. Parsing never
/// Parse V3, or explicitly migrate a valid V1/V2 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)
}
pub fn parse_limited(source: &str, limits: ParseLimits) -> Result<Project, Vec<Diagnostic>> {
parse_with_report_limited(source, limits).map(|decoded| decoded.project)
}
pub fn parse_with_report(
source: &str,
) -> Result<crate::persistence::DecodeResult, Vec<Diagnostic>> {
parse_with_report_limited(source, ParseLimits::UNLIMITED)
}
pub fn parse_with_report_limited(
source: &str,
limits: ParseLimits,
) -> Result<crate::persistence::DecodeResult, Vec<Diagnostic>> {
let parsed = parse_document(source, limits)?;
let errors: Vec<_> = parsed
.diagnostics(source)
@@ -378,7 +395,10 @@ pub fn parse_limited(source: &str, limits: ParseLimits) -> Result<Project, Vec<D
.filter(|d| d.severity == validation::Severity::Error)
.collect();
if errors.is_empty() {
Ok(parsed.project)
Ok(crate::persistence::DecodeResult {
project: parsed.project,
migration: parsed.migration,
})
} else {
Err(errors)
}
@@ -386,6 +406,7 @@ pub fn parse_limited(source: &str, limits: ParseLimits) -> Result<Project, Vec<D
struct ParsedDocument {
project: Project,
migration: Option<crate::persistence::MigrationReport>,
locations: BTreeMap<Id, usize>,
fields: BTreeMap<(Id, String), usize>,
}
@@ -460,22 +481,31 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
})
.collect::<Vec<_>>()
})?;
let project = crate::persistence::migrate_v1_limited(old, limits).map_err(|message| {
vec![diagnostic(
source,
Error {
code: "invalid_source",
offset: 0,
message,
},
)]
})?;
let decoded =
crate::persistence::migrate_v1_with_report_limited(old, limits).map_err(|error| {
vec![diagnostic(
source,
Error {
code: if matches!(error, crate::persistence::FileError::Limit(_)) {
"model_limit"
} else {
"invalid_source"
},
offset: 0,
message: error.to_string(),
},
)]
})?;
return Ok(ParsedDocument {
project,
project: decoded.project,
migration: decoded.migration,
locations: BTreeMap::new(),
fields: BTreeMap::new(),
});
}
if tokens.get(1).is_some_and(|t| t.text(source) == "2") {
return v2_source::parse(source, tokens, limits);
}
Parser {
source,
tokens,
@@ -510,7 +540,7 @@ pub fn format(project: &Project) -> String {
.map(|(id, a)| (id, a.to_owned()))
.collect::<BTreeMap<_, _>>();
let mut out = std::format!(
"arcadia 2;\nproject {} {} id \"{}\" {{\n description {};\n\n",
"arcadia 3;\nproject {} {} id \"{}\" {{\n description {};\n\n",
project.alias,
quote(&project.name),
project.id,
@@ -1000,8 +1030,8 @@ impl Parser<'_> {
}
fn document(&mut self) -> Result<ParsedDocument, Error> {
self.take("arcadia")?;
if self.text() != "2" {
return Err(self.error("Unsupported schema version: expected 'arcadia 2;'."));
if self.text() != "3" {
return Err(self.error("Unsupported schema version: expected 'arcadia 3;'."));
}
self.cursor += 1;
self.take(";")?;
@@ -1116,7 +1146,22 @@ impl Parser<'_> {
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
let parent =
take_optional_id(&mut fields, "parent").map_err(&conversion_error)?;
let mut initial = ElementBody::new(kind, layer).unwrap();
let mut initial = if kind == Kind::Port {
if !fields.contains_key("contract") {
return Err(conversion_error(
"Missing required field 'contract'.".into(),
));
}
ElementBody::shared_in(
layer,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: None,
},
)
} else {
ElementBody::new(kind, layer).unwrap()
};
// Schema-2 sources predate explicit scenario viewpoints.
// Preserve their mixed lifelines instead of reinterpreting
// old source using a new graphical factory's typed default.
@@ -1151,7 +1196,12 @@ impl Parser<'_> {
DeclKind::Relation(kind) => {
let description =
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
for required in ["source", "target"] {
let required: &[&str] = if kind == RelationKind::FunctionalExchange {
&["source", "target", "source_port", "target_port"]
} else {
&["source", "target"]
};
for &required in required {
if !fields.contains_key(required) {
return Err(conversion_error(std::format!(
"Missing required field '{required}'."
@@ -1246,6 +1296,7 @@ impl Parser<'_> {
})?;
Ok(ParsedDocument {
project,
migration: None,
locations,
fields: field_locations,
})