diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 15:26:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 15:26:44 +0900 |
| commit | fe461d7eec72881f2e25d73f2719138fa88b3689 (patch) | |
| tree | c5c4cceb376d9f644440a508639997eda6a3a2a1 | |
| parent | db14ddcaef545fe47b5c98dc134ae67fb8483e41 (diff) | |
| download | php-shirabe-fe461d7eec72881f2e25d73f2719138fa88b3689.tar.gz php-shirabe-fe461d7eec72881f2e25d73f2719138fa88b3689.tar.zst php-shirabe-fe461d7eec72881f2e25d73f2719138fa88b3689.zip | |
feat(port): port FilesystemRepository.php
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 37 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/filesystem_repository.rs | 718 |
2 files changed, 755 insertions, 0 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 12972b5..2dbeab6 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1105,6 +1105,43 @@ pub fn str_ends_with(haystack: &str, needle: &str) -> bool { todo!() } +pub fn sort<T: Ord>(array: &mut Vec<T>) { + todo!() +} + +pub fn sort_with_flags<T: Ord>(array: &mut Vec<T>, flags: i64) { + todo!() +} + +pub const SORT_REGULAR: i64 = 0; +pub const SORT_NUMERIC: i64 = 1; +pub const SORT_STRING: i64 = 2; +pub const SORT_NATURAL: i64 = 6; +pub const SORT_FLAG_CASE: i64 = 8; + +pub fn usort<T, F>(array: &mut Vec<T>, compare: F) +where + F: FnMut(&T, &T) -> i64, +{ + todo!() +} + +pub fn ksort<V>(array: &mut IndexMap<String, V>) { + todo!() +} + +pub fn is_int(value: &PhpMixed) -> bool { + todo!() +} + +pub fn is_null(value: &PhpMixed) -> bool { + todo!() +} + +pub fn r#eval(code: &str) -> PhpMixed { + todo!() +} + pub fn call_user_func_array(callback: &str, args: &PhpMixed) -> PhpMixed { todo!() } diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index b771b6c..5cbff22 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -1 +1,719 @@ //! ref: composer/src/Composer/Repository/FilesystemRepository.php + +use std::any::Any; + +use anyhow::Result; +use indexmap::IndexMap; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_php_shim::{ + array_flip, dirname, file_get_contents, get_class, get_debug_type, in_array, is_array, + is_int, is_null, is_string, ksort, php_dir, r#eval, realpath, sort, sort_with_flags, + str_repeat, strtr, trim, usort, var_export, InvalidArgumentException, LogicException, + PhpMixed, Silencer, UnexpectedValueException, SORT_NATURAL, +}; + +use crate::installed_versions::InstalledVersions; +use crate::installer::installation_manager::InstallationManager; +use crate::json::json_file::JsonFile; +use crate::package::alias_package::AliasPackage; +use crate::package::dumper::array_dumper::ArrayDumper; +use crate::package::loader::array_loader::ArrayLoader; +use crate::package::package_interface::PackageInterface; +use crate::package::root_alias_package::RootAliasPackage; +use crate::package::root_package_interface::RootPackageInterface; +use crate::repository::invalid_repository_exception::InvalidRepositoryException; +use crate::repository::platform_repository::PlatformRepository; +use crate::repository::writable_array_repository::WritableArrayRepository; +use crate::util::filesystem::Filesystem; +use crate::util::platform::Platform; + +/// Filesystem repository. +#[derive(Debug)] +pub struct FilesystemRepository { + pub(crate) inner: WritableArrayRepository, + /// @var JsonFile + pub(crate) file: JsonFile, + /// @var bool + dump_versions: bool, + /// @var ?RootPackageInterface + root_package: Option<Box<dyn RootPackageInterface>>, + /// @var Filesystem + filesystem: Filesystem, + /// @var bool|null + dev_mode: Option<bool>, +} + +impl FilesystemRepository { + /// Initializes filesystem repository. + /// + /// @param JsonFile $repositoryFile repository json file + /// @param ?RootPackageInterface $rootPackage Must be provided if $dumpVersions is true + pub fn new( + repository_file: JsonFile, + dump_versions: bool, + root_package: Option<Box<dyn RootPackageInterface>>, + filesystem: Option<Filesystem>, + ) -> Result<Self> { + let filesystem = filesystem.unwrap_or_else(Filesystem::new); + if dump_versions && root_package.is_none() { + return Err(InvalidArgumentException { + message: "Expected a root package instance if $dumpVersions is true".to_string(), + code: 0, + } + .into()); + } + Ok(Self { + // TODO(phase-b): WritableArrayRepository::new() needs to be exposed + inner: todo!("WritableArrayRepository::new()"), + file: repository_file, + dump_versions, + root_package, + filesystem, + dev_mode: None, + }) + } + + /// @return bool|null true if dev requirements were installed, false if --no-dev was used, null if yet unknown + pub fn get_dev_mode(&self) -> Option<bool> { + self.dev_mode + } + + /// Initializes repository (reads file, or remote address). + pub(crate) fn initialize(&mut self) -> Result<()> { + self.inner.initialize(); + + if !self.file.exists() { + return Ok(()); + } + + // TODO(phase-b): use anyhow::Result<Result<T, E>> to model PHP try/catch + let packages: PhpMixed = match (|| -> Result<PhpMixed> { + let data = self.file.read()?; + let packages_value = if let PhpMixed::Array(ref m) = data { + if m.contains_key("packages") { + (*m.get("packages").unwrap().clone()).clone() + } else { + data.clone() + } + } else { + data.clone() + }; + + if let PhpMixed::Array(ref m) = data { + if let Some(names) = m.get("dev-package-names") { + let dev_names: Vec<String> = names + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + self.inner.set_dev_package_names(dev_names); + } + if let Some(dev) = m.get("dev") { + self.dev_mode = dev.as_bool(); + } + } + + if !is_array(&packages_value) { + return Err(UnexpectedValueException { + message: "Could not parse package list from the repository".to_string(), + code: 0, + } + .into()); + } + + Ok(packages_value) + })() { + Ok(p) => p, + Err(e) => { + return Err(InvalidRepositoryException::new(format!( + "Invalid repository data in {}, packages could not be loaded: [{}] {}", + self.file.get_path(), + get_class(&e), + e, + )) + .into()); + } + }; + + let mut loader = ArrayLoader::new(None, true); + if let Some(packages_list) = packages.as_list() { + for package_data in packages_list.iter() { + let package = loader.load((**package_data).clone(), "Composer\\Package\\CompletePackage")?; + self.inner.add_package(package)?; + } + } else if let Some(packages_array) = packages.as_array() { + for (_, package_data) in packages_array.iter() { + let package = loader.load((**package_data).clone(), "Composer\\Package\\CompletePackage")?; + self.inner.add_package(package)?; + } + } + + Ok(()) + } + + pub fn reload(&mut self) -> Result<()> { + // TODO(phase-b): clear inner packages cache (PHP: $this->packages = null) + self.inner.reload(); + self.initialize() + } + + /// Writes writable repository. + pub fn write( + &mut self, + dev_mode: bool, + installation_manager: &InstallationManager, + ) -> Result<()> { + let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); + data.insert("packages".to_string(), PhpMixed::List(vec![])); + data.insert("dev".to_string(), PhpMixed::Bool(dev_mode)); + data.insert("dev-package-names".to_string(), PhpMixed::List(vec![])); + + let dumper = ArrayDumper::new(); + + // make sure the directory is created so we can realpath it + // as realpath() does some additional normalizations with network paths that normalizePath does not + // and we need to find shortest path correctly + let repo_dir = dirname(self.file.get_path()); + self.filesystem.ensure_directory_exists(&repo_dir); + + let repo_dir = self.filesystem.normalize_path( + &realpath(&repo_dir).unwrap_or_default(), + ); + let mut install_paths: IndexMap<String, Option<String>> = IndexMap::new(); + + for package in self.inner.get_canonical_packages() { + let mut pkg_array = dumper.dump(&*package); + let path = installation_manager.get_install_path(&*package); + let mut install_path: Option<String> = None; + if let Some(path_str) = &path { + if !path_str.is_empty() { + let normalized_path = self.filesystem.normalize_path( + &if self.filesystem.is_absolute_path(path_str) { + path_str.clone() + } else { + format!("{}/{}", Platform::get_cwd(false).unwrap_or_default(), path_str) + }, + ); + install_path = Some(self.filesystem.find_shortest_path( + &repo_dir, + &normalized_path, + true, + )); + } + } + install_paths.insert(package.get_name().to_string(), install_path.clone()); + + pkg_array.insert( + "install-path".to_string(), + match install_path { + Some(s) => PhpMixed::String(s), + None => PhpMixed::Null, + }, + ); + if let Some(PhpMixed::List(list)) = data.get_mut("packages") { + list.push(Box::new(PhpMixed::Array( + pkg_array.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + ))); + } + + // only write to the files the names which are really installed, as we receive the full list + // of dev package names before they get installed during composer install + if in_array( + PhpMixed::String(package.get_name().to_string()), + &PhpMixed::List( + self.inner + .dev_package_names + .iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + true, + ) { + if let Some(PhpMixed::List(list)) = data.get_mut("dev-package-names") { + list.push(Box::new(PhpMixed::String(package.get_name().to_string()))); + } + } + } + + // PHP: sort($data['dev-package-names']); + if let Some(PhpMixed::List(list)) = data.get_mut("dev-package-names") { + // TODO(phase-b): sort PhpMixed::List in-place using string comparison + sort(list); + } + // PHP: usort($data['packages'], static function ($a, $b): int { return strcmp($a['name'], $b['name']); }); + if let Some(PhpMixed::List(list)) = data.get_mut("packages") { + usort(list, |a: &Box<PhpMixed>, b: &Box<PhpMixed>| -> i64 { + let a_name = a + .as_array() + .and_then(|m| m.get("name")) + .and_then(|v| v.as_string()) + .unwrap_or(""); + let b_name = b + .as_array() + .and_then(|m| m.get("name")) + .and_then(|v| v.as_string()) + .unwrap_or(""); + shirabe_php_shim::strcmp(a_name, b_name) + }); + } + + self.file.write( + PhpMixed::Array( + data.clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + shirabe_php_shim::JSON_UNESCAPED_SLASHES + | shirabe_php_shim::JSON_PRETTY_PRINT + | shirabe_php_shim::JSON_UNESCAPED_UNICODE, + )?; + + if self.dump_versions { + let versions = + self.generate_installed_versions(installation_manager, &install_paths, dev_mode, &repo_dir)?; + + self.filesystem.file_put_contents_if_modified( + &format!("{}/installed.php", repo_dir), + &format!( + "<?php return {};\n", + self.dump_to_php_code(&versions, 0), + ), + ); + let installed_versions_class = file_get_contents(&format!( + "{}/../InstalledVersions.php", + php_dir(), + )); + + // this normally should not happen but during upgrades of Composer when it is installed in the project it is a possibility + if let Some(class_content) = installed_versions_class { + self.filesystem.file_put_contents_if_modified( + &format!("{}/InstalledVersions.php", repo_dir), + &class_content, + ); + + // make sure the in memory state is up to date with on disk + InstalledVersions::reload(versions); + + // make sure the selfDir matches the expected data at runtime if the class was loaded from the vendor dir, as it may have been + // loaded from the Composer sources, causing packages to appear twice in that case if the installed.php is loaded in addition to the + // in memory loaded data from above + // TODO(phase-b): Reflection API on static properties — confirm porting approach with user + let _attempt: Result<()> = (|| -> Result<()> { + todo!( + "ReflectionProperty(Composer\\InstalledVersions::class, 'selfDir')->setValue(null, strtr($repoDir, '\\\\', '/'))" + ); + // (the second reflection block sets installedIsLocalDir = true) + })(); + // PHP: catches \ReflectionException and rethrows if not "Property does not exist" + } + } + + Ok(()) + } + + /// As we load the file from vendor dir during bootstrap, we need to make sure it contains only expected code before executing it + /// + /// @internal + pub fn safely_load_installed_versions(path: &str) -> bool { + // PHP: @file_get_contents($path) + let installed_versions_data = Silencer::call(|| Ok(file_get_contents(path))) + .ok() + .flatten(); + let pattern = "{(?(DEFINE)\n (?<number> -? \\s*+ \\d++ (?:\\.\\d++)? )\n (?<boolean> true | false | null )\n (?<strings> (?&string) (?: \\s*+ \\. \\s*+ (?&string))*+ )\n (?<string> (?: \" (?:[^\"\\\\$]*+ | \\\\ [\"\\\\0] )* \" | ' (?:[^'\\\\]*+ | \\\\ ['\\\\] )* ' ) )\n (?<array> array\\( \\s*+ (?: (?:(?&number)|(?&strings)) \\s*+ => \\s*+ (?: (?:__DIR__ \\s*+ \\. \\s*+)? (?&strings) | (?&value) ) \\s*+, \\s*+ )*+ \\s*+ \\) )\n (?<value> (?: (?&number) | (?&boolean) | (?&strings) | (?&array) ) )\n)\n^<\\?php\\s++return\\s++(?&array)\\s*+;$}ix"; + if let Some(data) = installed_versions_data { + let mixed = PhpMixed::String(data.clone()); + if is_string(&mixed) && Preg::is_match(pattern, &trim(&data, None)) { + let replaced = Preg::replace( + r"{=>\s*+__DIR__\s*+\.\s*+(['\"])}", + &format!( + "=> {} . $1", + var_export(&PhpMixed::String(dirname(path)), true), + ), + &data, + ); + let evaluated = r#eval(&format!("?>{}", replaced)); + InstalledVersions::reload( + evaluated + .as_array() + .cloned() + .map(|m| m.into_iter().map(|(k, v)| (k, *v)).collect()) + .unwrap_or_default(), + ); + + return true; + } + } + + false + } + + /// @param array<mixed> $array + fn dump_to_php_code(&self, array: &IndexMap<String, PhpMixed>, level: i64) -> String { + let mut lines = String::from("array(\n"); + let level = level + 1; + + for (key, value) in array { + lines.push_str(&str_repeat(" ", level as usize)); + lines.push_str(&if is_int(&PhpMixed::String(key.clone())) { + // TODO(phase-b): PHP integer-keyed array entries — IndexMap keys are strings + format!("{} => ", key) + } else { + format!("{} => ", var_export(&PhpMixed::String(key.clone()), true)) + }); + + if is_array(value) { + if let Some(inner_arr) = value.as_array() { + if !inner_arr.is_empty() { + let inner_map: IndexMap<String, PhpMixed> = inner_arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + lines.push_str(&self.dump_to_php_code(&inner_map, level)); + } else { + lines.push_str("array(),\n"); + } + } else if let Some(list) = value.as_list() { + if !list.is_empty() { + let inner_map: IndexMap<String, PhpMixed> = list + .iter() + .enumerate() + .map(|(i, v)| (i.to_string(), (**v).clone())) + .collect(); + lines.push_str(&self.dump_to_php_code(&inner_map, level)); + } else { + lines.push_str("array(),\n"); + } + } + } else if key == "install_path" && is_string(value) { + let s = value.as_string().unwrap_or("").to_string(); + if self.filesystem.is_absolute_path(&s) { + lines.push_str(&format!( + "{},\n", + var_export(&PhpMixed::String(s), true), + )); + } else { + lines.push_str(&format!( + "__DIR__ . {},\n", + var_export(&PhpMixed::String(format!("/{}", s)), true), + )); + } + } else if is_string(value) { + lines.push_str(&format!("{},\n", var_export(value, true))); + } else if let PhpMixed::Bool(b) = value { + lines.push_str(&format!("{},\n", if *b { "true" } else { "false" })); + } else if is_null(value) { + lines.push_str("null,\n"); + } else { + // PHP: throw new \UnexpectedValueException('Unexpected type '.get_debug_type($value)); + panic!("{}", format!("Unexpected type {}", get_debug_type(value))); + } + } + + lines.push_str(&format!( + "{}){}", + str_repeat(" ", (level - 1) as usize), + if (level - 1) == 0 { "" } else { ",\n" }, + )); + + lines + } + + /// @param array<string, string> $installPaths + fn generate_installed_versions( + &self, + installation_manager: &InstallationManager, + install_paths: &IndexMap<String, Option<String>>, + dev_mode: bool, + repo_dir: &str, + ) -> Result<IndexMap<String, PhpMixed>> { + let dev_packages = array_flip(&PhpMixed::List( + self.inner + .dev_package_names + .iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + )); + let mut packages: Vec<Box<dyn PackageInterface>> = self + .inner + .get_packages() + .into_iter() + // TODO(phase-b): Box<BasePackage> -> Box<dyn PackageInterface> + .map(|p| todo!("Box<BasePackage> to Box<dyn PackageInterface>")) + .collect(); + let mut root_package = match &self.root_package { + None => { + return Err(LogicException { + message: "It should not be possible to dump packages if no root package is given".to_string(), + code: 0, + } + .into()); + } + // TODO(phase-b): clone root_package to push into packages list + Some(_r) => todo!("clone root_package"), + }; + // packages[] = $rootPackage = $this->rootPackage; + // TODO(phase-b): track current root_package in mutable variable + let mut current_root: Box<dyn RootPackageInterface> = root_package; + // packages.push(current_root.clone_box()); + + while let Some(_alias) = (current_root.as_any() as &dyn Any).downcast_ref::<RootAliasPackage>() { + current_root = todo!("RootAliasPackage::get_alias_of() returning Box<dyn RootPackageInterface>"); + // packages.push(current_root.clone_box()); + } + let mut versions: IndexMap<String, PhpMixed> = IndexMap::new(); + versions.insert( + "root".to_string(), + PhpMixed::Array( + self.dump_root_package( + &*current_root, + install_paths, + dev_mode, + repo_dir, + &dev_packages, + ) + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + ); + versions.insert( + "versions".to_string(), + PhpMixed::Array(IndexMap::new()), + ); + + // add real installed packages + for package in &packages { + if (package.as_any() as &dyn Any).downcast_ref::<AliasPackage>().is_some() { + continue; + } + + let dumped = self.dump_installed_package(&**package, install_paths, repo_dir, &dev_packages); + if let Some(PhpMixed::Array(versions_map)) = versions.get_mut("versions") { + versions_map.insert( + package.get_name().to_string(), + Box::new(PhpMixed::Array( + dumped.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + )), + ); + } + } + + // add provided/replaced packages + for package in &packages { + let is_dev_package = dev_packages + .as_array() + .map(|m| m.contains_key(package.get_name())) + .unwrap_or(false); + for replace in package.get_replaces() { + // exclude platform replaces as when they are really there we can not check for their presence + if PlatformRepository::is_platform_package(replace.get_target()) { + continue; + } + // PHP: dev_requirement handling + // TODO(phase-b): mutate nested versions['versions'][$replace->getTarget()]['dev_requirement'] + todo!("mutate nested versions['versions'][target]['dev_requirement']"); + #[allow(unreachable_code)] + { + let mut replaced = replace.get_pretty_constraint().unwrap_or("").to_string(); + if replaced == "self.version" { + replaced = package.get_pretty_version().to_string(); + } + // TODO(phase-b): mutate nested versions['versions'][$replace->getTarget()]['replaced'] + todo!("append replaced to versions['versions'][target]['replaced']"); + } + } + for provide in package.get_provides() { + // exclude platform provides as when they are really there we can not check for their presence + if PlatformRepository::is_platform_package(provide.get_target()) { + continue; + } + // TODO(phase-b): mutate nested versions['versions'][$provide->getTarget()]['dev_requirement'] + todo!("mutate nested versions['versions'][target]['dev_requirement']"); + #[allow(unreachable_code)] + { + let mut provided = provide.get_pretty_constraint().unwrap_or("").to_string(); + if provided == "self.version" { + provided = package.get_pretty_version().to_string(); + } + // TODO(phase-b): mutate nested versions['versions'][$provide->getTarget()]['provided'] + todo!("append provided to versions['versions'][target]['provided']"); + } + } + } + + // add aliases + for package in &packages { + let Some(alias) = (package.as_any() as &dyn Any).downcast_ref::<AliasPackage>() else { + continue; + }; + // TODO(phase-b): mutate nested versions['versions'][name]['aliases'] + todo!("append alias->getPrettyVersion() to versions['versions'][name]['aliases']"); + if (package.as_any() as &dyn Any).downcast_ref::<dyn RootPackageInterface>().is_some() { + // TODO(phase-b): same mutation on versions['root']['aliases'] + todo!("append alias->getPrettyVersion() to versions['root']['aliases']"); + } + } + + if let Some(PhpMixed::Array(versions_map)) = versions.get_mut("versions") { + // TODO(phase-b): ksort signature mismatch on nested IndexMap; cast appropriately + ksort(versions_map); + } + ksort(&mut versions); + + if let Some(PhpMixed::Array(versions_map)) = versions.get_mut("versions") { + for (_name, version) in versions_map.iter_mut() { + if let PhpMixed::Array(version_map) = version.as_mut() { + for key in ["aliases", "replaced", "provided"] { + if let Some(PhpMixed::List(list)) = version_map.get_mut(key) { + // PHP: sort($versions['versions'][$name][$key], SORT_NATURAL); + sort_with_flags(list, SORT_NATURAL); + } + } + } + } + } + + Ok(versions) + } + + /// @param array<string, string> $installPaths + /// @param array<string, int> $devPackages + /// @return array{pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev_requirement: bool} + fn dump_installed_package( + &self, + package: &dyn PackageInterface, + install_paths: &IndexMap<String, Option<String>>, + repo_dir: &str, + dev_packages: &PhpMixed, + ) -> IndexMap<String, PhpMixed> { + let mut reference: Option<String> = None; + if let Some(install_src) = package.get_installation_source() { + reference = if install_src == "source" { + package.get_source_reference().map(String::from) + } else { + package.get_dist_reference().map(String::from) + }; + } + if reference.is_none() { + // PHP: ($package->getSourceReference() ?: $package->getDistReference()) ?: null; + let source = package.get_source_reference().unwrap_or(""); + let dist = package.get_dist_reference().unwrap_or(""); + let combined = if !source.is_empty() { + source.to_string() + } else { + dist.to_string() + }; + reference = if combined.is_empty() { + None + } else { + Some(combined) + }; + } + + let install_path = if (package.as_any() as &dyn Any).downcast_ref::<dyn RootPackageInterface>().is_some() { + let to = self.filesystem.normalize_path( + &realpath(&Platform::get_cwd(false).unwrap_or_default()) + .unwrap_or_default(), + ); + Some(self.filesystem.find_shortest_path(repo_dir, &to, true)) + } else { + install_paths + .get(package.get_name()) + .cloned() + .flatten() + }; + + let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); + data.insert( + "pretty_version".to_string(), + PhpMixed::String(package.get_pretty_version().to_string()), + ); + data.insert( + "version".to_string(), + PhpMixed::String(package.get_version().to_string()), + ); + data.insert( + "reference".to_string(), + match reference { + Some(s) => PhpMixed::String(s), + None => PhpMixed::Null, + }, + ); + data.insert( + "type".to_string(), + PhpMixed::String(package.get_type().to_string()), + ); + data.insert( + "install_path".to_string(), + match install_path { + Some(s) => PhpMixed::String(s), + None => PhpMixed::Null, + }, + ); + data.insert("aliases".to_string(), PhpMixed::List(vec![])); + data.insert( + "dev_requirement".to_string(), + PhpMixed::Bool( + dev_packages + .as_array() + .map(|m| m.contains_key(package.get_name())) + .unwrap_or(false), + ), + ); + + data + } + + /// @param array<string, string> $installPaths + /// @param array<string, int> $devPackages + /// @return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + fn dump_root_package( + &self, + package: &dyn RootPackageInterface, + install_paths: &IndexMap<String, Option<String>>, + dev_mode: bool, + repo_dir: &str, + dev_packages: &PhpMixed, + ) -> IndexMap<String, PhpMixed> { + let data = + // TODO(phase-b): RootPackageInterface trait bound — pass as &dyn PackageInterface + self.dump_installed_package(todo!("package as &dyn PackageInterface"), install_paths, repo_dir, dev_packages); + + let mut result: IndexMap<String, PhpMixed> = IndexMap::new(); + result.insert( + "name".to_string(), + PhpMixed::String(package.get_name().to_string()), + ); + result.insert( + "pretty_version".to_string(), + data.get("pretty_version").cloned().unwrap_or(PhpMixed::Null), + ); + result.insert( + "version".to_string(), + data.get("version").cloned().unwrap_or(PhpMixed::Null), + ); + result.insert( + "reference".to_string(), + data.get("reference").cloned().unwrap_or(PhpMixed::Null), + ); + result.insert( + "type".to_string(), + data.get("type").cloned().unwrap_or(PhpMixed::Null), + ); + result.insert( + "install_path".to_string(), + data.get("install_path").cloned().unwrap_or(PhpMixed::Null), + ); + result.insert( + "aliases".to_string(), + data.get("aliases").cloned().unwrap_or(PhpMixed::List(vec![])), + ); + result.insert("dev".to_string(), PhpMixed::Bool(dev_mode)); + + result + } +} |
