1pub use cargo_toml::Dependency;
4use cargo_toml::{
5 Badges, DependencyDetail, DepsSet, Edition, FeatureSet, Inheritable, InheritedDependencyDetail,
6 LintGroups, Manifest, Package, PatchSet, Product, Profiles, TargetDepsSet, Workspace,
7};
8use serde::{Deserialize, Serialize};
9use std::{collections::BTreeMap, path::Path};
10use toml::Value;
11
12use crate::artifacts::{CapabilityConfig, PlaybookIdent};
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, PartialOrd, Ord, Hash)]
15pub struct CapabilityIdent {
16 pub author: String,
17 pub package: String,
18 pub version: String,
19}
20
21impl CapabilityIdent {
22 pub fn to_package(self) -> Package<Value> {
23 let mut package = Package::new(self.package, self.version);
24 package.authors = Inheritable::Set(vec![self.author]);
25 package.edition = Inheritable::Set(Edition::E2024);
26 package
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(untagged)]
32pub enum ProjectManifest {
33 Capability(CapabilityManifest),
34 Module(ModuleManifest),
35}
36
37impl ProjectManifest {
38 pub fn ident(&self) -> &CapabilityIdent {
39 match self {
40 ProjectManifest::Capability(c) => &c.capability,
41 ProjectManifest::Module(m) => &m.module,
42 }
43 }
44
45 pub fn to_cargo_manifest(self, cache_manager: Option<&crate::cache::CacheManager>) -> Manifest {
46 match self {
47 ProjectManifest::Capability(c) => c.to_capability_manifest(),
48 ProjectManifest::Module(m) => m.to_cargo(cache_manager),
49 }
50 }
51
52 pub fn to_interface_manifest(self) -> Option<Manifest> {
53 match self {
54 ProjectManifest::Capability(c) => Some(c.to_interface_manifest()),
55 ProjectManifest::Module(_) => None,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub struct CapabilityManifest<Metadata = Value> {
63 pub capability: CapabilityIdent,
64 pub workspace: Option<Workspace<Metadata>>,
65 #[serde(default = "default_pyroduct")]
66 pub pyroduct: Dependency,
67 #[serde(default)]
68 pub dependencies: CapabilityDependencies,
69 #[serde(default)]
70 pub dev_dependencies: DepsSet,
71 #[serde(default)]
72 pub build_dependencies: DepsSet,
73 #[serde(default)]
74 pub target: TargetDepsSet,
75 #[serde(default)]
76 pub features: FeatureSet,
77 #[serde(default)]
78 #[deprecated(note = "Cargo recommends patch instead")]
79 pub replace: DepsSet,
80 #[serde(default)]
81 pub patch: PatchSet,
82 pub lib: Option<Product>,
83 #[serde(default)]
84 pub profile: Profiles,
85 #[serde(default)]
86 pub badges: Badges,
87 #[serde(default)]
88 pub bin: Vec<Product>,
89 #[serde(default)]
90 pub bench: Vec<Product>,
91 #[serde(default)]
92 pub test: Vec<Product>,
93 #[serde(default)]
94 pub example: Vec<Product>,
95 #[serde(default)]
96 pub lints: Inheritable<LintGroups>,
97}
98
99#[derive(Debug, thiserror::Error)]
100pub enum ManifestError {
101 #[error("Pyroduct does not support inherited versions (yet!)")]
102 InheritedVersionNotSupported,
103 #[error("[capability] section is missing")]
104 CapabilitySectionMissing,
105}
106
107impl std::fmt::Display for CapabilityIdent {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "{}:{}:{}", self.author, self.package, self.version)
110 }
111}
112
113#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
114pub struct ConfiguredCapability {
115 pub author: String,
116 pub package: String,
117 pub version: String,
118 pub configuration: CapabilityConfig,
119}
120
121impl ConfiguredCapability {
122 pub fn ident(&self) -> CapabilityIdent {
123 CapabilityIdent {
124 author: self.author.clone(),
125 package: self.package.clone(),
126 version: self.version.clone(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "kebab-case")]
133pub struct ModuleManifest<Metadata = Value> {
134 pub module: CapabilityIdent,
135 pub workspace: Option<Workspace<Metadata>>,
136 #[serde(default = "default_pyroduct")]
137 pub pyroduct: Dependency,
138 #[serde(default)]
139 pub capabilities: BTreeMap<String, ConfiguredCapability>,
140 #[serde(default)]
141 pub dependencies: DepsSet,
142 #[serde(default)]
143 pub dev_dependencies: DepsSet,
144 #[serde(default)]
145 pub build_dependencies: DepsSet,
146 #[serde(default)]
147 pub target: TargetDepsSet,
148 #[serde(default)]
149 pub features: FeatureSet,
150 #[serde(default)]
151 pub patch: PatchSet,
152 pub lib: Option<Product>,
153 #[serde(default)]
154 pub profile: Profiles,
155 #[serde(default)]
156 pub badges: Badges,
157 #[serde(default)]
158 pub bin: Vec<Product>,
159 #[serde(default)]
160 pub bench: Vec<Product>,
161 #[serde(default)]
162 pub test: Vec<Product>,
163 #[serde(default)]
164 pub example: Vec<Product>,
165 #[serde(default)]
166 pub lints: Inheritable<LintGroups>,
167 #[serde(default)]
168 pub interconnect: BTreeMap<String, PlaybookIdent>,
169}
170
171fn default_pyroduct() -> Dependency {
172 Dependency::Inherited(InheritedDependencyDetail {
173 workspace: true,
174 ..Default::default()
175 })
176}
177
178#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(rename_all = "kebab-case")]
180pub struct CapabilityDependencies {
181 #[serde(default)]
182 pub host: DepsSet,
183 #[serde(default)]
184 pub module: DepsSet,
185 #[serde(default)]
186 pub shared: DepsSet,
187}
188
189impl CapabilityManifest {
190 pub fn to_capability_manifest(self) -> Manifest {
193 let mut final_deps = BTreeMap::new();
194 let mut pyro_dep = self.pyroduct.clone();
195 pyro_dep
196 .detail_mut()
197 .features
198 .push("capability".to_string());
199 final_deps.insert("pyroduct".to_string(), pyro_dep);
200 final_deps.extend(self.dependencies.shared.clone());
201 self.augment_deps(&mut final_deps, &self.dependencies.host, true);
202 self.augment_deps(&mut final_deps, &self.dependencies.module, true);
203 let final_features = self.create_requisite_features(&self.features);
204
205 #[allow(deprecated)]
206 Manifest {
207 package: Some(self.capability.to_package()),
208 workspace: self.workspace,
209 dependencies: final_deps,
210 dev_dependencies: self.dev_dependencies,
211 build_dependencies: self.build_dependencies,
212 target: self.target,
213 features: final_features,
214 patch: self.patch,
215 lib: ensure_cdylib(self.lib),
216 profile: self.profile,
217 badges: self.badges,
218 bin: self.bin,
219 bench: self.bench,
220 test: self.test,
221 example: self.example,
222 lints: self.lints,
223 replace: BTreeMap::default(),
224 }
225 }
226
227 pub fn to_interface_manifest(self) -> Manifest {
228 let mut final_deps = BTreeMap::new();
229
230 let mut pyro_dep = self.pyroduct.clone();
232 pyro_dep.detail_mut().features.push("module".to_string());
233
234 final_deps.extend(self.dependencies.shared.clone());
236 final_deps.insert("pyroduct".to_string(), pyro_dep);
237
238 self.augment_deps(&mut final_deps, &self.dependencies.module, false);
240
241 let final_features = self.features.clone();
244
245 #[allow(deprecated)]
246 Manifest {
247 package: Some(self.capability.to_package()),
248 workspace: self.workspace,
249 dependencies: final_deps,
250 dev_dependencies: self.dev_dependencies,
251 build_dependencies: self.build_dependencies,
252 target: self.target,
253 features: final_features,
254 patch: self.patch,
255 lib: self.lib,
256 profile: self.profile,
257 badges: self.badges,
258 bin: Vec::new(),
259 bench: self.bench,
260 test: self.test,
261 example: self.example,
262 lints: self.lints,
263 replace: BTreeMap::default(),
264 }
265 }
266
267 fn augment_deps(&self, target_map: &mut DepsSet, source_map: &DepsSet, make_optional: bool) {
270 for (name, dep) in source_map {
271 let new_dep = if make_optional {
272 match dep {
273 Dependency::Simple(ver) => Dependency::Detailed(Box::new(DependencyDetail {
275 version: Some(ver.clone()),
276 optional: true,
277 ..Default::default()
278 })),
279 Dependency::Detailed(detail) => {
281 let mut d = detail.clone();
282 d.optional = true;
283 Dependency::Detailed(d)
284 }
285 Dependency::Inherited(inherited) => {
287 let mut d = inherited.clone();
288 d.optional = true;
289 Dependency::Inherited(d)
290 }
291 }
292 } else {
293 dep.clone()
294 };
295 target_map.insert(name.clone(), new_dep);
296 }
297 }
298
299 fn create_requisite_features(&self, existing_features: &FeatureSet) -> FeatureSet {
301 let mut new_features = existing_features.clone();
302
303 let capability_feature: Vec<String> = self
305 .dependencies
306 .host
307 .keys()
308 .map(|name| format!("dep:{}", name))
309 .collect();
310
311 let module_feature: Vec<String> = self
312 .dependencies
313 .module
314 .keys()
315 .map(|name| format!("dep:{}", name))
316 .collect();
317
318 new_features.insert("capability".to_string(), capability_feature);
319 new_features.insert("module".to_string(), module_feature);
320
321 new_features.entry("default".to_string()).or_default();
323
324 new_features
325 }
326}
327
328impl ModuleManifest {
329 pub fn to_cargo(self, cache_manager: Option<&crate::cache::CacheManager>) -> Manifest {
330 let mut final_deps = BTreeMap::new();
331 let mut pyro_dep = self.pyroduct.clone();
332 pyro_dep.detail_mut().features.push("module".to_string());
333 final_deps.insert("pyroduct".to_string(), pyro_dep);
334 final_deps.extend(self.dependencies.clone());
335 self.augment_deps(&mut final_deps, &self.capabilities, cache_manager);
336
337 #[allow(deprecated)]
338 Manifest {
339 package: Some(self.module.to_package()),
340 workspace: self.workspace,
341 dependencies: final_deps,
342 dev_dependencies: self.dev_dependencies,
343 build_dependencies: self.build_dependencies,
344 target: self.target,
345 features: BTreeMap::default(),
346 patch: self.patch,
347 lib: ensure_cdylib(self.lib),
348 profile: self.profile,
349 badges: self.badges,
350 bin: self.bin,
351 bench: self.bench,
352 test: self.test,
353 example: self.example,
354 lints: self.lints,
355 replace: BTreeMap::default(),
356 }
357 }
358
359 fn augment_deps(
360 &self,
361 target_map: &mut DepsSet,
362 capabilities: &BTreeMap<String, ConfiguredCapability>,
363 cache_manager: Option<&crate::cache::CacheManager>,
364 ) {
365 for (name, cap) in capabilities.iter() {
366 let path = if let Some(cm) = cache_manager {
367 cm.interface_dir(&cap.author, &cap.package, &cap.version)
368 .to_string_lossy()
369 .into()
370 } else {
371 Path::new("..")
372 .join(&cap.author)
373 .join(&cap.package)
374 .join(&cap.version)
375 .to_string_lossy()
376 .into()
377 };
378 let dep = Dependency::Detailed(Box::new(DependencyDetail {
379 path: Some(path),
380 ..Default::default()
381 }));
382 target_map.insert(name.clone(), dep);
383 }
384 }
385}
386
387pub fn ensure_cdylib(lib: Option<Product>) -> Option<Product> {
388 let lib = if let Some(mut lib) = lib {
389 if !lib.crate_type.iter().any(|s| s.as_str() == "cdylib") {
390 lib.crate_type.push("cdylib".to_string());
391 lib
392 } else {
393 lib
394 }
395 } else {
396 Product {
397 crate_type: vec!["cdylib".to_string()],
398 ..Default::default()
399 }
400 };
401 Some(lib)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_full_transformation() {
410 let input_toml = r#"
411[capability]
412package = "my-capability"
413version = "0.1.0"
414author = "Me"
415
416[pyroduct]
417path = "../../lib/pyroduct"
418
419[dependencies.host]
420tokio = "1.0"
421uuid = { version = "1.0", features = ["v4"] }
422
423[dependencies.module]
424wasm-bindgen = "0.2"
425
426[dependencies.shared]
427serde = { version = "1.0", features = ["derive"] }
428"#;
429
430 let cap_manifest: CapabilityManifest = toml::from_str(input_toml).unwrap();
432
433 let standard_manifest = cap_manifest.to_capability_manifest();
435
436 let deps = &standard_manifest.dependencies;
438
439 match deps.get("tokio").unwrap() {
441 Dependency::Detailed(d) => assert!(d.optional),
442 _ => panic!("tokio should be detailed"),
443 }
444
445 match deps.get("serde").unwrap() {
447 Dependency::Detailed(d) => assert!(!d.optional),
448 _ => panic!("serde should be detailed"),
449 }
450
451 assert!(deps.contains_key("pyroduct"));
453
454 let features = &standard_manifest.features;
456 let cap_feat = features.get("capability").unwrap();
457
458 assert!(cap_feat.contains(&"dep:tokio".to_string()));
459 assert!(cap_feat.contains(&"dep:uuid".to_string()));
460 assert!(!cap_feat.contains(&"dep:wasm-bindgen".to_string()));
462
463 let output = toml::to_string_pretty(&standard_manifest).unwrap();
465 println!("{}", output);
466 }
467}