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
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 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 let pyroduct = Dependency::Simple("*".to_string());
214
215 final_deps.extend(self.dependencies.shared.clone().into_iter());
217 final_deps.insert("pyroduct".to_string(), pyroduct);
218
219 self.augment_deps(&mut final_deps, &self.dependencies.module, false);
221
222 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 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 Dependency::Simple(ver) => Dependency::Detailed(Box::new(DependencyDetail {
256 version: Some(ver.clone()),
257 optional: true,
258 ..Default::default()
259 })),
260 Dependency::Detailed(detail) => {
262 let mut d = detail.clone();
263 d.optional = true;
264 Dependency::Detailed(d)
265 }
266 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 fn create_requisite_features(&self, existing_features: &FeatureSet) -> FeatureSet {
282 let mut new_features = existing_features.clone();
283
284 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 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 let cap_manifest: CapabilityManifest = toml::from_str(input_toml).unwrap();
413
414 let standard_manifest = cap_manifest.to_capability_manifest();
416
417 let deps = &standard_manifest.dependencies;
419
420 match deps.get("tokio").unwrap() {
422 Dependency::Detailed(d) => assert_eq!(d.optional, true),
423 _ => panic!("tokio should be detailed"),
424 }
425
426 match deps.get("serde").unwrap() {
428 Dependency::Detailed(d) => assert_eq!(d.optional, false),
429 _ => panic!("serde should be detailed"),
430 }
431
432 assert!(deps.contains_key("pyroduct"));
434
435 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 assert!(!cap_feat.contains(&"dep:wasm-bindgen".to_string()));
443
444 let output = toml::to_string_pretty(&standard_manifest).unwrap();
446 println!("{}", output);
447 }
448}