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
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
This commit is contained in:
@@ -1,6 +1,48 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{io::Write, path::Path};
|
||||
use thiserror::Error;
|
||||
#[path = "migration_v2/mod.rs"]
|
||||
pub(crate) mod v2;
|
||||
pub(crate) use v2::adapter::migrate as migrate_v2_limited;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MigrationReport {
|
||||
pub from_schema: u32,
|
||||
pub to_schema: u32,
|
||||
pub preserved_ports: Vec<PreservedPort>,
|
||||
pub generated_endpoints: Vec<GeneratedEndpoint>,
|
||||
pub orientation_defaults: Vec<OrientationDefault>,
|
||||
pub required_resolutions: Vec<Id>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PreservedPort {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub required_resolution: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GeneratedEndpoint {
|
||||
pub exchange: Id,
|
||||
pub side: String,
|
||||
pub port: Id,
|
||||
pub owner: Id,
|
||||
pub alias: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OrientationDefault {
|
||||
pub exchange: Id,
|
||||
pub oriented: bool,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DecodeResult {
|
||||
pub project: Project,
|
||||
pub migration: Option<MigrationReport>,
|
||||
}
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FileError {
|
||||
#[error("File operation failed: {0}")]
|
||||
@@ -11,6 +53,11 @@ pub enum FileError {
|
||||
Text(String),
|
||||
#[error("Invalid project: {0}")]
|
||||
Invalid(String),
|
||||
#[error("Invalid project: {message}")]
|
||||
Model {
|
||||
message: String,
|
||||
issues: Vec<validation::Issue>,
|
||||
},
|
||||
#[error(
|
||||
"This file changed outside Arcadia Studio. Save as a new file, or reopen it to review the external changes."
|
||||
)]
|
||||
@@ -19,21 +66,30 @@ pub enum FileError {
|
||||
Limit(String),
|
||||
}
|
||||
pub fn decode(bytes: &[u8]) -> Result<Project, FileError> {
|
||||
decode_limited(bytes, crate::language::ParseLimits::UNLIMITED)
|
||||
decode_with_report(bytes).map(|decoded| decoded.project)
|
||||
}
|
||||
pub fn decode_with_report(bytes: &[u8]) -> Result<DecodeResult, FileError> {
|
||||
decode_with_report_limited(bytes, crate::language::ParseLimits::UNLIMITED)
|
||||
}
|
||||
|
||||
/// Apply admission budgets before validating a candidate or its migrated form.
|
||||
pub fn decode_limited(
|
||||
bytes: &[u8],
|
||||
limits: crate::language::ParseLimits,
|
||||
) -> Result<Project, FileError> {
|
||||
decode_with_report_limited(bytes, limits).map(|decoded| decoded.project)
|
||||
}
|
||||
/// Admission budgets cover the original graph and all generated migration data.
|
||||
/// Decoding never rewrites a file or advances a hosted revision.
|
||||
pub fn decode_with_report_limited(
|
||||
bytes: &[u8],
|
||||
limits: crate::language::ParseLimits,
|
||||
) -> Result<DecodeResult, FileError> {
|
||||
if bytes.len() > limits.max_source_bytes {
|
||||
return Err(FileError::Limit(format!(
|
||||
"Input exceeds {} bytes.",
|
||||
limits.max_source_bytes
|
||||
)));
|
||||
}
|
||||
let p: Project = if bytes.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{') {
|
||||
let decoded = if bytes.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{') {
|
||||
let header: serde_json::Value = serde_json::from_slice(bytes)?;
|
||||
match header
|
||||
.get("schema_version")
|
||||
@@ -42,43 +98,63 @@ pub fn decode_limited(
|
||||
Some(1) => {
|
||||
let old = serde_json::from_slice(bytes)?;
|
||||
check_v1_limits(&old, limits).map_err(FileError::Limit)?;
|
||||
migrate_v1_limited(old, limits).map_err(FileError::Invalid)?
|
||||
migrate_v1_with_report_limited(old, limits)?
|
||||
}
|
||||
Some(2) => serde_json::from_slice(bytes)?,
|
||||
Some(2) => migrate_v2_limited(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(3) => DecodeResult {
|
||||
project: serde_json::from_slice(bytes)?,
|
||||
migration: None,
|
||||
},
|
||||
_ => {
|
||||
return Err(FileError::Invalid(
|
||||
"Unsupported or missing schema_version; expected 1 or 2.".into(),
|
||||
"Unsupported or missing schema_version; expected 1, 2 or 3.".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let text = std::str::from_utf8(bytes).map_err(|e| FileError::Text(e.to_string()))?;
|
||||
crate::language::parse_limited(text, limits).map_err(|diagnostics| {
|
||||
crate::language::parse_with_report_limited(text, limits).map_err(|diagnostics| {
|
||||
let detail = diagnostics
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if diagnostics.iter().any(|diagnostic| {
|
||||
matches!(diagnostic.code.as_str(), "model_limit" | "source_limit")
|
||||
}) {
|
||||
if diagnostics
|
||||
.iter()
|
||||
.any(|d| matches!(d.code.as_str(), "model_limit" | "source_limit"))
|
||||
{
|
||||
FileError::Limit(detail)
|
||||
} else {
|
||||
FileError::Text(detail)
|
||||
}
|
||||
})?
|
||||
};
|
||||
limits.check_model(&p).map_err(FileError::Limit)?;
|
||||
validate_project(&p)?;
|
||||
Ok(p)
|
||||
limits
|
||||
.check_model(&decoded.project)
|
||||
.map_err(FileError::Limit)?;
|
||||
validate_project(&decoded.project)?;
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
pub(crate) fn migrate_v1_with_report_limited(
|
||||
old: v1::Project,
|
||||
limits: crate::language::ParseLimits,
|
||||
) -> Result<DecodeResult, FileError> {
|
||||
check_v1_limits(&old, limits).map_err(FileError::Limit)?;
|
||||
let normalized = normalize_v1_to_v2(old, limits).map_err(FileError::Invalid)?;
|
||||
let mut decoded = migrate_v2_limited(normalized, limits)?;
|
||||
if let Some(report) = &mut decoded.migration {
|
||||
report.from_schema = 1;
|
||||
}
|
||||
Ok(decoded)
|
||||
}
|
||||
/// Explicit V1 adapter. Old identities, labels, descriptions, endpoints and
|
||||
/// coordinates survive; every newly required identity is deterministic.
|
||||
pub(crate) fn migrate_v1_limited(
|
||||
fn normalize_v1_to_v2(
|
||||
mut old: v1::Project,
|
||||
limits: crate::language::ParseLimits,
|
||||
) -> Result<Project, String> {
|
||||
) -> Result<v2::model::Project, String> {
|
||||
use v2::model::*;
|
||||
check_v1_limits(&old, limits)?;
|
||||
if let Some(issue) = v1::validation::validate(&old).into_iter().next() {
|
||||
return Err(issue.message);
|
||||
@@ -180,7 +256,10 @@ pub(crate) fn migrate_v1_limited(
|
||||
id,
|
||||
alias,
|
||||
element: e.id,
|
||||
position: e.position,
|
||||
position: Position {
|
||||
x: e.position.x,
|
||||
y: e.position.y,
|
||||
},
|
||||
size: Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
@@ -236,9 +315,9 @@ pub(crate) fn migrate_v1_limited(
|
||||
}
|
||||
*p.elements.get_mut(&e.id).unwrap().steps_mut().unwrap() = steps;
|
||||
}
|
||||
if let Some(issue) = validation::validate(&p)
|
||||
if let Some(issue) = v2::validation::validate(&p)
|
||||
.into_iter()
|
||||
.find(|i| i.severity == validation::Severity::Error)
|
||||
.find(|i| i.severity == v2::validation::Severity::Error)
|
||||
{
|
||||
return Err(std::format!(
|
||||
"V1 migration cannot satisfy V2 validation: {}",
|
||||
@@ -263,7 +342,7 @@ pub(crate) fn check_v1_limits(
|
||||
let records = graph.saturating_add(old.elements.len()).saturating_add(2);
|
||||
limits.check_counts(graph, records, steps)
|
||||
}
|
||||
fn migration_id(p: &Project, role: &str) -> Id {
|
||||
fn migration_id(p: &v2::model::Project, role: &str) -> Id {
|
||||
let mut suffix = 0;
|
||||
loop {
|
||||
let id = Id::new_v5(
|
||||
@@ -478,12 +557,18 @@ fn validate_project(p: &Project) -> Result<(), FileError> {
|
||||
.into_iter()
|
||||
.find(|i| i.severity == validation::Severity::Error)
|
||||
{
|
||||
return Err(FileError::Invalid(i.message));
|
||||
return Err(FileError::Model {
|
||||
message: i.message.clone(),
|
||||
issues: vec![i],
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||
decode(&std::fs::read(path)?)
|
||||
load_with_report(path).map(|decoded| decoded.project)
|
||||
}
|
||||
pub fn load_with_report(path: &Path) -> Result<DecodeResult, FileError> {
|
||||
decode_with_report(&std::fs::read(path)?)
|
||||
}
|
||||
/// Serialize to a sibling temporary file, flush, then atomically replace the destination.
|
||||
pub fn save(path: &Path, p: &Project) -> Result<(), FileError> {
|
||||
|
||||
Reference in New Issue
Block a user