Skip to main content

moddef_core/
resolve.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Import resolution (spec §19). Import URIs follow the package form
4//! `moddef:<namespace>:<name>:<version>` (e.g. `moddef:stdlib:measurands:1.0.0`),
5//! resolved against package roots as
6//! `<root>/<name>/<version>/<name>.moddef.{yaml,json,binary}` — the layout of
7//! moddef/stdlib and the Go resolver's MODDEF_PACKAGE_ROOTS.
8
9use std::collections::BTreeMap;
10use std::path::PathBuf;
11
12use crate::document::{parse_document, DocumentFormat, ParseError};
13use crate::schema::{EnumType, MeasurandDefinition, ModDefDocument};
14
15/// Supplies the bytes for an import uri. Implement over any store (fs, http,
16/// embedded); [`DirResolver`] covers the standard directory layout.
17pub trait PackageResolver {
18    fn fetch(&self, uri: &str) -> Result<(Vec<u8>, DocumentFormat), ParseError>;
19}
20
21/// Resolver over `MODDEF_PACKAGE_ROOTS`-style directories.
22pub struct DirResolver {
23    roots: Vec<PathBuf>,
24}
25
26impl DirResolver {
27    pub fn new<I, P>(roots: I) -> Self
28    where
29        I: IntoIterator<Item = P>,
30        P: Into<PathBuf>,
31    {
32        DirResolver {
33            roots: roots.into_iter().map(Into::into).collect(),
34        }
35    }
36
37    /// Roots from the `MODDEF_PACKAGE_ROOTS` environment variable (`:`-separated).
38    pub fn from_env() -> Self {
39        let roots = std::env::var("MODDEF_PACKAGE_ROOTS").unwrap_or_default();
40        DirResolver::new(roots.split(':').filter(|s| !s.is_empty()))
41    }
42}
43
44impl PackageResolver for DirResolver {
45    fn fetch(&self, uri: &str) -> Result<(Vec<u8>, DocumentFormat), ParseError> {
46        let parts: Vec<&str> = uri.split(':').collect();
47        let [scheme, _ns, name, version] = parts[..] else {
48            return Err(ParseError::new(format!("unsupported import uri: {uri}")));
49        };
50        if scheme != "moddef" {
51            return Err(ParseError::new(format!("unsupported import uri: {uri}")));
52        }
53        let candidates = [
54            (format!("{name}.moddef.yaml"), DocumentFormat::Yaml),
55            (format!("{name}.moddef.json"), DocumentFormat::Json),
56            (format!("{name}.moddef"), DocumentFormat::Binary),
57        ];
58        for root in &self.roots {
59            for (file, format) in &candidates {
60                let path = root.join(name).join(version).join(file);
61                if let Ok(data) = std::fs::read(&path) {
62                    return Ok((data, *format));
63                }
64            }
65        }
66        Err(ParseError::new(format!(
67            "import not found under package roots: {uri}"
68        )))
69    }
70}
71
72/// A document's visible symbol tables after import resolution: local symbols
73/// first, then imported ones (alias-prefixed as `<alias>:<id>`, never
74/// overriding an existing entry).
75pub struct ResolvedDocument {
76    pub enums: BTreeMap<String, EnumType>,
77    pub measurands: BTreeMap<String, MeasurandDefinition>,
78    /// Imported documents keyed by uri.
79    pub imports: BTreeMap<String, ModDefDocument>,
80}
81
82/// Resolve a document's imports and build the visible symbol tables.
83pub fn resolve_imports(
84    doc: &ModDefDocument,
85    resolver: Option<&dyn PackageResolver>,
86) -> Result<ResolvedDocument, ParseError> {
87    let mut out = ResolvedDocument {
88        enums: BTreeMap::new(),
89        measurands: BTreeMap::new(),
90        imports: BTreeMap::new(),
91    };
92
93    for e in &doc.enums {
94        out.enums.insert(e.type_id.clone(), e.clone());
95    }
96    for m in &doc.measurands {
97        out.measurands.insert(m.measurand_id.clone(), m.clone());
98    }
99
100    for imp in &doc.imports {
101        let Some(resolver) = resolver else {
102            return Err(ParseError::new(format!(
103                "document imports {} but no resolver was supplied",
104                imp.uri
105            )));
106        };
107        let (data, format) = resolver.fetch(&imp.uri)?;
108        let idoc = parse_document(&data, format)?;
109        let prefix = if imp.alias.is_empty() {
110            String::new()
111        } else {
112            format!("{}:", imp.alias)
113        };
114        for e in &idoc.enums {
115            out.enums
116                .entry(format!("{prefix}{}", e.type_id))
117                .or_insert_with(|| e.clone());
118        }
119        for m in &idoc.measurands {
120            out.measurands
121                .entry(format!("{prefix}{}", m.measurand_id))
122                .or_insert_with(|| m.clone());
123        }
124        out.imports.insert(imp.uri.clone(), idoc);
125    }
126    Ok(out)
127}