Skip to main content

pyro_artifacts/
cargo.rs

1//! Conversions between our manifest and Cargo's
2
3pub 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    /// Reads from a file string, processes logic, and returns a standard Manifest
191    /// ready for serialization.
192    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        // Use a simple version dependency for pyroduct to allow patching by the builder
231        let mut pyro_dep = self.pyroduct.clone();
232        pyro_dep.detail_mut().features.push("module".to_string());
233
234        // 1. Shared Dependencies (Required)
235        final_deps.extend(self.dependencies.shared.clone());
236        final_deps.insert("pyroduct".to_string(), pyro_dep);
237
238        // 2. Module Dependencies (Required, NOT optional)
239        self.augment_deps(&mut final_deps, &self.dependencies.module, false);
240
241        // 3. Pyroduct
242
243        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    /// Helper: Augments dependencies with `optional = true` if requested
268    /// and inserts them into the final map.
269    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                    // Convert Simple ("1.0") -> Detailed { version = "1.0", optional = true }
274                    Dependency::Simple(ver) => Dependency::Detailed(Box::new(DependencyDetail {
275                        version: Some(ver.clone()),
276                        optional: true,
277                        ..Default::default()
278                    })),
279                    // Update Detailed to ensure optional is true
280                    Dependency::Detailed(detail) => {
281                        let mut d = detail.clone();
282                        d.optional = true;
283                        Dependency::Detailed(d)
284                    }
285                    // Inherited workspace deps also need to become detailed to hold the optional flag
286                    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    /// Helper: Generates the "capability" feature and defaults
300    fn create_requisite_features(&self, existing_features: &FeatureSet) -> FeatureSet {
301        let mut new_features = existing_features.clone();
302
303        // Generate "dep:xxx" entries for all Host dependencies
304        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        // Ensure default and module exist (if not provided in input)
322        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        // 1. Deserialize into Custom Struct
431        let cap_manifest: CapabilityManifest = toml::from_str(input_toml).unwrap();
432
433        // 2. Convert to Standard Manifest (Augment logic runs here)
434        let standard_manifest = cap_manifest.to_capability_manifest();
435
436        // 3. Verify Dependencies
437        let deps = &standard_manifest.dependencies;
438
439        // Check Host (converted to optional)
440        match deps.get("tokio").unwrap() {
441            Dependency::Detailed(d) => assert!(d.optional),
442            _ => panic!("tokio should be detailed"),
443        }
444
445        // Check Shared (remains not optional)
446        match deps.get("serde").unwrap() {
447            Dependency::Detailed(d) => assert!(!d.optional),
448            _ => panic!("serde should be detailed"),
449        }
450
451        // Check Pyroduct added
452        assert!(deps.contains_key("pyroduct"));
453
454        // 4. Verify Features
455        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        // Module deps should NOT be in capability feature
461        assert!(!cap_feat.contains(&"dep:wasm-bindgen".to_string()));
462
463        // 5. Serialize back to String
464        let output = toml::to_string_pretty(&standard_manifest).unwrap();
465        println!("{}", output);
466    }
467}