Migrate frozen data contracts and authored source to schema 4
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user