From 23815a2aab72f9d80b2e3da6b18554e07e12e98c Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Sun, 31 Aug 2025 00:17:15 +0200 Subject: [PATCH] Expand `ManifestBuilder` with new helper methods for setting attributes, licenses, links, and dependencies. - Added `add_set`, `add_license`, `add_link`, and `add_depend` methods for streamlined manifest construction. - Updated documentation with new examples showcasing the extended `ManifestBuilder` API. - Enhanced test coverage to validate the new helper methods. --- libips/src/api.rs | 116 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/libips/src/api.rs b/libips/src/api.rs index 82c3954..fe31e09 100644 --- a/libips/src/api.rs +++ b/libips/src/api.rs @@ -58,9 +58,9 @@ use walkdir::WalkDir; pub use crate::actions::Manifest; // Core typed manifest -use crate::actions::{Attr, File as FileAction}; -pub use crate::fmri::Fmri; +use crate::actions::{Attr, Dependency as DependAction, File as FileAction, License as LicenseAction, Link as LinkAction, Property}; pub use crate::depend::{FileDep, GenerateOptions as DependGenerateOptions}; +pub use crate::fmri::Fmri; // For BaseMeta use crate::repository::file_backend::{FileBackend, Transaction}; use crate::repository::{ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository}; @@ -123,6 +123,31 @@ pub struct BaseMeta { /// Example (no_run): /// ```no_run /// use libips::api as ips; +/// let mut builder = ips::ManifestBuilder::new(); +/// let fmri = ips::Fmri::parse("pkg://pub/name@1.0").unwrap(); +/// let summary = String::from("A summary"); +/// let classification = "Applications/Other"; +/// let project_url = String::from("https://example.com"); +/// let source_url = String::from("https://example.com/src.tar.gz"); +/// let license_file_name = "license.txt"; +/// let license_name = "MIT"; +/// builder.add_set("pkg.fmri", &fmri.to_string()); +/// builder.add_set("pkg.summary", &summary); +/// builder.add_set( +/// "info.classification", +/// &format!("org.opensolaris.category.2008:{}", classification), +/// ); +/// builder.add_set("info.upstream-url", &project_url); +/// builder.add_set("info.source-url", &source_url); +/// builder.add_license(&license_file_name, &license_name); +/// let manifest = builder.build(); +/// # Ok::<(), ips::IpsError>(()) +/// ``` +/// +/// Another style using with_base_metadata: +/// Example (no_run): +/// ```no_run +/// use libips::api as ips; /// use std::path::Path; /// let proto = Path::new("/proto"); /// let mut manifest = ips::ManifestBuilder::new() @@ -142,6 +167,48 @@ pub struct ManifestBuilder { } impl ManifestBuilder { + /// Add a simple set (attribute) action: set name= value= + /// Returns self for chaining. + pub fn add_set, V: ToString>(&mut self, key: K, value: V) -> &mut Self { + self.manifest.attributes.push(Attr { + key: key.into(), + values: vec![value.to_string()], + properties: Default::default(), + }); + self + } + + /// Add a license action, equivalent to: license path= license= + pub fn add_license(&mut self, path: &str, license_name: &str) -> &mut Self { + let mut props = std::collections::HashMap::new(); + props.insert( + "path".to_string(), + Property { key: "path".to_string(), value: path.to_string() }, + ); + props.insert( + "license".to_string(), + Property { key: "license".to_string(), value: license_name.to_string() }, + ); + self.manifest.licenses.push(LicenseAction { payload: String::new(), properties: props }); + self + } + + /// Add a link action + pub fn add_link(&mut self, path: &str, target: &str) -> &mut Self { + self.manifest.links.push(LinkAction { path: path.to_string(), target: target.to_string(), properties: Default::default() }); + self + } + + /// Add a dependency action with a type and an FMRI string (name or full FMRI). + /// If FMRI parsing fails, the dependency is added without an fmri (will be flagged by lint). + pub fn add_depend(&mut self, dep_type: &str, fmri_str: &str) -> &mut Self { + let fmri = Fmri::parse(fmri_str).ok(); + let mut d = DependAction::default(); + d.dependency_type = dep_type.to_string(); + d.fmri = fmri; + self.manifest.dependencies.push(d); + self + } /// Start a new empty builder pub fn new() -> Self { Self { manifest: Manifest::new() } @@ -560,7 +627,6 @@ pub struct LintConfig { pub mod lint { use super::*; use miette::Diagnostic; - use std::collections::HashSet; use thiserror::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -619,7 +685,7 @@ pub mod lint { if fmri_attr_count > 1 { diags.push(miette::Report::new(LintIssue::DuplicateFmri)); } match (fmri_attr_count, fmri_text) { (0, _) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)), - (_, Some(txt)) => { if Fmri::parse(&txt).is_err() { diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)); } }, + (_, Some(txt)) => { if crate::fmri::Fmri::parse(&txt).is_err() { diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)); } }, (_, None) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)), } diags @@ -666,10 +732,10 @@ pub mod lint { fn rule_enabled(rule_id: &str, cfg: &LintConfig) -> bool { if let Some(only) = &cfg.enabled_only { - let set: HashSet<&str> = only.iter().map(|s| s.as_str()).collect(); + let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect(); return set.contains(rule_id); } - let disabled: HashSet<&str> = cfg.disabled_rules.iter().map(|s| s.as_str()).collect(); + let disabled: std::collections::HashSet<&str> = cfg.disabled_rules.iter().map(|s| s.as_str()).collect(); !disabled.contains(rule_id) } @@ -808,4 +874,42 @@ mod tests { // fmri is valid, dependencies empty, summary rule disabled => no diags assert!(diags.is_empty(), "expected no diagnostics when summary rule disabled, got: {:?}", diags); } + + #[test] + fn builder_add_set_license_link_depend() { + // add_set with Fmri and strings + let fmri = Fmri::parse("pkg://pub/example@1.0").unwrap(); + let mut b = ManifestBuilder::new(); + b.add_set("pkg.fmri", &fmri); + b.add_set("pkg.summary", "Summary"); + b.add_set("info.upstream-url", "https://example.com"); + b.add_license("LICENSE", "MIT"); + b.add_link("usr/bin/foo", "../libexec/foo"); + b.add_depend("require", "pkg://pub/dep@1.2"); + let m = b.build(); + + // Validate attributes include fmri and summary + assert!(m.attributes.iter().any(|a| a.key == "pkg.fmri" && a.values.get(0).map(|v| v == &fmri.to_string()).unwrap_or(false))); + assert!(m.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.get(0).map(|v| v == "Summary").unwrap_or(false))); + + // Validate license + assert_eq!(m.licenses.len(), 1); + let lic = &m.licenses[0]; + assert_eq!(lic.properties.get("path").map(|p| p.value.as_str()), Some("LICENSE")); + assert_eq!(lic.properties.get("license").map(|p| p.value.as_str()), Some("MIT")); + + // Validate link + assert_eq!(m.links.len(), 1); + let ln = &m.links[0]; + assert_eq!(ln.path, "usr/bin/foo"); + assert_eq!(ln.target, "../libexec/foo"); + + // Validate dependency + assert_eq!(m.dependencies.len(), 1); + let dep = &m.dependencies[0]; + assert_eq!(dep.dependency_type, "require"); + let df = dep.fmri.as_ref().expect("dep fmri parsed"); + assert_eq!(df.publisher.as_deref(), Some("pub")); + assert_eq!(df.version.as_ref().unwrap().to_string(), "1.2"); + } }