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, ®istry.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}