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