Skip to main content

morphir_projection/normalize/
mod.rs

1mod v3;
2mod v4;
3
4use morphir_core::format_version::{Compatibility, ReleaseTriplet, SupportTable};
5use serde_json::Value;
6use thiserror::Error;
7
8use crate::lower_camel;
9use crate::model::ProjectionPackage;
10
11/// Failure to decode the supplied Morphir IR generation.
12#[derive(Debug, Error)]
13pub enum NormalizeError {
14    /// The IR root has no `formatVersion` member.
15    #[error("Morphir IR root is missing formatVersion")]
16    MissingFormatVersion,
17    /// The version has an unsupported JSON scalar type.
18    #[error("formatVersion has invalid scalar type: {value}")]
19    InvalidFormatVersionType {
20        /// Supplied JSON value.
21        value: String,
22    },
23    /// The version string is not a canonical semantic version.
24    #[error("formatVersion has invalid syntax: {value}")]
25    InvalidFormatVersionSyntax {
26        /// Supplied version spelling.
27        value: String,
28    },
29    /// A version component exceeds the supported integer range.
30    #[error("formatVersion component is outside the unsigned 32-bit range: {value}")]
31    FormatVersionOutOfRange {
32        /// Supplied version spelling.
33        value: String,
34    },
35    /// The requested major IR generation is unsupported.
36    #[error("unsupported Morphir IR format major: {major}")]
37    UnsupportedFormatVersionMajor {
38        /// Requested major generation.
39        major: u32,
40    },
41    /// The requested minor revision is outside this reader's support table.
42    #[error("release {major}.{minor}.{patch} is a minor revision this reader does not support")]
43    UnsupportedFormatVersionMinor {
44        /// Requested major component.
45        major: u32,
46        /// Requested minor component.
47        minor: u32,
48        /// Requested patch component.
49        patch: u32,
50    },
51    /// A v4 entry point does not target one canonical public value.
52    #[error("entry point {identifier:?} has {reason} target {target:?}")]
53    InvalidEntryPointTarget {
54        /// Entry-point identifier.
55        identifier: String,
56        /// Supplied target FQName.
57        target: String,
58        /// Stable validation reason.
59        reason: &'static str,
60    },
61    /// More than one entry-point identifier targets the same value.
62    #[error("entry point target {target:?} is declared more than once by {identifiers:?}")]
63    DuplicateEntryPointTarget {
64        /// Duplicate target FQName.
65        target: String,
66        /// Entry-point identifiers sharing the target.
67        identifiers: Vec<String>,
68    },
69    /// The generation-specific Morphir IR decoder rejected the document.
70    #[error("invalid Morphir IR: {0}")]
71    Decode(#[from] serde_json::Error),
72    /// A v3 `Specs` distribution was supplied where a projection needs
73    /// definitions; `Specs` carries no bodies to normalize.
74    #[error("a v3 Specs distribution has no definitions to normalize")]
75    UnsupportedSpecsDistribution,
76}
77
78impl NormalizeError {
79    /// Stable category for callers that need structured failure handling.
80    pub fn code(&self) -> &'static str {
81        match self {
82            Self::MissingFormatVersion => "missing_format_version",
83            Self::InvalidFormatVersionType { .. } => "invalid_format_version_type",
84            Self::InvalidFormatVersionSyntax { .. } => "invalid_format_version_syntax",
85            Self::FormatVersionOutOfRange { .. } => "format_version_out_of_range",
86            Self::UnsupportedFormatVersionMajor { .. } => "unsupported_format_version_major",
87            Self::UnsupportedFormatVersionMinor { .. } => "unsupported_format_version_minor",
88            Self::InvalidEntryPointTarget { .. } => "invalid_entry_point_target",
89            Self::DuplicateEntryPointTarget { .. } => "duplicate_entry_point_target",
90            Self::Decode(_) => "invalid_ir",
91            Self::UnsupportedSpecsDistribution => "unsupported_specs_distribution",
92        }
93    }
94}
95
96/// Normalize a supported Morphir IR distribution to its public, body-free model.
97///
98/// `formatVersion` is recognized and checked for exact-release compatibility
99/// before a generation-specific decoder runs.
100///
101/// # Examples
102///
103/// ```
104/// use morphir_projection::{DistributionKind, normalize};
105///
106/// let ir = serde_json::json!({
107///     "formatVersion": 3,
108///     "distribution": ["Library", [["example"]], [], { "modules": [] }]
109/// });
110/// let package = normalize(&ir)?;
111/// assert_eq!(package.kind, DistributionKind::Library);
112/// # Ok::<(), morphir_projection::NormalizeError>(())
113/// ```
114pub fn normalize(ir: &Value) -> Result<ProjectionPackage, NormalizeError> {
115    match recognize_version(ir)? {
116        SupportedVersion::V3 => {
117            let distribution = serde_json::from_value(with_integer_version(ir, 3))?;
118            v3::normalize(distribution)
119        }
120        SupportedVersion::V4 => {
121            let ir = serde_json::from_value(with_integer_version(ir, 4))?;
122            v4::normalize(ir)
123        }
124    }
125}
126
127fn with_integer_version(ir: &Value, major: u32) -> Value {
128    let mut normalized = ir.clone();
129    normalized["formatVersion"] = Value::from(major);
130    normalized
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134enum SupportedVersion {
135    V3,
136    V4,
137}
138
139fn recognize_version(ir: &Value) -> Result<SupportedVersion, NormalizeError> {
140    let version = ir
141        .as_object()
142        .and_then(|root| root.get("formatVersion"))
143        .ok_or(NormalizeError::MissingFormatVersion)?;
144    let release = match version {
145        Value::Number(number) => {
146            let Some(major) = number.as_u64() else {
147                return Err(NormalizeError::InvalidFormatVersionType {
148                    value: version.to_string(),
149                });
150            };
151            let major =
152                u32::try_from(major).map_err(|_| NormalizeError::FormatVersionOutOfRange {
153                    value: version.to_string(),
154                })?;
155            if major == 0 {
156                return Err(NormalizeError::InvalidFormatVersionSyntax {
157                    value: version.to_string(),
158                });
159            }
160            (major, 0, 0)
161        }
162        Value::String(source) => parse_release(source)?,
163        _ => {
164            return Err(NormalizeError::InvalidFormatVersionType {
165                value: version.to_string(),
166            });
167        }
168    };
169    supported_version(release)
170}
171
172/// Decide support from the declared table, then dispatch on the major family.
173///
174/// `with_integer_version` rewrites `formatVersion` to the major before decoding,
175/// so every supported patch of a supported minor reaches the same decoder as its
176/// baseline.
177fn supported_version(release: (u32, u32, u32)) -> Result<SupportedVersion, NormalizeError> {
178    let (major, minor, patch) = release;
179    let triplet = ReleaseTriplet::new(major, minor, patch);
180    match SupportTable::reference().check(&triplet) {
181        Compatibility::Supported => match major {
182            3 => Ok(SupportedVersion::V3),
183            4 => Ok(SupportedVersion::V4),
184            major => Err(NormalizeError::UnsupportedFormatVersionMajor { major }),
185        },
186        Compatibility::UnsupportedMinor => Err(NormalizeError::UnsupportedFormatVersionMinor {
187            major,
188            minor,
189            patch,
190        }),
191        Compatibility::UnsupportedMajor => {
192            Err(NormalizeError::UnsupportedFormatVersionMajor { major })
193        }
194    }
195}
196
197fn parse_release(source: &str) -> Result<(u32, u32, u32), NormalizeError> {
198    let components = source.split('.').collect::<Vec<_>>();
199    if components.len() != 3
200        || components.iter().any(|component| {
201            component.is_empty()
202                || !component.bytes().all(|byte| byte.is_ascii_digit())
203                || (component.len() > 1 && component.starts_with('0'))
204        })
205    {
206        return Err(NormalizeError::InvalidFormatVersionSyntax {
207            value: source.to_owned(),
208        });
209    }
210    let values = components
211        .into_iter()
212        .map(|component| parse_component(source, component))
213        .collect::<Result<Vec<_>, _>>()?;
214    if values[0] < 3 {
215        return Err(NormalizeError::InvalidFormatVersionSyntax {
216            value: source.to_owned(),
217        });
218    }
219    Ok((values[0], values[1], values[2]))
220}
221
222fn parse_component(source: &str, component: &str) -> Result<u32, NormalizeError> {
223    component.bytes().try_fold(0_u32, |value, byte| {
224        value
225            .checked_mul(10)
226            .and_then(|value| value.checked_add(u32::from(byte - b'0')))
227            .ok_or_else(|| NormalizeError::FormatVersionOutOfRange {
228                value: source.to_owned(),
229            })
230    })
231}
232
233pub(crate) fn canonical_fq_name(package: &str, module: &[String], local: &str) -> String {
234    format!("{package}:{}#{local}", module.join("/"))
235}
236
237pub(crate) fn normalize_signature(
238    mut inputs: Vec<crate::model::NamedType>,
239    mut output: Option<crate::model::TypeExpr>,
240) -> (
241    Vec<crate::model::NamedType>,
242    Option<crate::model::TypeExpr>,
243    crate::model::ValueKind,
244) {
245    let mut next_argument = 1;
246    while let Some(crate::model::TypeExpr::Function {
247        input,
248        output: next_output,
249    }) = output
250    {
251        while inputs
252            .iter()
253            .any(|input| lower_camel(&input.name) == format!("arg{next_argument}"))
254        {
255            next_argument += 1;
256        }
257        inputs.push(crate::model::NamedType {
258            name: format!("arg{next_argument}"),
259            tpe: *input,
260        });
261        next_argument += 1;
262        output = Some(*next_output);
263    }
264    let value_kind = if inputs.is_empty() {
265        crate::model::ValueKind::Constant
266    } else {
267        crate::model::ValueKind::Function
268    };
269    (inputs, output, value_kind)
270}