Skip to main content

runmat_package/manifest/
model.rs

1use super::{DependencyGroup, DependencySpec, HostCapability, TargetPredicate};
2use crate::{CanonicalPackageId, ManifestError, PackageVersion, RegistryId};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, BTreeSet};
5use url::Url;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct RegistryDeclaration {
9    pub name: RegistryId,
10    pub index: String,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SourceReplacement {
15    pub source: RegistryId,
16    pub replace_with: RegistryId,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct PublicationDeclaration {
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub registry: Option<RegistryId>,
23    pub include: BTreeSet<String>,
24    pub exclude: BTreeSet<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub license: Option<String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub readme: Option<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct PackageManifest {
33    pub local_name: String,
34    pub singleton: bool,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub canonical_id: Option<CanonicalPackageId>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub version: Option<PackageVersion>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub runmat_requirement: Option<semver::VersionReq>,
41    pub dependencies: Vec<DependencySpec>,
42    pub features: BTreeMap<String, BTreeSet<String>>,
43    pub required_capabilities: BTreeSet<HostCapability>,
44    pub optional_capabilities: BTreeSet<HostCapability>,
45    pub registries: Vec<RegistryDeclaration>,
46    pub source_replacements: Vec<SourceReplacement>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub publication: Option<PublicationDeclaration>,
49}
50
51impl TryFrom<&runmat_config::project::ProjectManifest> for PackageManifest {
52    type Error = ManifestError;
53
54    fn try_from(manifest: &runmat_config::project::ProjectManifest) -> Result<Self, Self::Error> {
55        let canonical_id = match manifest.package.organization.as_deref() {
56            Some(organization) => Some(
57                CanonicalPackageId::new(
58                    manifest
59                        .package
60                        .registry
61                        .as_deref()
62                        .unwrap_or("default")
63                        .parse::<RegistryId>()
64                        .map_err(|error| ManifestError::InvalidPackage(error.to_string()))?,
65                    organization,
66                    &manifest.package.name,
67                )
68                .map_err(|error| ManifestError::InvalidPackage(error.to_string()))?,
69            ),
70            None if manifest.package.registry.is_some() => {
71                return Err(ManifestError::InvalidPackage(
72                    "`registry` requires `organization`".to_string(),
73                ));
74            }
75            None => None,
76        };
77        let version = manifest
78            .package
79            .version
80            .as_deref()
81            .map(str::parse::<PackageVersion>)
82            .transpose()
83            .map_err(|error| ManifestError::InvalidPackage(error.to_string()))?;
84        let runmat_requirement = manifest
85            .package
86            .runmat_version
87            .as_deref()
88            .map(semver::VersionReq::parse)
89            .transpose()
90            .map_err(|error| ManifestError::InvalidPackage(error.to_string()))?;
91
92        let mut dependencies = Vec::new();
93        append_dependencies(
94            &mut dependencies,
95            "dependencies",
96            &manifest.dependencies,
97            DependencyGroup::Runtime,
98            None,
99        )?;
100        append_dependencies(
101            &mut dependencies,
102            "dev-dependencies",
103            &manifest.dev_dependencies,
104            DependencyGroup::Development,
105            None,
106        )?;
107        append_dependencies(
108            &mut dependencies,
109            "test-dependencies",
110            &manifest.test_dependencies,
111            DependencyGroup::Test,
112            None,
113        )?;
114        for (target, groups) in &manifest.targets {
115            let predicate: TargetPredicate = target.parse()?;
116            append_dependencies(
117                &mut dependencies,
118                &format!("target.{target}.dependencies"),
119                &groups.dependencies,
120                DependencyGroup::Runtime,
121                Some(predicate.clone()),
122            )?;
123            append_dependencies(
124                &mut dependencies,
125                &format!("target.{target}.dev-dependencies"),
126                &groups.dev_dependencies,
127                DependencyGroup::Development,
128                Some(predicate.clone()),
129            )?;
130            append_dependencies(
131                &mut dependencies,
132                &format!("target.{target}.test-dependencies"),
133                &groups.test_dependencies,
134                DependencyGroup::Test,
135                Some(predicate),
136            )?;
137        }
138
139        let features = manifest
140            .features
141            .iter()
142            .map(|(feature, requests)| {
143                if feature.trim().is_empty() {
144                    return Err(ManifestError::InvalidFeature {
145                        feature: feature.clone(),
146                        reason: "name must be non-empty".to_string(),
147                    });
148                }
149                Ok((
150                    feature.clone(),
151                    requests.iter().map(|request| request.to_string()).collect(),
152                ))
153            })
154            .collect::<Result<_, _>>()?;
155        let required_capabilities = parse_capabilities(&manifest.capabilities.required)?;
156        let optional_capabilities = parse_capabilities(&manifest.capabilities.optional)?;
157        let registries = manifest
158            .registries
159            .iter()
160            .map(|(name, registry)| {
161                validate_public_url(name, &registry.index)?;
162                Ok(RegistryDeclaration {
163                    name: name.parse::<RegistryId>().map_err(|error| {
164                        ManifestError::InvalidRegistry {
165                            registry: name.clone(),
166                            reason: error.to_string(),
167                        }
168                    })?,
169                    index: registry.index.clone(),
170                })
171            })
172            .collect::<Result<_, ManifestError>>()?;
173        let source_replacements = manifest
174            .source_replacements
175            .iter()
176            .map(|(source, replacement)| {
177                Ok(SourceReplacement {
178                    source: source.parse::<RegistryId>().map_err(|error| {
179                        ManifestError::InvalidSourceReplacement {
180                            registry: source.clone(),
181                            reason: error.to_string(),
182                        }
183                    })?,
184                    replace_with: replacement.replace_with.parse::<RegistryId>().map_err(
185                        |error| ManifestError::InvalidSourceReplacement {
186                            registry: source.clone(),
187                            reason: error.to_string(),
188                        },
189                    )?,
190                })
191            })
192            .collect::<Result<_, ManifestError>>()?;
193        let publication = manifest
194            .publish
195            .as_ref()
196            .map(|publication| {
197                let registry = publication
198                    .registry
199                    .as_deref()
200                    .map(str::parse::<RegistryId>)
201                    .transpose()
202                    .map_err(|error| ManifestError::InvalidPackage(error.to_string()))?;
203                let readme = publication
204                    .readme
205                    .as_deref()
206                    .map(|path| {
207                        path.to_str().map(str::to_string).ok_or_else(|| {
208                            ManifestError::InvalidPackage(
209                                "publication readme path must be valid UTF-8".to_string(),
210                            )
211                        })
212                    })
213                    .transpose()?;
214                Ok(PublicationDeclaration {
215                    registry,
216                    include: publication.include.iter().cloned().collect(),
217                    exclude: publication.exclude.iter().cloned().collect(),
218                    license: publication.license.clone(),
219                    readme,
220                })
221            })
222            .transpose()?;
223        Ok(Self {
224            local_name: manifest.package.name.clone(),
225            singleton: manifest.package.singleton,
226            canonical_id,
227            version,
228            runmat_requirement,
229            dependencies,
230            features,
231            required_capabilities,
232            optional_capabilities,
233            registries,
234            source_replacements,
235            publication,
236        })
237    }
238}
239
240fn append_dependencies(
241    output: &mut Vec<DependencySpec>,
242    table: &str,
243    dependencies: &BTreeMap<String, runmat_config::project::ProjectDependency>,
244    group: DependencyGroup,
245    target: Option<TargetPredicate>,
246) -> Result<(), ManifestError> {
247    for (alias, dependency) in dependencies {
248        output.push(DependencySpec::from_config(
249            table,
250            alias,
251            dependency,
252            group,
253            target.clone(),
254        )?);
255    }
256    Ok(())
257}
258
259fn parse_capabilities(values: &[String]) -> Result<BTreeSet<HostCapability>, ManifestError> {
260    values.iter().map(|value| value.parse()).collect()
261}
262
263fn validate_public_url(registry: &str, value: &str) -> Result<(), ManifestError> {
264    let url = Url::parse(value).map_err(|error| ManifestError::InvalidRegistry {
265        registry: registry.to_string(),
266        reason: error.to_string(),
267    })?;
268    if url.scheme() != "https"
269        || !url.username().is_empty()
270        || url.password().is_some()
271        || url.query().is_some()
272        || url.fragment().is_some()
273    {
274        return Err(ManifestError::InvalidRegistry {
275            registry: registry.to_string(),
276            reason: "index must be a credential-free HTTPS URL without query or fragment"
277                .to_string(),
278        });
279    }
280    Ok(())
281}