mod v3;
mod v4;
use morphir_core::format_version::{Compatibility, ReleaseTriplet, SupportTable};
use serde_json::Value;
use thiserror::Error;
use crate::lower_camel;
use crate::model::ProjectionPackage;
#[derive(Debug, Error)]
pub enum NormalizeError {
#[error("Morphir IR root is missing formatVersion")]
MissingFormatVersion,
#[error("formatVersion has invalid scalar type: {value}")]
InvalidFormatVersionType {
value: String,
},
#[error("formatVersion has invalid syntax: {value}")]
InvalidFormatVersionSyntax {
value: String,
},
#[error("formatVersion component is outside the unsigned 32-bit range: {value}")]
FormatVersionOutOfRange {
value: String,
},
#[error("unsupported Morphir IR format major: {major}")]
UnsupportedFormatVersionMajor {
major: u32,
},
#[error("release {major}.{minor}.{patch} is a minor revision this reader does not support")]
UnsupportedFormatVersionMinor {
major: u32,
minor: u32,
patch: u32,
},
#[error("entry point {identifier:?} has {reason} target {target:?}")]
InvalidEntryPointTarget {
identifier: String,
target: String,
reason: &'static str,
},
#[error("entry point target {target:?} is declared more than once by {identifiers:?}")]
DuplicateEntryPointTarget {
target: String,
identifiers: Vec<String>,
},
#[error("invalid Morphir IR: {0}")]
Decode(#[from] serde_json::Error),
#[error("a v3 Specs distribution has no definitions to normalize")]
UnsupportedSpecsDistribution,
}
impl NormalizeError {
pub fn code(&self) -> &'static str {
match self {
Self::MissingFormatVersion => "missing_format_version",
Self::InvalidFormatVersionType { .. } => "invalid_format_version_type",
Self::InvalidFormatVersionSyntax { .. } => "invalid_format_version_syntax",
Self::FormatVersionOutOfRange { .. } => "format_version_out_of_range",
Self::UnsupportedFormatVersionMajor { .. } => "unsupported_format_version_major",
Self::UnsupportedFormatVersionMinor { .. } => "unsupported_format_version_minor",
Self::InvalidEntryPointTarget { .. } => "invalid_entry_point_target",
Self::DuplicateEntryPointTarget { .. } => "duplicate_entry_point_target",
Self::Decode(_) => "invalid_ir",
Self::UnsupportedSpecsDistribution => "unsupported_specs_distribution",
}
}
}
pub fn normalize(ir: &Value) -> Result<ProjectionPackage, NormalizeError> {
match recognize_version(ir)? {
SupportedVersion::V3 => {
let distribution = serde_json::from_value(with_integer_version(ir, 3))?;
v3::normalize(distribution)
}
SupportedVersion::V4 => {
let ir = serde_json::from_value(with_integer_version(ir, 4))?;
v4::normalize(ir)
}
}
}
fn with_integer_version(ir: &Value, major: u32) -> Value {
let mut normalized = ir.clone();
normalized["formatVersion"] = Value::from(major);
normalized
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SupportedVersion {
V3,
V4,
}
fn recognize_version(ir: &Value) -> Result<SupportedVersion, NormalizeError> {
let version = ir
.as_object()
.and_then(|root| root.get("formatVersion"))
.ok_or(NormalizeError::MissingFormatVersion)?;
let release = match version {
Value::Number(number) => {
let Some(major) = number.as_u64() else {
return Err(NormalizeError::InvalidFormatVersionType {
value: version.to_string(),
});
};
let major =
u32::try_from(major).map_err(|_| NormalizeError::FormatVersionOutOfRange {
value: version.to_string(),
})?;
if major == 0 {
return Err(NormalizeError::InvalidFormatVersionSyntax {
value: version.to_string(),
});
}
(major, 0, 0)
}
Value::String(source) => parse_release(source)?,
_ => {
return Err(NormalizeError::InvalidFormatVersionType {
value: version.to_string(),
});
}
};
supported_version(release)
}
fn supported_version(release: (u32, u32, u32)) -> Result<SupportedVersion, NormalizeError> {
let (major, minor, patch) = release;
let triplet = ReleaseTriplet::new(major, minor, patch);
match SupportTable::reference().check(&triplet) {
Compatibility::Supported => match major {
3 => Ok(SupportedVersion::V3),
4 => Ok(SupportedVersion::V4),
major => Err(NormalizeError::UnsupportedFormatVersionMajor { major }),
},
Compatibility::UnsupportedMinor => Err(NormalizeError::UnsupportedFormatVersionMinor {
major,
minor,
patch,
}),
Compatibility::UnsupportedMajor => {
Err(NormalizeError::UnsupportedFormatVersionMajor { major })
}
}
}
fn parse_release(source: &str) -> Result<(u32, u32, u32), NormalizeError> {
let components = source.split('.').collect::<Vec<_>>();
if components.len() != 3
|| components.iter().any(|component| {
component.is_empty()
|| !component.bytes().all(|byte| byte.is_ascii_digit())
|| (component.len() > 1 && component.starts_with('0'))
})
{
return Err(NormalizeError::InvalidFormatVersionSyntax {
value: source.to_owned(),
});
}
let values = components
.into_iter()
.map(|component| parse_component(source, component))
.collect::<Result<Vec<_>, _>>()?;
if values[0] < 3 {
return Err(NormalizeError::InvalidFormatVersionSyntax {
value: source.to_owned(),
});
}
Ok((values[0], values[1], values[2]))
}
fn parse_component(source: &str, component: &str) -> Result<u32, NormalizeError> {
component.bytes().try_fold(0_u32, |value, byte| {
value
.checked_mul(10)
.and_then(|value| value.checked_add(u32::from(byte - b'0')))
.ok_or_else(|| NormalizeError::FormatVersionOutOfRange {
value: source.to_owned(),
})
})
}
pub(crate) fn canonical_fq_name(package: &str, module: &[String], local: &str) -> String {
format!("{package}:{}#{local}", module.join("/"))
}
pub(crate) fn normalize_signature(
mut inputs: Vec<crate::model::NamedType>,
mut output: Option<crate::model::TypeExpr>,
) -> (
Vec<crate::model::NamedType>,
Option<crate::model::TypeExpr>,
crate::model::ValueKind,
) {
let mut next_argument = 1;
while let Some(crate::model::TypeExpr::Function {
input,
output: next_output,
}) = output
{
while inputs
.iter()
.any(|input| lower_camel(&input.name) == format!("arg{next_argument}"))
{
next_argument += 1;
}
inputs.push(crate::model::NamedType {
name: format!("arg{next_argument}"),
tpe: *input,
});
next_argument += 1;
output = Some(*next_output);
}
let value_kind = if inputs.is_empty() {
crate::model::ValueKind::Constant
} else {
crate::model::ValueKind::Function
};
(inputs, output, value_kind)
}