diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 20:02:46 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 20:02:46 +0900 |
| commit | 0cfb074ba0e02a4a451d5dba1d6546f3c11eb35b (patch) | |
| tree | 2a69c996eaec5d04c9deb8199f9f88a9bf727f86 /crates | |
| parent | 51e97c79cc117f739835dfe124f017d97b0f2c30 (diff) | |
| download | php-shirabe-0cfb074ba0e02a4a451d5dba1d6546f3c11eb35b.tar.gz php-shirabe-0cfb074ba0e02a4a451d5dba1d6546f3c11eb35b.tar.zst php-shirabe-0cfb074ba0e02a4a451d5dba1d6546f3c11eb35b.zip | |
feat(port): port ArrayLoader.php
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 12 | ||||
| -rw-r--r-- | crates/shirabe/src/package/loader/array_loader.rs | 870 |
2 files changed, 882 insertions, 0 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 99ab4f0..b4a9a74 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -870,6 +870,18 @@ pub fn str_starts_with(haystack: &str, needle: &str) -> bool { pub const DATE_ATOM: &str = "Y-m-d\\TH:i:sP"; +pub fn ucfirst(s: &str) -> String { + todo!() +} + +pub fn is_scalar(value: &PhpMixed) -> bool { + todo!() +} + +pub fn strval(value: &PhpMixed) -> String { + todo!() +} + pub fn usleep(microseconds: u64) { todo!() } diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs index 388a30d..c0dc2a2 100644 --- a/crates/shirabe/src/package/loader/array_loader.rs +++ b/crates/shirabe/src/package/loader/array_loader.rs @@ -1 +1,871 @@ //! ref: composer/src/Composer/Package/Loader/ArrayLoader.php + +use anyhow::Result; +use chrono::{DateTime, TimeZone, Utc}; +use indexmap::IndexMap; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_php_shim::{ + is_scalar, is_string, json_encode, ltrim, sprintf, stripos, strpos, strtolower, strval, substr, + trigger_error, trim, ucfirst, Exception, LogicException, PhpMixed, UnexpectedValueException, + E_USER_DEPRECATED, +}; + +use crate::package::base_package::{BasePackage, SUPPORTED_LINK_TYPES}; +use crate::package::complete_alias_package::CompleteAliasPackage; +use crate::package::complete_package::CompletePackage; +use crate::package::complete_package_interface::CompletePackageInterface; +use crate::package::link::Link; +use crate::package::loader::loader_interface::LoaderInterface; +use crate::package::package_interface::PackageInterface; +use crate::package::root_alias_package::RootAliasPackage; +use crate::package::root_package::RootPackage; +use crate::package::version::version_parser::VersionParser; + +#[derive(Debug)] +pub struct ArrayLoader { + /// @var VersionParser + pub(crate) version_parser: VersionParser, + /// @var bool + pub(crate) load_options: bool, +} + +impl ArrayLoader { + pub fn new(parser: Option<VersionParser>, load_options: bool) -> Self { + let parser = match parser { + Some(p) => p, + None => { + // TODO(phase-b): VersionParser has no public `new` yet + todo!("VersionParser::new()") + } + }; + Self { + version_parser: parser, + load_options, + } + } +} + +impl LoaderInterface for ArrayLoader { + /// @inheritDoc + fn load( + &self, + mut config: IndexMap<String, PhpMixed>, + class: Option<String>, + ) -> Result<Box<BasePackage>> { + let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string()); + + if class != "Composer\\Package\\CompletePackage" + && class != "Composer\\Package\\RootPackage" + { + trigger_error( + "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.", + E_USER_DEPRECATED, + ); + } + + let mut package = self.create_object(&config, &class)?; + + for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { + let entry = config.get(*r#type); + let entry_is_array = entry + .map(|v| matches!(v, PhpMixed::Array(_))) + .unwrap_or(false); + if entry.is_none() || !entry_is_array { + continue; + } + let _method = format!("set{}", ucfirst(opts.method)); + let links = self.parse_links( + package.get_name(), + package.get_pretty_version(), + opts.method, + match entry.unwrap() { + PhpMixed::Array(arr) => arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + _ => IndexMap::new(), + }, + )?; + // TODO(phase-b): PHP `$package->{$method}($links)` — dynamic setter dispatch by name + let _ = &mut package; + let _ = links; + } + + let package = self.configure_object(package, &mut config)?; + + Ok(package) + } +} + +impl ArrayLoader { + /// @param array<array<mixed>> $versions + /// + /// @return list<CompletePackage|CompleteAliasPackage> + pub fn load_packages( + &self, + versions: Vec<IndexMap<String, PhpMixed>>, + ) -> Result<Vec<Box<BasePackage>>> { + let mut packages: Vec<Box<BasePackage>> = vec![]; + let mut link_cache: IndexMap< + String, + IndexMap<String, IndexMap<String, IndexMap<String, (String, Link)>>>, + > = IndexMap::new(); + + for mut version in versions { + let package = self.create_object(&version, "Composer\\Package\\CompletePackage")?; + + self.configure_cached_links(&mut link_cache, &package, &version)?; + let package = self.configure_object(package, &mut version)?; + + packages.push(package); + } + + Ok(packages) + } + + /// @template PackageClass of CompletePackage + /// + /// @param mixed[] $config package data + /// @param string $class FQCN to be instantiated + /// + /// @return CompletePackage|RootPackage + /// + /// @phpstan-param class-string<PackageClass> $class + fn create_object( + &self, + config: &IndexMap<String, PhpMixed>, + class: &str, + ) -> Result<Box<CompletePackage>> { + if !config.contains_key("name") { + return Err(UnexpectedValueException { + message: format!( + "Unknown package has no name defined ({}).", + json_encode(&PhpMixed::Array( + config + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + )) + .unwrap_or_default() + ), + code: 0, + } + .into()); + } + if !config.contains_key("version") || !is_scalar(config.get("version").unwrap()) { + return Err(UnexpectedValueException { + message: format!( + "Package {} has no version defined.", + config.get("name").and_then(|v| v.as_string()).unwrap_or("") + ), + code: 0, + } + .into()); + } + let mut config_version = config.get("version").cloned().unwrap_or(PhpMixed::Null); + if !is_string(&config_version) { + config_version = PhpMixed::String(strval(&config_version)); + } + + // handle already normalized versions + let version: String; + if config.contains_key("version_normalized") + && is_string(config.get("version_normalized").unwrap()) + { + let mut v = config + .get("version_normalized") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it + if v == VersionParser::DEFAULT_BRANCH_ALIAS { + v = self + .version_parser + .normalize(config_version.as_string().unwrap_or(""), None)?; + } + version = v; + } else { + match self + .version_parser + .normalize(config_version.as_string().unwrap_or(""), None) + { + Ok(v) => version = v, + Err(e) => { + // TODO(phase-b): preserve original exception chain via anyhow::Error::context + return Err(UnexpectedValueException { + message: format!( + "Failed to normalize version for package \"{}\": {}", + config.get("name").and_then(|v| v.as_string()).unwrap_or(""), + e + ), + code: 0, + } + .into()); + } + } + } + + // PHP: return new $class($config['name'], $version, $config['version']); + // TODO(phase-b): dispatch class-string $class to CompletePackage / RootPackage + // constructor; for now we only support CompletePackage + let _ = class; + let _name = config.get("name").and_then(|v| v.as_string()).unwrap_or(""); + let _pretty_version = config_version.as_string().unwrap_or("").to_string(); + let _ = version; + todo!("phase-b: dynamic class-string instantiation new $class($name, $version, $prettyVersion)") + } + + /// @param CompletePackage $package + /// @param mixed[] $config package data + /// + /// @return RootPackage|RootAliasPackage|CompletePackage|CompleteAliasPackage + fn configure_object( + &self, + mut package: Box<CompletePackage>, + config: &mut IndexMap<String, PhpMixed>, + ) -> Result<Box<BasePackage>> { + // PHP: if (!$package instanceof CompletePackage) — true by construction in Rust + // (create_object always returns Box<CompletePackage>); kept as a no-op for parity. + let _ = LogicException { + message: "ArrayLoader expects instances of the Composer\\Package\\CompletePackage class to function correctly".to_string(), + code: 0, + }; + + // PHP: $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); + // TODO(phase-b): set_type on CompletePackage/Package + let _type_value = if let Some(t) = config.get("type") { + strtolower(t.as_string().unwrap_or("")) + } else { + "library".to_string() + }; + + if let Some(target_dir) = config.get("target-dir") { + // TODO(phase-b): package.set_target_dir + let _ = target_dir; + } + + if let Some(extra) = config.get("extra") { + if matches!(extra, PhpMixed::Array(_)) { + // TODO(phase-b): package.set_extra + let _ = extra; + } + } + + if let Some(bin) = config.get("bin").cloned() { + let mut bin_list = match bin { + PhpMixed::Array(_) | PhpMixed::List(_) => bin, + other => PhpMixed::List(vec![Box::new(other)]), + }; + // foreach ($config['bin'] as $key => $bin) { $config['bin'][$key] = ltrim($bin, '/'); } + if let PhpMixed::List(ref mut list) = bin_list { + for item in list.iter_mut() { + if let Some(s) = item.as_string() { + *item = Box::new(PhpMixed::String(ltrim(s, Some("/")))); + } + } + } else if let PhpMixed::Array(ref mut map) = bin_list { + for (_k, v) in map.iter_mut() { + if let Some(s) = v.as_string() { + *v = Box::new(PhpMixed::String(ltrim(s, Some("/")))); + } + } + } + config.insert("bin".to_string(), bin_list); + // TODO(phase-b): package.set_binaries + } + + if let Some(installation_source) = config.get("installation-source") { + // TODO(phase-b): package.set_installation_source + let _ = installation_source; + } + + if let Some(default_branch) = config.get("default-branch") { + if default_branch.as_bool() == Some(true) { + // TODO(phase-b): package.set_is_default_branch(true) + } + } + + if let Some(source) = config.get("source").cloned() { + let source_map = match &source { + PhpMixed::Array(m) => Some(m.clone()), + _ => None, + }; + let has_required = source_map + .as_ref() + .map(|m| { + m.contains_key("type") && m.contains_key("url") && m.contains_key("reference") + }) + .unwrap_or(false); + if !has_required { + return Err(UnexpectedValueException { + message: sprintf( + "Package %s's source key should be specified as {\"type\": ..., \"url\": ..., \"reference\": ...},\n%s given.", + &[ + PhpMixed::String( + config + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + ), + PhpMixed::String(json_encode(&source).unwrap_or_default()), + ], + ), + code: 0, + } + .into()); + } + let source_map = source_map.unwrap(); + // TODO(phase-b): package.set_source_type/_url/_reference/_mirrors + let _ = source_map.get("type"); + let _ = source_map.get("url"); + let _reference = source_map.get("reference").map(|v| strval(v)); + let _ = _reference; + if let Some(mirrors) = source_map.get("mirrors") { + let _ = mirrors; + } + } + + if let Some(dist) = config.get("dist").cloned() { + let dist_map = match &dist { + PhpMixed::Array(m) => Some(m.clone()), + _ => None, + }; + let has_required = dist_map + .as_ref() + .map(|m| m.contains_key("type") && m.contains_key("url")) + .unwrap_or(false); + if !has_required { + return Err(UnexpectedValueException { + message: sprintf( + "Package %s's dist key should be specified as {\"type\": ..., \"url\": ..., \"reference\": ..., \"shasum\": ...},\n%s given.", + &[ + PhpMixed::String( + config + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + ), + PhpMixed::String(json_encode(&dist).unwrap_or_default()), + ], + ), + code: 0, + } + .into()); + } + let dist_map = dist_map.unwrap(); + // TODO(phase-b): package.set_dist_type/_url/_reference/_sha1_checksum/_mirrors + let _ = dist_map.get("type"); + let _ = dist_map.get("url"); + let _reference = dist_map.get("reference").map(|v| strval(v)); + let _ = _reference; + let _shasum = dist_map.get("shasum"); + let _ = _shasum; + if let Some(mirrors) = dist_map.get("mirrors") { + let _ = mirrors; + } + } + + if let Some(suggest) = config.get("suggest").cloned() { + if let PhpMixed::Array(mut suggest_map) = suggest { + for (target, reason) in suggest_map.iter_mut() { + if let Some(r) = reason.as_string() { + if trim(r, None) == "self.version" { + *reason = Box::new(PhpMixed::String( + package.get_pretty_version().to_string(), + )); + let _ = target; + } + } + } + config.insert("suggest".to_string(), PhpMixed::Array(suggest_map)); + // TODO(phase-b): package.set_suggests + } + } + + if let Some(autoload) = config.get("autoload") { + // TODO(phase-b): package.set_autoload + let _ = autoload; + } + + if let Some(autoload_dev) = config.get("autoload-dev") { + // TODO(phase-b): package.set_dev_autoload + let _ = autoload_dev; + } + + if let Some(include_path) = config.get("include-path") { + // TODO(phase-b): package.set_include_paths + let _ = include_path; + } + + if let Some(php_ext) = config.get("php-ext") { + // TODO(phase-b): package.set_php_ext + let _ = php_ext; + } + + if let Some(time_value) = config.get("time") { + if !shirabe_php_shim::empty(time_value) { + let time_str = time_value.as_string().unwrap_or(""); + let time = if Preg::is_match(r"/^\d++$/D", time_str) { + format!("@{}", time_str) + } else { + time_str.to_string() + }; + + let result: std::result::Result<DateTime<Utc>, Exception> = + // TODO(phase-b): port PHP `new \DateTime($time, new \DateTimeZone('UTC'))` + Utc.datetime_from_str(&time, "%Y-%m-%dT%H:%M:%S%z") + .map_err(|e| Exception { + message: e.to_string(), + code: 0, + }); + if let Ok(date) = result { + // TODO(phase-b): package.set_release_date(date) + let _ = date; + } + } + } + + if let Some(notification_url) = config.get("notification-url") { + if !shirabe_php_shim::empty(notification_url) { + // TODO(phase-b): package.set_notification_url + let _ = notification_url; + } + } + + // PHP: $package instanceof CompletePackageInterface — true since $package is CompletePackage + { + if let Some(archive) = config.get("archive").cloned() { + if let PhpMixed::Array(archive_map) = archive { + if let Some(name) = archive_map.get("name") { + if !shirabe_php_shim::empty(name) { + // TODO(phase-b): package.set_archive_name + let _ = name; + } + } + if let Some(exclude) = archive_map.get("exclude") { + if !shirabe_php_shim::empty(exclude) { + // TODO(phase-b): package.set_archive_excludes + let _ = exclude; + } + } + } + } + + if let Some(scripts) = config.get("scripts").cloned() { + if let PhpMixed::Array(mut scripts_map) = scripts { + for (event, listeners) in scripts_map.iter_mut() { + let listeners_array = match listeners.as_ref() { + PhpMixed::Array(_) | PhpMixed::List(_) => listeners.clone(), + other => Box::new(PhpMixed::List(vec![Box::new(other.clone())])), + }; + *listeners = listeners_array; + let _ = event; + } + for reserved in ["composer", "php", "putenv"].iter() { + if scripts_map.contains_key(*reserved) { + trigger_error( + &format!( + "The `{}` script name is reserved for internal use, please avoid defining it", + reserved + ), + E_USER_DEPRECATED, + ); + } + } + config.insert("scripts".to_string(), PhpMixed::Array(scripts_map)); + // TODO(phase-b): package.set_scripts + } + } + + if let Some(description) = config.get("description") { + if !shirabe_php_shim::empty(description) && is_string(description) { + package.set_description( + description.as_string().unwrap_or("").to_string(), + ); + } + } + + if let Some(homepage) = config.get("homepage") { + if !shirabe_php_shim::empty(homepage) && is_string(homepage) { + package.set_homepage(homepage.as_string().unwrap_or("").to_string()); + } + } + + if let Some(keywords) = config.get("keywords") { + if !shirabe_php_shim::empty(keywords) { + if matches!(keywords, PhpMixed::Array(_) | PhpMixed::List(_)) { + // PHP: array_map('strval', $config['keywords']) + let keywords_vec: Vec<String> = match keywords { + PhpMixed::List(list) => list.iter().map(|v| strval(v)).collect(), + PhpMixed::Array(map) => map.values().map(|v| strval(v)).collect(), + _ => vec![], + }; + package.set_keywords(keywords_vec); + } + } + } + + if let Some(license) = config.get("license") { + if !shirabe_php_shim::empty(license) { + let license_vec: Vec<String> = match license { + PhpMixed::Array(map) => map + .values() + .map(|v| v.as_string().unwrap_or("").to_string()) + .collect(), + PhpMixed::List(list) => list + .iter() + .map(|v| v.as_string().unwrap_or("").to_string()) + .collect(), + other => vec![other.as_string().unwrap_or("").to_string()], + }; + package.set_license(license_vec); + } + } + + if let Some(authors) = config.get("authors") { + if !shirabe_php_shim::empty(authors) { + if let PhpMixed::List(list) = authors { + let authors_vec: Vec<IndexMap<String, String>> = list + .iter() + .filter_map(|v| match v.as_ref() { + PhpMixed::Array(m) => Some( + m.iter() + .map(|(k, v)| { + (k.clone(), v.as_string().unwrap_or("").to_string()) + }) + .collect(), + ), + _ => None, + }) + .collect(); + package.set_authors(authors_vec); + } + } + } + + if let Some(support) = config.get("support") { + if let PhpMixed::Array(map) = support { + let support_map: IndexMap<String, String> = map + .iter() + .map(|(k, v)| (k.clone(), v.as_string().unwrap_or("").to_string())) + .collect(); + package.set_support(support_map); + } + } + + if let Some(funding) = config.get("funding") { + if !shirabe_php_shim::empty(funding) { + if let PhpMixed::List(list) = funding { + let funding_vec: Vec<IndexMap<String, PhpMixed>> = list + .iter() + .filter_map(|v| match v.as_ref() { + PhpMixed::Array(m) => Some( + m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect(), + ), + _ => None, + }) + .collect(); + package.set_funding(funding_vec); + } + } + } + + if let Some(abandoned) = config.get("abandoned") { + package.set_abandoned(abandoned.clone()); + } + } + + if self.load_options { + if let Some(transport_options) = config.get("transport-options") { + // TODO(phase-b): package.set_transport_options + let _ = transport_options; + } + } + + let alias_normalized = self.get_branch_alias(config)?; + if let Some(alias_normalized) = alias_normalized { + if !alias_normalized.is_empty() { + let pretty_alias = Preg::replace(r"{(\.9{7})+}", ".x", &alias_normalized); + + // TODO(phase-b): `$package instanceof RootPackage` downcast from CompletePackage + let package_as_root: Option<RootPackage> = None; + if let Some(root) = package_as_root { + let _ = RootAliasPackage::new(root, alias_normalized, pretty_alias); + // TODO(phase-b): return Box<RootAliasPackage> wrapped as Box<BasePackage> + todo!("phase-b: return RootAliasPackage as Box<BasePackage>") + } + + let _ = CompleteAliasPackage::new(*package, alias_normalized, pretty_alias); + // TODO(phase-b): return Box<CompleteAliasPackage> wrapped as Box<BasePackage> + todo!("phase-b: return CompleteAliasPackage as Box<BasePackage>") + } + } + + // TODO(phase-b): coerce Box<CompletePackage> -> Box<BasePackage> + let _ = package; + todo!("phase-b: return Box<CompletePackage> as Box<BasePackage>") + } + + /// @param array<string, array<string, array<int|string, array<int|string, array{string, Link}>>>> $linkCache + /// @param mixed[] $config + fn configure_cached_links( + &self, + link_cache: &mut IndexMap< + String, + IndexMap<String, IndexMap<String, IndexMap<String, (String, Link)>>>, + >, + package: &Box<CompletePackage>, + config: &IndexMap<String, PhpMixed>, + ) -> Result<()> { + let name = package.get_name().to_string(); + let pretty_version = package.get_pretty_version().to_string(); + + for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { + if let Some(entry) = config.get(*r#type) { + let _method = format!("set{}", ucfirst(opts.method)); + + let mut links: IndexMap<String, Link> = IndexMap::new(); + let entries: IndexMap<String, PhpMixed> = match entry { + PhpMixed::Array(m) => m + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + _ => continue, + }; + for (pretty_target, constraint) in entries { + let target = strtolower(&pretty_target); + + // recursive links are not supported + if target == name { + continue; + } + + let constraint_str = constraint.as_string().unwrap_or("").to_string(); + if constraint_str == "self.version" { + let link = self.create_link( + &name, + &pretty_version, + opts.method, + &target, + &constraint_str, + )?; + links.insert(target, link); + } else { + let cached = link_cache + .get(&name) + .and_then(|m| m.get(*r#type)) + .and_then(|m| m.get(&target)) + .and_then(|m| m.get(&constraint_str)) + .cloned(); + let (target, link) = if let Some(cached) = cached { + cached + } else { + let link = self.create_link( + &name, + &pretty_version, + opts.method, + &target, + &constraint_str, + )?; + let entry = (target.clone(), link); + link_cache + .entry(name.clone()) + .or_insert_with(IndexMap::new) + .entry(r#type.to_string()) + .or_insert_with(IndexMap::new) + .entry(target.clone()) + .or_insert_with(IndexMap::new) + .insert(constraint_str.clone(), entry.clone()); + entry + }; + links.insert(target, link); + } + } + + // TODO(phase-b): PHP `$package->{$method}($links)` — dynamic setter dispatch by name + let _ = links; + } + } + + Ok(()) + } + + /// @param string $source source package name + /// @param string $sourceVersion source package version (pretty version ideally) + /// @param string $description link description (e.g. requires, replaces, ..) + /// @param array<string|int, string> $links array of package name => constraint mappings + /// + /// @return Link[] + /// + /// @phpstan-param Link::TYPE_* $description + pub fn parse_links( + &self, + source: &str, + source_version: &str, + description: &str, + links: IndexMap<String, PhpMixed>, + ) -> Result<IndexMap<String, Link>> { + let mut res: IndexMap<String, Link> = IndexMap::new(); + for (target, constraint) in links { + if !is_string(&constraint) { + continue; + } + let target = strtolower(&target); + let link = self.create_link( + source, + source_version, + description, + &target, + constraint.as_string().unwrap_or(""), + )?; + res.insert(target, link); + } + + Ok(res) + } + + /// @param string $source source package name + /// @param string $sourceVersion source package version (pretty version ideally) + /// @param Link::TYPE_* $description link description (e.g. requires, replaces, ..) + /// @param string $target target package name + /// @param string $prettyConstraint constraint string + fn create_link( + &self, + source: &str, + source_version: &str, + description: &str, + target: &str, + pretty_constraint: &str, + ) -> Result<Link> { + // PHP: if (!\is_string($prettyConstraint)) — always true in Rust signature, kept for parity + let _ = pretty_constraint; + + let constraint = if pretty_constraint == "self.version" { + source_version.to_string() + } else { + pretty_constraint.to_string() + }; + + let parsed_constraint = match self.version_parser.parse_constraints(&constraint) { + Ok(c) => c, + Err(e) => { + // TODO(phase-b): preserve original exception chain + let _ = &e; + return Err(UnexpectedValueException { + message: format!( + "Link constraint in {} {} > {} should be a valid version constraint, got \"{}\"", + source, description, target, constraint + ), + code: 0, + } + .into()); + } + }; + + // TODO(phase-b): Link::new expects Box<dyn ConstraintInterface>; we have Arc<dyn ConstraintInterface + Send + Sync> + let _ = parsed_constraint; + Ok(Link::new( + source.to_string(), + target.to_string(), + todo!("phase-b: convert Arc<dyn ConstraintInterface> to Box<dyn ConstraintInterface>"), + Some(description.to_string()), + Some(pretty_constraint.to_string()), + )) + } + + /// Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists + /// + /// @param mixed[] $config the entire package config + /// + /// @return string|null normalized version of the branch alias or null if there is none + pub fn get_branch_alias( + &self, + config: &IndexMap<String, PhpMixed>, + ) -> Result<Option<String>> { + if !config.contains_key("version") || !is_scalar(config.get("version").unwrap()) { + return Err(UnexpectedValueException { + message: "no/invalid version defined".to_string(), + code: 0, + } + .into()); + } + let mut config_version = config.get("version").cloned().unwrap_or(PhpMixed::Null); + if !is_string(&config_version) { + config_version = PhpMixed::String(strval(&config_version)); + } + + let version_str = config_version.as_string().unwrap_or("").to_string(); + if strpos(&version_str, "dev-") != Some(0) && "-dev" != substr(&version_str, -4, None) { + return Ok(None); + } + + let extra_branch_alias = config + .get("extra") + .and_then(|v| match v { + PhpMixed::Array(m) => m.get("branch-alias").cloned(), + _ => None, + }) + .and_then(|v| match v.as_ref() { + PhpMixed::Array(m) => Some(m.clone()), + _ => None, + }); + + if let Some(branch_alias_map) = extra_branch_alias { + for (source_branch, target_branch_value) in branch_alias_map { + let source_branch = strval(&PhpMixed::String(source_branch)); + let target_branch = target_branch_value.as_string().unwrap_or("").to_string(); + + // ensure it is an alias to a -dev package + if "-dev" != substr(&target_branch, -4, None) { + continue; + } + + // normalize without -dev and ensure it's a numeric branch that is parseable + let validated_target_branch = if target_branch == VersionParser::DEFAULT_BRANCH_ALIAS + { + VersionParser::DEFAULT_BRANCH_ALIAS.to_string() + } else { + self.version_parser + .normalize_branch(&substr(&target_branch, 0, Some(-4)))? + }; + if "-dev" != substr(&validated_target_branch, -4, None) { + continue; + } + + // ensure that it is the current branch aliasing itself + if strtolower(&version_str) != strtolower(&source_branch) { + continue; + } + + // If using numeric aliases ensure the alias is a valid subversion + let source_prefix = + self.version_parser.parse_numeric_alias_prefix(&source_branch); + let target_prefix = self.version_parser.parse_numeric_alias_prefix(&target_branch); + if let (Some(sp), Some(tp)) = (source_prefix.as_ref(), target_prefix.as_ref()) { + if stripos(tp, sp) != Some(0) { + continue; + } + } + + return Ok(Some(validated_target_branch)); + } + } + + let default_branch_is_true = + config.get("default-branch").and_then(|v| v.as_bool()) == Some(true); + if config.contains_key("default-branch") + && default_branch_is_true + && self + .version_parser + .parse_numeric_alias_prefix(&Preg::replace(r"{^v}", "", &version_str)) + .is_none() + { + return Ok(Some(VersionParser::DEFAULT_BRANCH_ALIAS.to_string())); + } + + Ok(None) + } +} |
