Skip to content

Commit 7cffa6c

Browse files
committed
Add transformer module for manifest transformation logic
- Introduced `transformer.rs` with a structured approach for parsing and applying transformation rules. - Added support for operations like `add`, `default`, `delete`, `drop`, `edit`, `emit`, and `set` on attributes, files, directories, and other targets. - Implemented regex-based matching for patterns and backreference handling in transformations. - Enhanced manifest modification functionality, including attribute/facet operations and deferred action emission. - Added comprehensive unit tests to validate transformation rules and their applications.
1 parent 88e06b4 commit 7cffa6c

File tree

6 files changed

+1257
-7
lines changed

6 files changed

+1257
-7
lines changed

libips/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ pub mod fmri;
1010
pub mod image;
1111
pub mod payload;
1212
pub mod repository;
13+
pub mod publisher;
14+
pub mod transformer;
1315
pub mod solver;
1416
mod test_json_manifest;
1517

18+
#[cfg(test)]
19+
mod publisher_tests;
20+
1621
#[cfg(test)]
1722
mod tests {
1823

libips/src/publisher.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// This Source Code Form is subject to the terms of
2+
// the Mozilla Public License, v. 2.0. If a copy of the
3+
// MPL was not distributed with this file, You can
4+
// obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
use std::path::{Path, PathBuf};
7+
use std::fs;
8+
9+
use miette::Diagnostic;
10+
use thiserror::Error;
11+
12+
use crate::actions::{File as FileAction, Manifest, Transform as TransformAction};
13+
use crate::repository::{ReadableRepository, RepositoryError, WritableRepository};
14+
use crate::repository::file_backend::{FileBackend, Transaction};
15+
use crate::transformer;
16+
17+
/// Error type for high-level publishing operations
18+
#[derive(Debug, Error, Diagnostic)]
19+
pub enum PublisherError {
20+
#[error(transparent)]
21+
#[diagnostic(transparent)]
22+
Repository(#[from] RepositoryError),
23+
24+
#[error(transparent)]
25+
#[diagnostic(transparent)]
26+
Transform(#[from] transformer::TransformError),
27+
28+
#[error("I/O error: {0}")]
29+
#[diagnostic(code(ips::publisher_error::io), help("Check the path and permissions"))]
30+
Io(String),
31+
32+
#[error("invalid root path: {0}")]
33+
#[diagnostic(code(ips::publisher_error::invalid_root_path), help("Ensure the directory exists and is readable"))]
34+
InvalidRoot(String),
35+
}
36+
37+
pub type Result<T> = std::result::Result<T, PublisherError>;
38+
39+
/// High-level Publisher client that keeps a repository handle and an open transaction.
40+
///
41+
/// This is intended to simplify software build/publish flows: instantiate once with a
42+
/// repository path and publisher, then build/transform manifests and publish.
43+
pub struct PublisherClient {
44+
backend: FileBackend,
45+
publisher: String,
46+
tx: Option<Transaction>,
47+
transform_rules: Vec<transformer::TransformRule>,
48+
}
49+
50+
impl PublisherClient {
51+
/// Open an existing repository located at `path` with a selected `publisher`.
52+
pub fn open<P: AsRef<Path>>(path: P, publisher: impl Into<String>) -> Result<Self> {
53+
let backend = FileBackend::open(path)?;
54+
Ok(Self { backend, publisher: publisher.into(), tx: None, transform_rules: Vec::new() })
55+
}
56+
57+
/// Open a transaction if not already open and return whether a new transaction was created.
58+
pub fn open_transaction(&mut self) -> Result<bool> {
59+
if self.tx.is_none() {
60+
let tx = self.backend.begin_transaction()?;
61+
self.tx = Some(tx);
62+
return Ok(true);
63+
}
64+
Ok(false)
65+
}
66+
67+
/// Build a new Manifest from a directory tree. Paths in the manifest are relative to `root`.
68+
pub fn build_manifest_from_dir(&mut self, root: &Path) -> Result<Manifest> {
69+
if !root.exists() {
70+
return Err(PublisherError::InvalidRoot(root.display().to_string()));
71+
}
72+
let mut manifest = Manifest::new();
73+
let root = root.canonicalize().map_err(|_| PublisherError::InvalidRoot(root.display().to_string()))?;
74+
75+
let walker = walkdir::WalkDir::new(&root).into_iter().filter_map(|e| e.ok());
76+
// Ensure a transaction is open
77+
if self.tx.is_none() {
78+
self.open_transaction()?;
79+
}
80+
let tx = self.tx.as_mut().expect("transaction must be open");
81+
82+
for entry in walker {
83+
let p = entry.path();
84+
if p.is_file() {
85+
// Create a File action from the absolute path
86+
let mut f = FileAction::read_from_path(p).map_err(RepositoryError::from)?;
87+
// Set path to be relative to root
88+
let rel: PathBuf = p
89+
.strip_prefix(&root)
90+
.map_err(RepositoryError::from)?
91+
.to_path_buf();
92+
f.path = rel.to_string_lossy().to_string();
93+
// Add into manifest and stage via transaction
94+
manifest.add_file(f.clone());
95+
tx.add_file(f, p)?;
96+
}
97+
}
98+
Ok(manifest)
99+
}
100+
101+
/// Make a new empty manifest
102+
pub fn new_empty_manifest(&self) -> Manifest {
103+
Manifest::new()
104+
}
105+
106+
/// Transform a manifest with a user-supplied rule function
107+
pub fn transform_manifest<F>(&self, mut manifest: Manifest, rule: F) -> Manifest
108+
where
109+
F: FnOnce(&mut Manifest),
110+
{
111+
rule(&mut manifest);
112+
manifest
113+
}
114+
115+
/// Add a single AST transform rule
116+
pub fn add_transform_rule(&mut self, rule: transformer::TransformRule) {
117+
self.transform_rules.push(rule);
118+
}
119+
120+
/// Add multiple AST transform rules
121+
pub fn add_transform_rules(&mut self, rules: Vec<transformer::TransformRule>) {
122+
self.transform_rules.extend(rules);
123+
}
124+
125+
/// Clear all configured transform rules
126+
pub fn clear_transform_rules(&mut self) {
127+
self.transform_rules.clear();
128+
}
129+
130+
/// Load transform rules from raw text (returns number of rules added)
131+
pub fn load_transform_rules_from_text(&mut self, text: &str) -> Result<usize> {
132+
let rules = transformer::parse_rules_ast(text)?;
133+
let n = rules.len();
134+
self.transform_rules.extend(rules);
135+
Ok(n)
136+
}
137+
138+
/// Load transform rules from a file (returns number of rules added)
139+
pub fn load_transform_rules_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<usize> {
140+
let p = path.as_ref();
141+
let content = fs::read_to_string(p).map_err(|e| PublisherError::Io(e.to_string()))?;
142+
self.load_transform_rules_from_text(&content)
143+
}
144+
145+
/// Publish the given manifest. If no transaction is open, one will be opened.
146+
/// The transaction will be updated with the provided manifest and committed.
147+
/// If `rebuild_metadata` is true, repository metadata (catalog/index) will be rebuilt.
148+
pub fn publish(&mut self, mut manifest: Manifest, rebuild_metadata: bool) -> Result<()> {
149+
// Apply configured transform rules (if any)
150+
if !self.transform_rules.is_empty() {
151+
let rules: Vec<TransformAction> = self
152+
.transform_rules
153+
.clone()
154+
.into_iter()
155+
.map(Into::into)
156+
.collect();
157+
transformer::apply(&mut manifest, &rules)?;
158+
}
159+
160+
// Ensure transaction exists
161+
if self.tx.is_none() {
162+
self.open_transaction()?;
163+
}
164+
165+
// Take ownership of the transaction, update and commit
166+
let mut tx = self.tx.take().expect("transaction must be open");
167+
tx.set_publisher(&self.publisher);
168+
tx.update_manifest(manifest);
169+
tx.commit()?;
170+
// Optionally rebuild repo metadata for the publisher
171+
if rebuild_metadata {
172+
self.backend.rebuild(Some(&self.publisher), false, false)?;
173+
}
174+
Ok(())
175+
}
176+
}

libips/src/publisher_tests.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// This Source Code Form is subject to the terms of
2+
// the Mozilla Public License, v. 2.0. If a copy of the
3+
// MPL was not distributed with this file, You can
4+
// obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
#[cfg(test)]
7+
mod tests {
8+
use std::fs;
9+
use std::io::Write;
10+
11+
use tempfile::TempDir;
12+
13+
use crate::publisher::PublisherClient;
14+
use crate::repository::file_backend::FileBackend;
15+
use crate::repository::{RepositoryVersion, WritableRepository};
16+
17+
#[test]
18+
fn publisher_client_basic_flow() {
19+
// Create a temporary repository directory
20+
let tmp = TempDir::new().expect("tempdir");
21+
let repo_path = tmp.path().to_path_buf();
22+
23+
// Initialize repository
24+
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
25+
backend.add_publisher("test").expect("add publisher");
26+
27+
// Prepare a prototype directory with a nested file
28+
let proto_dir = repo_path.join("proto");
29+
let nested = proto_dir.join("nested").join("dir");
30+
fs::create_dir_all(&nested).expect("create proto dirs");
31+
let file_path = nested.join("hello.txt");
32+
let content = b"Hello PublisherClient!";
33+
let mut f = fs::File::create(&file_path).expect("create file");
34+
f.write_all(content).expect("write content");
35+
36+
// Use PublisherClient to publish
37+
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
38+
client.open_transaction().expect("open tx");
39+
let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest");
40+
client.publish(manifest, true).expect("publish");
41+
42+
// Verify the manifest exists at the default path for unknown version
43+
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
44+
assert!(manifest_path.exists(), "manifest not found at {}", manifest_path.display());
45+
46+
// Verify at least one file was stored under publisher/test/file
47+
let file_root = repo_path.join("publisher").join("test").join("file");
48+
assert!(file_root.exists(), "file store root does not exist: {}", file_root.display());
49+
let mut any_file = false;
50+
if let Ok(entries) = fs::read_dir(&file_root) {
51+
for entry in entries.flatten() {
52+
let path = entry.path();
53+
if path.is_dir() {
54+
if let Ok(files) = fs::read_dir(&path) {
55+
for f in files.flatten() {
56+
if f.path().is_file() {
57+
any_file = true;
58+
break;
59+
}
60+
}
61+
}
62+
} else if path.is_file() {
63+
any_file = true;
64+
}
65+
if any_file { break; }
66+
}
67+
}
68+
assert!(any_file, "no stored file found in file store");
69+
}
70+
}
71+
72+
73+
#[cfg(test)]
74+
mod transform_rule_integration_tests {
75+
use crate::actions::Manifest;
76+
use crate::publisher::PublisherClient;
77+
use crate::repository::file_backend::FileBackend;
78+
use crate::repository::{RepositoryVersion, WritableRepository};
79+
use std::fs;
80+
use std::io::Write;
81+
use tempfile::TempDir;
82+
83+
#[test]
84+
fn publisher_client_applies_transform_rules_from_file() {
85+
// Setup repository and publisher
86+
let tmp = TempDir::new().expect("tempdir");
87+
let repo_path = tmp.path().to_path_buf();
88+
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
89+
backend.add_publisher("test").expect("add publisher");
90+
91+
// Prototype directory with a file
92+
let proto_dir = repo_path.join("proto2");
93+
fs::create_dir_all(&proto_dir).expect("mkdir proto2");
94+
let file_path = proto_dir.join("foo.txt");
95+
let mut f = fs::File::create(&file_path).expect("create file");
96+
f.write_all(b"data").expect("write");
97+
98+
// Create a rules file that emits a pkg.summary attribute
99+
let rules_path = repo_path.join("rules.txt");
100+
let rules_text = "<transform file match_type=path pattern=.* operation=emit -> set name=pkg.summary value=\"Added via rules\">\n";
101+
fs::write(&rules_path, rules_text).expect("write rules");
102+
103+
// Use PublisherClient to load rules, build manifest and publish
104+
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
105+
let loaded = client.load_transform_rules_from_file(&rules_path).expect("load rules");
106+
assert!(loaded >= 1, "expected at least one rule loaded");
107+
client.open_transaction().expect("open tx");
108+
let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest");
109+
client.publish(manifest, false).expect("publish");
110+
111+
// Read stored manifest and verify attribute
112+
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
113+
assert!(manifest_path.exists(), "manifest missing: {}", manifest_path.display());
114+
let json = fs::read_to_string(&manifest_path).expect("read manifest");
115+
let parsed: Manifest = serde_json::from_str(&json).expect("parse manifest json");
116+
let has_summary = parsed.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| v == "Added via rules"));
117+
assert!(has_summary, "pkg.summary attribute added via rules not found");
118+
}
119+
}

libips/src/repository/file_backend.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -565,12 +565,17 @@ impl Transaction {
565565
}
566566

567567
// Construct the manifest path using the helper method
568-
let pkg_manifest_path = FileBackend::construct_manifest_path(
569-
&self.repo,
570-
&publisher,
571-
&package_stem,
572-
&package_version,
573-
);
568+
let pkg_manifest_path = if package_version.is_empty() {
569+
// If no version was provided, store as a default manifest file
570+
FileBackend::construct_package_dir(&self.repo, &publisher, &package_stem).join("manifest")
571+
} else {
572+
FileBackend::construct_manifest_path(
573+
&self.repo,
574+
&publisher,
575+
&package_stem,
576+
&package_version,
577+
)
578+
};
574579
debug!("Manifest path: {}", pkg_manifest_path.display());
575580

576581
// Create parent directories if they don't exist

libips/src/repository/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ impl From<bincode::error::EncodeError> for RepositoryError {
204204
}
205205
}
206206
pub mod catalog;
207-
mod file_backend;
207+
pub(crate) mod file_backend;
208208
mod obsoleted;
209209
pub mod progress;
210210
mod rest_backend;

0 commit comments

Comments
 (0)