use super::pool::{Pool, PoolLink, PoolPackageInput}; use indexmap::IndexSet; use std::collections::VecDeque; /// Builder for constructing a Pool from package metadata. /// /// The builder accepts package inputs and recursively discovers /// transitive dependencies. This is done by the registry layer /// before solving. pub struct PoolBuilder { /// Packages to add to the pool. inputs: Vec, /// Names already added (to avoid duplicates). added: IndexSet, /// Queue of package names that need to be explored. pending_names: VecDeque, /// Package names that have already been explored (returned by next_pending). explored_names: IndexSet, /// Specific platform packages to ignore (from `--ignore-platform-req=name`). ignore_platform_reqs: IndexSet, /// When true, ignore every platform package (php, ext-*, lib-*, composer-*). /// Mirrors `--ignore-platform-reqs` (no value). ignore_all_platform_reqs: bool, } impl PoolBuilder { pub fn new() -> Self { PoolBuilder { inputs: Vec::new(), added: IndexSet::new(), pending_names: VecDeque::new(), explored_names: IndexSet::new(), ignore_platform_reqs: IndexSet::new(), ignore_all_platform_reqs: false, } } /// Set platform requirements to ignore during exploration. pub fn set_ignore_platform_reqs(&mut self, names: IndexSet) { self.ignore_platform_reqs = names; } /// When set, every platform package is skipped during exploration. pub fn set_ignore_all_platform_reqs(&mut self, ignore_all: bool) { self.ignore_all_platform_reqs = ignore_all; } fn is_ignored_platform_dep(&self, name: &str) -> bool { if self .ignore_platform_reqs .iter() .any(|p| crate::matches_wildcard(name, p)) { return true; } self.ignore_all_platform_reqs && crate::platform::is_platform_package(name) } /// Add a package version to the builder. Returns true if it's new. pub fn add_package(&mut self, input: PoolPackageInput) -> bool { let key = format!("{}@{}", input.name, input.version); if self.added.contains(&key) { return false; } self.added.insert(key); // Queue dependency names for exploration for link in &input.requires { if !self.is_ignored_platform_dep(&link.target) { self.pending_names.push_back(link.target.clone()); } } self.inputs.push(input); true } /// Get the next package name that needs to be explored. /// The caller should fetch available versions for this package /// and add them via `add_package`. pub fn next_pending(&mut self) -> Option { while let Some(name) = self.pending_names.pop_front() { // Skip if already explored or already has versions in inputs if self.explored_names.contains(&name) { continue; } if self.inputs.iter().any(|p| p.name == name) { continue; } self.explored_names.insert(name.clone()); return Some(name); } None } /// Check if there are more names to explore. pub fn has_pending(&self) -> bool { !self.pending_names.is_empty() } /// Build the final Pool. pub fn build(self) -> Pool { Pool::new(self.inputs, vec![]) } /// Get the number of packages added so far. pub fn len(&self) -> usize { self.inputs.len() } /// Read-only access to package inputs collected so far. Used by the /// registry layer to materialize root aliases (`require: "X as Y"`) once /// every base + branch-alias entry is in place: a second pass scans for /// matching `(name, version)` and pushes the alias entry on top. pub fn inputs(&self) -> &[PoolPackageInput] { &self.inputs } /// Whether the builder has no packages. pub fn is_empty(&self) -> bool { self.inputs.is_empty() } } impl Default for PoolBuilder { fn default() -> Self { Self::new() } } /// Helper to convert (name, constraint) pairs from Packagist into PoolLinks. /// /// `source_version` is the normalized version of the package declaring these /// links; it replaces any `"self.version"` constraint, mirroring Composer's /// `ArrayLoader::createLink` (and `AliasPackage::replaceSelfVersionDependencies`, /// which feeds the alias's own version in for the same purpose). pub fn make_pool_links( source: &str, source_version: &str, deps: &[(String, String)], ) -> Vec { deps.iter() .map(|(target, constraint)| PoolLink { target: target.clone(), constraint: if constraint.trim() == "self.version" { source_version.to_string() } else { constraint.clone() }, source: source.to_string(), }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_pool_builder_basic() { let mut builder = PoolBuilder::new(); builder.add_package(PoolPackageInput { name: "a/a".to_string(), version: "1.0.0.0".to_string(), pretty_version: "1.0.0".to_string(), requires: vec![PoolLink { target: "b/b".to_string(), constraint: "^1.0".to_string(), source: "a/a".to_string(), }], replaces: vec![], provides: vec![], conflicts: vec![], is_fixed: false, is_alias_of: None, }); // Should have b/b pending let pending = builder.next_pending(); assert_eq!(pending, Some("b/b".to_string())); builder.add_package(PoolPackageInput { name: "b/b".to_string(), version: "1.0.0.0".to_string(), pretty_version: "1.0.0".to_string(), requires: vec![], replaces: vec![], provides: vec![], conflicts: vec![], is_fixed: false, is_alias_of: None, }); // No more pending assert!(builder.next_pending().is_none()); let pool = builder.build(); assert_eq!(pool.len(), 2); } #[test] fn test_deduplication() { let mut builder = PoolBuilder::new(); let input = PoolPackageInput { name: "a/a".to_string(), version: "1.0.0.0".to_string(), pretty_version: "1.0.0".to_string(), requires: vec![], replaces: vec![], provides: vec![], conflicts: vec![], is_fixed: false, is_alias_of: None, }; assert!(builder.add_package(input.clone())); assert!(!builder.add_package(input)); assert_eq!(builder.len(), 1); } }