aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/package
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/package')
-rw-r--r--crates/shirabe/src/package/alias_package.rs81
-rw-r--r--crates/shirabe/src/package/archiver/archivable_files_finder.rs18
-rw-r--r--crates/shirabe/src/package/archiver/archive_manager.rs27
-rw-r--r--crates/shirabe/src/package/archiver/base_exclude_filter.rs4
-rw-r--r--crates/shirabe/src/package/archiver/git_exclude_filter.rs7
-rw-r--r--crates/shirabe/src/package/archiver/phar_archiver.rs14
-rw-r--r--crates/shirabe/src/package/archiver/zip_archiver.rs8
-rw-r--r--crates/shirabe/src/package/base_package.rs74
-rw-r--r--crates/shirabe/src/package/comparer/comparer.rs2
-rw-r--r--crates/shirabe/src/package/complete_alias_package.rs15
-rw-r--r--crates/shirabe/src/package/complete_package.rs10
-rw-r--r--crates/shirabe/src/package/complete_package_interface.rs4
-rw-r--r--crates/shirabe/src/package/dumper/array_dumper.rs22
-rw-r--r--crates/shirabe/src/package/link.rs14
-rw-r--r--crates/shirabe/src/package/loader/array_loader.rs2
-rw-r--r--crates/shirabe/src/package/loader/invalid_package_exception.rs11
-rw-r--r--crates/shirabe/src/package/loader/json_loader.rs11
-rw-r--r--crates/shirabe/src/package/loader/validating_array_loader.rs5
-rw-r--r--crates/shirabe/src/package/locker.rs5
-rw-r--r--crates/shirabe/src/package/package.rs26
-rw-r--r--crates/shirabe/src/package/package_interface.rs40
-rw-r--r--crates/shirabe/src/package/root_alias_package.rs10
-rw-r--r--crates/shirabe/src/package/root_package.rs25
-rw-r--r--crates/shirabe/src/package/root_package_interface.rs14
-rw-r--r--crates/shirabe/src/package/version/stability_filter.rs4
-rw-r--r--crates/shirabe/src/package/version/version_bumper.rs12
-rw-r--r--crates/shirabe/src/package/version/version_parser.rs22
27 files changed, 309 insertions, 178 deletions
diff --git a/crates/shirabe/src/package/alias_package.rs b/crates/shirabe/src/package/alias_package.rs
index cd2e5f1..e115362 100644
--- a/crates/shirabe/src/package/alias_package.rs
+++ b/crates/shirabe/src/package/alias_package.rs
@@ -38,12 +38,12 @@ pub struct AliasPackage {
pub(crate) requires: IndexMap<String, Link>,
/// @var Link[]
pub(crate) dev_requires: IndexMap<String, Link>,
- /// @var Link[]
- pub(crate) conflicts: Vec<Link>,
- /// @var Link[]
- pub(crate) provides: Vec<Link>,
- /// @var Link[]
- pub(crate) replaces: Vec<Link>,
+ /// @var array<string, Link>
+ pub(crate) conflicts: IndexMap<String, Link>,
+ /// @var array<string, Link>
+ pub(crate) provides: IndexMap<String, Link>,
+ /// @var array<string, Link>
+ pub(crate) replaces: IndexMap<String, Link>,
}
impl AliasPackage {
@@ -69,9 +69,9 @@ impl AliasPackage {
alias_of,
requires: IndexMap::new(),
dev_requires: IndexMap::new(),
- conflicts: vec![],
- provides: vec![],
- replaces: vec![],
+ conflicts: IndexMap::new(),
+ provides: IndexMap::new(),
+ replaces: IndexMap::new(),
};
for r#type in Link::types() {
@@ -101,9 +101,24 @@ impl AliasPackage {
.map(|l| (l.get_target().to_string(), l))
.collect();
}
- Link::TYPE_PROVIDE => this.provides = replaced,
- Link::TYPE_CONFLICT => this.conflicts = replaced,
- Link::TYPE_REPLACE => this.replaces = replaced,
+ Link::TYPE_PROVIDE => {
+ this.provides = replaced
+ .into_iter()
+ .map(|l| (l.get_target().to_string(), l))
+ .collect()
+ }
+ Link::TYPE_CONFLICT => {
+ this.conflicts = replaced
+ .into_iter()
+ .map(|l| (l.get_target().to_string(), l))
+ .collect()
+ }
+ Link::TYPE_REPLACE => {
+ this.replaces = replaced
+ .into_iter()
+ .map(|l| (l.get_target().to_string(), l))
+ .collect()
+ }
_ => {}
}
}
@@ -202,7 +217,7 @@ impl std::fmt::Display for AliasPackage {
write!(
f,
"{} ({}alias of {})",
- self.inner,
+ self.alias_of,
if self.root_package_alias { "root " } else { "" },
self.alias_of.get_version(),
)
@@ -210,24 +225,28 @@ impl std::fmt::Display for AliasPackage {
}
impl PackageInterface for AliasPackage {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
fn get_name(&self) -> &str {
- self.inner.get_name()
+ self.alias_of.get_name()
}
fn get_pretty_name(&self) -> &str {
- self.inner.get_pretty_name()
+ self.alias_of.get_pretty_name()
}
fn get_names(&self, provides: bool) -> Vec<String> {
- self.inner.get_names(provides)
+ self.alias_of.get_names(provides)
}
fn set_id(&mut self, id: i64) {
- self.inner.set_id(id);
+ self.alias_of.set_id(id);
}
fn get_id(&self) -> i64 {
- self.inner.get_id()
+ self.alias_of.get_id()
}
fn is_dev(&self) -> bool {
@@ -251,20 +270,20 @@ impl PackageInterface for AliasPackage {
}
/// @inheritDoc
- /// @return array<string|int, Link>
- fn get_conflicts(&self) -> Vec<Link> {
+ /// @return array<string, Link>
+ fn get_conflicts(&self) -> IndexMap<String, Link> {
self.conflicts.clone()
}
/// @inheritDoc
- /// @return array<string|int, Link>
- fn get_provides(&self) -> Vec<Link> {
+ /// @return array<string, Link>
+ fn get_provides(&self) -> IndexMap<String, Link> {
self.provides.clone()
}
/// @inheritDoc
- /// @return array<string|int, Link>
- fn get_replaces(&self) -> Vec<Link> {
+ /// @return array<string, Link>
+ fn get_replaces(&self) -> IndexMap<String, Link> {
self.replaces.clone()
}
@@ -410,25 +429,25 @@ impl PackageInterface for AliasPackage {
fn get_full_pretty_version(&self, truncate: bool, display_mode: i64) -> String {
// TODO(phase-b): BasePackage.get_full_pretty_version returns Result; bridge here
- self.inner
+ self.alias_of
.get_full_pretty_version(truncate, display_mode)
.unwrap_or_default()
}
fn get_unique_name(&self) -> String {
- self.inner.get_unique_name()
+ self.alias_of.get_unique_name()
}
fn get_pretty_string(&self) -> String {
- self.inner.get_pretty_string()
+ self.alias_of.get_pretty_string()
}
fn set_repository(&mut self, repository: Box<dyn RepositoryInterface>) -> anyhow::Result<()> {
- self.inner.set_repository(repository)
+ self.alias_of.set_repository(repository)
}
fn get_repository(&self) -> Option<&dyn RepositoryInterface> {
- self.inner.get_repository()
+ self.alias_of.get_repository()
}
}
@@ -469,10 +488,6 @@ impl BasePackage for AliasPackage {
todo!()
}
- fn as_any(&self) -> &dyn std::any::Any {
- todo!()
- }
-
fn clone_box(&self) -> Box<dyn BasePackage> {
todo!()
}
diff --git a/crates/shirabe/src/package/archiver/archivable_files_finder.rs b/crates/shirabe/src/package/archiver/archivable_files_finder.rs
index 35ec36f..72ffabf 100644
--- a/crates/shirabe/src/package/archiver/archivable_files_finder.rs
+++ b/crates/shirabe/src/package/archiver/archivable_files_finder.rs
@@ -23,7 +23,7 @@ impl std::fmt::Debug for ArchivableFilesFinder {
impl ArchivableFilesFinder {
pub fn new(sources: &str, excludes: Vec<String>, ignore_filters: bool) -> anyhow::Result<Self> {
- let fs = Filesystem::new();
+ let fs = Filesystem::new(None);
let sources_real_path = realpath(sources);
if sources_real_path.is_none() {
@@ -39,8 +39,8 @@ impl ArchivableFilesFinder {
vec![]
} else {
vec![
- Box::new(GitExcludeFilter::new(&sources)),
- Box::new(ComposerExcludeFilter::new(&sources, excludes)),
+ Box::new(GitExcludeFilter::new(sources.clone())),
+ Box::new(ComposerExcludeFilter::new(sources.clone(), excludes)),
]
};
@@ -61,7 +61,8 @@ impl ArchivableFilesFinder {
&format!("^{}", preg_quote(&sources_clone, Some('#'))),
"",
&fs.normalize_path(&realpath),
- );
+ )
+ .unwrap_or_default();
let mut exclude = false;
for f in &filters {
@@ -72,13 +73,14 @@ impl ArchivableFilesFinder {
};
finder
- .in_dir(&sources)
- .filter(Box::new(filter))
+ .r#in(&sources)
+ // TODO(phase-b): symfony Finder filter takes Box<dyn Fn(&SplFileInfo) -> bool>; signature not yet wired
.ignore_vcs(true)
.ignore_dot_files(false)
.sort_by_name();
+ let _ = filter;
- let inner_iter = finder.get_iterator();
+ let inner_iter: Box<dyn Iterator<Item = SplFileInfo>> = Box::new(finder.get_iterator());
Ok(Self { finder, inner_iter })
}
@@ -88,7 +90,7 @@ impl ArchivableFilesFinder {
return true;
}
- let path = current.to_string();
+ let path = current.get_pathname();
match std::fs::read_dir(&path) {
Ok(mut iter) => iter.next().is_none(),
Err(_) => false,
diff --git a/crates/shirabe/src/package/archiver/archive_manager.rs b/crates/shirabe/src/package/archiver/archive_manager.rs
index 3f7bfe0..73b0d84 100644
--- a/crates/shirabe/src/package/archiver/archive_manager.rs
+++ b/crates/shirabe/src/package/archiver/archive_manager.rs
@@ -19,8 +19,8 @@ use crate::util::r#loop::Loop;
use crate::util::sync_helper::SyncHelper;
pub struct ArchiveManager {
- pub(crate) download_manager: DownloadManager,
- pub(crate) r#loop: Loop,
+ pub(crate) download_manager: std::rc::Rc<std::cell::RefCell<DownloadManager>>,
+ pub(crate) r#loop: std::rc::Rc<std::cell::RefCell<Loop>>,
pub(crate) archivers: Vec<Box<dyn ArchiverInterface>>,
pub(crate) overwrite_files: bool,
}
@@ -34,7 +34,10 @@ impl std::fmt::Debug for ArchiveManager {
}
impl ArchiveManager {
- pub fn new(download_manager: DownloadManager, r#loop: Loop) -> Self {
+ pub fn new(
+ download_manager: std::rc::Rc<std::cell::RefCell<DownloadManager>>,
+ r#loop: std::rc::Rc<std::cell::RefCell<Loop>>,
+ ) -> Self {
Self {
download_manager,
r#loop,
@@ -144,7 +147,7 @@ impl ArchiveManager {
}
};
- let filesystem = Filesystem::new();
+ let filesystem = Filesystem::new(None);
let is_root = package.as_any().is::<dyn RootPackageInterface>();
let source_path: String;
@@ -158,10 +161,16 @@ impl ArchiveManager {
filesystem.ensure_directory_exists(&source_path)?;
let download_result = (|| -> anyhow::Result<()> {
- let promise = self.download_manager.download(package, &source_path)?;
- SyncHelper::r#await(&self.r#loop, promise)?;
- let promise = self.download_manager.install(package, &source_path)?;
- SyncHelper::r#await(&self.r#loop, promise)?;
+ let promise =
+ self.download_manager
+ .borrow()
+ .download(package, &source_path, None)?;
+ SyncHelper::r#await(&self.r#loop, Some(promise))?;
+ let promise = self
+ .download_manager
+ .borrow()
+ .install(package, &source_path)?;
+ SyncHelper::r#await(&self.r#loop, Some(promise))?;
Ok(())
})();
@@ -172,7 +181,7 @@ impl ArchiveManager {
let composer_json_path = format!("{}/composer.json", source_path);
if file_exists(&composer_json_path) {
- let json_file = JsonFile::new(composer_json_path, None, None);
+ let json_file = JsonFile::new(composer_json_path, None, None)?;
let json_data = json_file.read()?;
if let Some(archive) = json_data.get("archive") {
if let Some(name) = archive.get("name").and_then(|v| v.as_str()) {
diff --git a/crates/shirabe/src/package/archiver/base_exclude_filter.rs b/crates/shirabe/src/package/archiver/base_exclude_filter.rs
index c7d2557..1a716d0 100644
--- a/crates/shirabe/src/package/archiver/base_exclude_filter.rs
+++ b/crates/shirabe/src/package/archiver/base_exclude_filter.rs
@@ -59,7 +59,7 @@ impl BaseExcludeFilterBase {
let rule = rule.trim_matches('/');
- let glob_regex = Glob::to_regex(rule);
+ let glob_regex = Glob::to_regex(rule, true, true);
let rule_regex = &glob_regex[2..glob_regex.len() - 2];
(
@@ -143,7 +143,7 @@ pub trait BaseExcludeFilter {
let rule = rule.trim_matches('/');
// remove delimiters as well as caret (^) and dollar sign ($) from the regex
- let glob_regex = Glob::to_regex(rule);
+ let glob_regex = Glob::to_regex(rule, true, true);
let rule_regex = &glob_regex[2..glob_regex.len() - 2];
(
diff --git a/crates/shirabe/src/package/archiver/git_exclude_filter.rs b/crates/shirabe/src/package/archiver/git_exclude_filter.rs
index dddad12..1a0ec4d 100644
--- a/crates/shirabe/src/package/archiver/git_exclude_filter.rs
+++ b/crates/shirabe/src/package/archiver/git_exclude_filter.rs
@@ -34,14 +34,17 @@ impl GitExcludeFilter {
}
fn parse_git_attributes_line_static(line: &str) -> Option<(String, bool, bool)> {
- let parts = Preg::split(r"\s+", line);
+ let parts = Preg::split(r"\s+", line).unwrap_or_default();
if parts.len() == 2 && parts[1] == "export-ignore" {
return Some(BaseExcludeFilterBase::generate_pattern(&parts[0]));
}
if parts.len() == 2 && parts[1] == "-export-ignore" {
- return BaseExcludeFilterBase::generate_pattern(&format!("!{}", parts[0]));
+ return Some(BaseExcludeFilterBase::generate_pattern(&format!(
+ "!{}",
+ parts[0]
+ )));
}
None
diff --git a/crates/shirabe/src/package/archiver/phar_archiver.rs b/crates/shirabe/src/package/archiver/phar_archiver.rs
index 2e9d96e..7b5142b 100644
--- a/crates/shirabe/src/package/archiver/phar_archiver.rs
+++ b/crates/shirabe/src/package/archiver/phar_archiver.rs
@@ -29,6 +29,12 @@ fn compress_formats() -> IndexMap<&'static str, i64> {
#[derive(Debug)]
pub struct PharArchiver;
+impl PharArchiver {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
impl ArchiverInterface for PharArchiver {
fn archive(
&self,
@@ -46,6 +52,7 @@ impl ArchiverInterface for PharArchiver {
unlink(&target);
}
+ let target_outer = target.clone();
let inner = (|| -> anyhow::Result<String> {
let pos = strrpos(&target, &format).unwrap_or(target.len());
let filename = target[..pos.saturating_sub(1)].to_string();
@@ -63,7 +70,10 @@ impl ArchiverInterface for PharArchiver {
*formats.get(format.as_str()).unwrap_or(&Phar::TAR),
);
let files = ArchivableFilesFinder::new(&sources, excludes, ignore_filters)?;
- let mut files_only = ArchivableFilesFilter::new(files);
+ // TODO(phase-b): unify iterator types (ArchivableFilesFinder yields SplFileInfo,
+ // ArchivableFilesFilter expects PathBuf).
+ let mut files_only =
+ ArchivableFilesFilter::new(Box::new(files.map(|f| f.get_pathname().into())));
phar.build_from_iterator(&mut files_only, &sources);
files_only.add_empty_dir(&phar, &sources);
@@ -137,7 +147,7 @@ impl ArchiverInterface for PharArchiver {
inner.map_err(|e| {
let message = format!(
"Could not create archive '{}' from '{}': {}",
- target, sources, e
+ target_outer, sources, e
);
anyhow::anyhow!(RuntimeException { message, code: 0 })
})
diff --git a/crates/shirabe/src/package/archiver/zip_archiver.rs b/crates/shirabe/src/package/archiver/zip_archiver.rs
index ef5b40a..471352f 100644
--- a/crates/shirabe/src/package/archiver/zip_archiver.rs
+++ b/crates/shirabe/src/package/archiver/zip_archiver.rs
@@ -13,6 +13,10 @@ use shirabe_php_shim::{
pub struct ZipArchiver;
impl ZipArchiver {
+ pub fn new() -> Self {
+ Self
+ }
+
fn formats() -> IndexMap<String, bool> {
let mut map = IndexMap::new();
map.insert("zip".to_string(), true);
@@ -33,7 +37,7 @@ impl ArchiverInterface for ZipArchiver {
excludes: Vec<String>,
ignore_filters: bool,
) -> anyhow::Result<String> {
- let fs = Filesystem::new();
+ let fs = Filesystem::new(None);
let sources_realpath = realpath(&sources);
let sources = if let Some(p) = sources_realpath {
p
@@ -47,7 +51,7 @@ impl ArchiverInterface for ZipArchiver {
let files = ArchivableFilesFinder::new(&sources, excludes, ignore_filters)?;
for file in files {
let filepath = file.get_pathname();
- let mut relative_path = file.get_relative_pathname();
+ let mut relative_path = file.get_relative_path_name();
if Platform::is_windows() {
relative_path = shirabe_php_shim::strtr(&relative_path, "\\", "/");
diff --git a/crates/shirabe/src/package/base_package.rs b/crates/shirabe/src/package/base_package.rs
index 2e50c44..fb906d1 100644
--- a/crates/shirabe/src/package/base_package.rs
+++ b/crates/shirabe/src/package/base_package.rs
@@ -83,62 +83,20 @@ pub trait BasePackage: PackageInterface + std::fmt::Display {
fn set_repository_box(&mut self, repository: Box<dyn RepositoryInterface>);
fn take_repository(&mut self) -> Option<Box<dyn RepositoryInterface>>;
- fn as_any(&self) -> &dyn std::any::Any;
fn clone_box(&self) -> Box<dyn BasePackage>;
- fn get_name(&self) -> &str {
- self.name()
- }
-
- fn get_pretty_name(&self) -> &str {
- self.pretty_name()
- }
-
- fn get_names(&self, provides: bool) -> Vec<String> {
- let mut names: IndexMap<String, bool> = IndexMap::new();
- names.insert(self.get_name().to_string(), true);
-
- if provides {
- for link in self.get_provides().values() {
- names.insert(link.get_target().to_string(), true);
- }
- }
-
- for link in self.get_replaces().values() {
- names.insert(link.get_target().to_string(), true);
- }
-
- names.into_keys().collect()
- }
-
- fn set_id(&mut self, id: i64) {
- *self.id_mut() = id;
- }
+ // as_alias_package / as_complete_package_interface inherited from PackageInterface.
- fn get_id(&self) -> i64 {
- self.id()
+ fn as_alias_package_mut(&mut self) -> Option<&mut crate::package::alias_package::AliasPackage> {
+ None
}
- fn set_repository(&mut self, repository: Box<dyn RepositoryInterface>) -> anyhow::Result<()> {
- if let Some(existing) = self.repository_opt() {
- // TODO(phase-b): proper reference identity check before raising error
- return Err(anyhow::anyhow!(LogicException {
- message: format!(
- "Package \"{}\" cannot be added to repository \"{}\" as it is already in repository \"{}\".",
- self.get_pretty_name(),
- repository.get_repo_name(),
- existing.get_repo_name(),
- ),
- code: 0,
- }));
- }
- self.set_repository_box(repository);
- Ok(())
- }
+ // get_name / get_pretty_name / get_names live on PackageInterface; the BasePackage
+ // duplicates were causing ambiguity at every call site (`pkg.get_name()` with
+ // pkg: &dyn BasePackage). Concrete impls already forward to name()/pretty_name().
- fn get_repository(&self) -> Option<&dyn RepositoryInterface> {
- self.repository_opt()
- }
+ // set_id, get_id, get_repository, get_unique_name, set_repository are inherited
+ // from PackageInterface; do not redeclare here to avoid trait-method ambiguity.
fn is_platform(&self) -> bool {
self.repository_opt()
@@ -146,24 +104,18 @@ pub trait BasePackage: PackageInterface + std::fmt::Display {
.is_some()
}
- fn get_unique_name(&self) -> String {
- format!("{}-{}", self.get_name(), self.get_version())
- }
-
fn equals(&self, _package: &dyn PackageInterface) -> bool {
// TODO(phase-b): implement via reference identity (requires Rc/Arc)
// PHP uses === which is reference equality; unwraps AliasPackage on both sides
todo!("equals requires reference identity which needs Rc/Arc")
}
- fn get_pretty_string(&self) -> String {
- format!("{} {}", self.get_pretty_name(), self.get_pretty_version())
- }
+ // get_pretty_string is inherited from PackageInterface.
fn get_full_pretty_version(&self, truncate: bool, display_mode: i64) -> anyhow::Result<String> {
- const DISPLAY_SOURCE_REF_IF_DEV: i64 = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV;
- const DISPLAY_SOURCE_REF: i64 = PackageInterface::DISPLAY_SOURCE_REF;
- const DISPLAY_DIST_REF: i64 = PackageInterface::DISPLAY_DIST_REF;
+ const DISPLAY_SOURCE_REF_IF_DEV: i64 = <dyn PackageInterface>::DISPLAY_SOURCE_REF_IF_DEV;
+ const DISPLAY_SOURCE_REF: i64 = <dyn PackageInterface>::DISPLAY_SOURCE_REF;
+ const DISPLAY_DIST_REF: i64 = <dyn PackageInterface>::DISPLAY_DIST_REF;
if display_mode == DISPLAY_SOURCE_REF_IF_DEV
&& (!self.is_dev()
@@ -207,7 +159,7 @@ pub trait BasePackage: PackageInterface + std::fmt::Display {
fn get_stability_priority(&self) -> i64 {
*STABILITIES
.get(self.get_stability())
- .unwrap_or(&Self::STABILITY_STABLE)
+ .unwrap_or(&STABILITY_STABLE)
}
fn php_clone(&mut self) {
diff --git a/crates/shirabe/src/package/comparer/comparer.rs b/crates/shirabe/src/package/comparer/comparer.rs
index 0e0d295..8f406a5 100644
--- a/crates/shirabe/src/package/comparer/comparer.rs
+++ b/crates/shirabe/src/package/comparer/comparer.rs
@@ -65,7 +65,7 @@ impl Comparer {
let mut source: IndexMap<String, IndexMap<String, Option<String>>> = IndexMap::new();
let mut destination: IndexMap<String, IndexMap<String, Option<String>>> = IndexMap::new();
self.changed = IndexMap::new();
- let current_directory = Platform::get_cwd();
+ let current_directory = Platform::get_cwd(false).unwrap_or_default();
shirabe_php_shim::chdir(&self.source);
if !Self::do_tree(".", &mut source) {
return;
diff --git a/crates/shirabe/src/package/complete_alias_package.rs b/crates/shirabe/src/package/complete_alias_package.rs
index 134ed3a..d187eea 100644
--- a/crates/shirabe/src/package/complete_alias_package.rs
+++ b/crates/shirabe/src/package/complete_alias_package.rs
@@ -13,7 +13,13 @@ pub struct CompleteAliasPackage {
impl CompleteAliasPackage {
pub fn new(alias_of: CompletePackage, version: String, pretty_version: String) -> Self {
- let inner = AliasPackage::new(alias_of.clone(), version, pretty_version);
+ // TODO(phase-b): alias_of is a PHP class (shared semantics); cloning is wrong.
+ // Use a dummy BasePackage placeholder until the field is migrated to Rc<CompletePackage>.
+ let inner = AliasPackage::new(
+ todo!("share CompletePackage via Rc"),
+ version,
+ pretty_version,
+ );
Self { inner, alias_of }
}
@@ -61,7 +67,8 @@ impl CompleteAliasPackage {
}
pub fn set_description(&mut self, description: Option<String>) {
- self.alias_of.set_description(description);
+ self.alias_of
+ .set_description(description.unwrap_or_default());
}
pub fn get_homepage(&self) -> Option<&str> {
@@ -69,7 +76,7 @@ impl CompleteAliasPackage {
}
pub fn set_homepage(&mut self, homepage: Option<String>) {
- self.alias_of.set_homepage(homepage);
+ self.alias_of.set_homepage(homepage.unwrap_or_default());
}
pub fn get_authors(&self) -> Vec<indexmap::IndexMap<String, String>> {
@@ -116,7 +123,7 @@ impl CompleteAliasPackage {
}
pub fn set_archive_name(&mut self, name: Option<String>) {
- self.alias_of.set_archive_name(name);
+ self.alias_of.set_archive_name(name.unwrap_or_default());
}
pub fn get_archive_excludes(&self) -> Vec<String> {
diff --git a/crates/shirabe/src/package/complete_package.rs b/crates/shirabe/src/package/complete_package.rs
index a3db372..27c49c6 100644
--- a/crates/shirabe/src/package/complete_package.rs
+++ b/crates/shirabe/src/package/complete_package.rs
@@ -133,6 +133,10 @@ impl CompletePackageInterface for CompletePackage {
}
impl PackageInterface for CompletePackage {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
fn get_name(&self) -> &str {
todo!()
}
@@ -253,15 +257,15 @@ impl PackageInterface for CompletePackage {
todo!()
}
- fn get_conflicts(&self) -> Vec<super::link::Link> {
+ fn get_conflicts(&self) -> IndexMap<String, super::link::Link> {
todo!()
}
- fn get_provides(&self) -> Vec<super::link::Link> {
+ fn get_provides(&self) -> IndexMap<String, super::link::Link> {
todo!()
}
- fn get_replaces(&self) -> Vec<super::link::Link> {
+ fn get_replaces(&self) -> IndexMap<String, super::link::Link> {
todo!()
}
diff --git a/crates/shirabe/src/package/complete_package_interface.rs b/crates/shirabe/src/package/complete_package_interface.rs
index 31780a7..d1120ac 100644
--- a/crates/shirabe/src/package/complete_package_interface.rs
+++ b/crates/shirabe/src/package/complete_package_interface.rs
@@ -55,4 +55,8 @@ pub trait CompletePackageInterface: PackageInterface {
fn get_archive_excludes(&self) -> Vec<String>;
fn set_archive_excludes(&mut self, excludes: Vec<String>);
+
+ fn as_package_interface(&self) -> &dyn crate::package::package_interface::PackageInterface {
+ todo!()
+ }
}
diff --git a/crates/shirabe/src/package/dumper/array_dumper.rs b/crates/shirabe/src/package/dumper/array_dumper.rs
index 02f1092..d81fd6c 100644
--- a/crates/shirabe/src/package/dumper/array_dumper.rs
+++ b/crates/shirabe/src/package/dumper/array_dumper.rs
@@ -131,8 +131,10 @@ impl ArrayDumper {
}
// corresponds to: foreach (BasePackage::$supportedLinkTypes as $type => $opts) { $links = $package->{'get'.ucfirst($opts['method'])}(); ... }
- for (type_name, method_name) in BasePackage::supported_link_types() {
- let links = package.get_links_by_method(&method_name);
+ for (type_name, method_name) in <dyn BasePackage>::supported_link_types() {
+ // TODO(phase-b): PackageInterface needs get_links_by_method to mimic PHP magic call
+ let links: Vec<crate::package::link::Link> = Vec::new();
+ let _ = (&method_name, package);
if links.is_empty() {
continue;
}
@@ -186,10 +188,9 @@ impl ArrayDumper {
),
);
}
- if let Some(pkg_type) = package.get_type() {
- if !pkg_type.is_empty() {
- data.insert("type".to_string(), PhpMixed::String(pkg_type.to_string()));
- }
+ let pkg_type = package.get_type();
+ if !pkg_type.is_empty() {
+ data.insert("type".to_string(), PhpMixed::String(pkg_type.to_string()));
}
let extra = package.get_extra();
if !extra.is_empty() {
@@ -247,10 +248,15 @@ impl ArrayDumper {
);
}
let php_ext = package.get_php_ext();
- if !php_ext.is_empty() {
+ if let Some(php_ext) = php_ext.as_ref().filter(|m| !m.is_empty()) {
data.insert(
"php-ext".to_string(),
- PhpMixed::Array(php_ext.into_iter().map(|(k, v)| (k, Box::new(v))).collect()),
+ PhpMixed::Array(
+ php_ext
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect(),
+ ),
);
}
diff --git a/crates/shirabe/src/package/link.rs b/crates/shirabe/src/package/link.rs
index 2233eb1..e41d431 100644
--- a/crates/shirabe/src/package/link.rs
+++ b/crates/shirabe/src/package/link.rs
@@ -13,6 +13,20 @@ pub struct Link {
pub(crate) pretty_constraint: Option<String>,
}
+impl Clone for Link {
+ fn clone(&self) -> Self {
+ // TODO(phase-b): Link is a PHP class; this clone is a shallow placeholder until
+ // Link is shared via Rc<Link>.
+ Self {
+ source: self.source.clone(),
+ target: self.target.clone(),
+ constraint: self.constraint.clone_box(),
+ description: self.description.clone(),
+ pretty_constraint: self.pretty_constraint.clone(),
+ }
+ }
+}
+
impl std::fmt::Debug for Link {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Link")
diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs
index 28e13af..275d718 100644
--- a/crates/shirabe/src/package/loader/array_loader.rs
+++ b/crates/shirabe/src/package/loader/array_loader.rs
@@ -588,7 +588,7 @@ impl ArrayLoader {
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);
+ 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;
diff --git a/crates/shirabe/src/package/loader/invalid_package_exception.rs b/crates/shirabe/src/package/loader/invalid_package_exception.rs
index 96725ba..764b31f 100644
--- a/crates/shirabe/src/package/loader/invalid_package_exception.rs
+++ b/crates/shirabe/src/package/loader/invalid_package_exception.rs
@@ -1,5 +1,6 @@
//! ref: composer/src/Composer/Package/Loader/InvalidPackageException.php
+use indexmap::IndexMap;
use shirabe_php_shim::{Exception, PhpMixed};
#[derive(Debug)]
@@ -7,11 +8,15 @@ pub struct InvalidPackageException {
inner: Exception,
errors: Vec<String>,
warnings: Vec<String>,
- data: Vec<PhpMixed>,
+ data: IndexMap<String, PhpMixed>,
}
impl InvalidPackageException {
- pub fn new(errors: Vec<String>, warnings: Vec<String>, data: Vec<PhpMixed>) -> Self {
+ pub fn new(
+ errors: Vec<String>,
+ warnings: Vec<String>,
+ data: IndexMap<String, PhpMixed>,
+ ) -> Self {
let message = format!(
"Invalid package information: \n{}",
errors
@@ -29,7 +34,7 @@ impl InvalidPackageException {
}
}
- pub fn get_data(&self) -> &[PhpMixed] {
+ pub fn get_data(&self) -> &IndexMap<String, PhpMixed> {
&self.data
}
diff --git a/crates/shirabe/src/package/loader/json_loader.rs b/crates/shirabe/src/package/loader/json_loader.rs
index 978bc28..cb3cbeb 100644
--- a/crates/shirabe/src/package/loader/json_loader.rs
+++ b/crates/shirabe/src/package/loader/json_loader.rs
@@ -22,13 +22,16 @@ impl JsonLoader {
pub fn load(&self, json: JsonLoaderInput) -> Result<Box<dyn BasePackage>> {
let config = match json {
- JsonLoaderInput::File(json_file) => json_file.read()?,
+ JsonLoaderInput::File(mut json_file) => json_file.read()?,
JsonLoaderInput::String(ref s) if Path::new(s).exists() => {
- JsonFile::parse_json(&std::fs::read_to_string(s)?, Some(s))?
+ let contents = std::fs::read_to_string(s)?;
+ JsonFile::parse_json(Some(&contents), Some(s))?
}
- JsonLoaderInput::String(ref s) => JsonFile::parse_json(s, None)?,
+ JsonLoaderInput::String(ref s) => JsonFile::parse_json(Some(s), None)?,
};
- self.loader.load(config, None)
+ // TODO(phase-b): JsonFile::parse_json returns PhpMixed; loader::load expects IndexMap
+ let _ = config;
+ self.loader.load(indexmap::IndexMap::new(), None)
}
}
diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs
index 3ba9212..5009a0d 100644
--- a/crates/shirabe/src/package/loader/validating_array_loader.rs
+++ b/crates/shirabe/src/package/loader/validating_array_loader.rs
@@ -1288,7 +1288,10 @@ impl ValidatingArrayLoader {
return Err(anyhow::anyhow!(InvalidPackageException::new(
self.errors.clone(),
self.warnings.clone(),
- config.values().map(|v| (**v).clone()).collect(),
+ config
+ .iter()
+ .map(|(k, v)| (k.clone(), (**v).clone()))
+ .collect(),
)));
}
diff --git a/crates/shirabe/src/package/locker.rs b/crates/shirabe/src/package/locker.rs
index 5950437..397af08 100644
--- a/crates/shirabe/src/package/locker.rs
+++ b/crates/shirabe/src/package/locker.rs
@@ -134,7 +134,6 @@ impl Locker {
.collect(),
),
0,
- JsonFile::INDENT_DEFAULT,
),
))
}
@@ -616,13 +615,13 @@ impl Locker {
} else {
self.virtual_file_written = true;
self.lock_data_cache = Some(JsonFile::parse_json(
- &JsonFile::encode(
+ Some(&JsonFile::encode_with_indent(
&PhpMixed::Array(lock.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,
JsonFile::INDENT_DEFAULT,
- ),
+ )),
None,
)?);
}
diff --git a/crates/shirabe/src/package/package.rs b/crates/shirabe/src/package/package.rs
index b8b6770..74286a0 100644
--- a/crates/shirabe/src/package/package.rs
+++ b/crates/shirabe/src/package/package.rs
@@ -71,7 +71,10 @@ impl Package {
let stability = VersionParser::parse_stability(&version).to_string();
let dev = stability == "dev";
Self {
- inner: BasePackage::new(name),
+ id: -1,
+ name: name.to_lowercase(),
+ pretty_name: name,
+ repository: None,
r#type: None,
target_dir: None,
installation_source: None,
@@ -458,7 +461,7 @@ impl Package {
let url = if url_type == "dist" && strpos(url, "%").is_some() {
ComposerMirror::process_url(
url,
- &self.inner.name,
+ &self.name,
&self.version,
r#ref.unwrap_or(""),
r#type.unwrap_or(""),
@@ -474,7 +477,7 @@ impl Package {
let mirror_url = if url_type == "dist" {
ComposerMirror::process_url(
&mirror.url,
- &self.inner.name,
+ &self.name,
&self.version,
r#ref.unwrap_or(""),
r#type.unwrap_or(""),
@@ -483,14 +486,14 @@ impl Package {
} else if url_type == "source" && r#type == Some("git") {
ComposerMirror::process_git_url(
&mirror.url,
- &self.inner.name,
+ &self.name,
&url,
r#type.unwrap_or(""),
)
} else if url_type == "source" && r#type == Some("hg") {
ComposerMirror::process_hg_url(
&mirror.url,
- &self.inner.name,
+ &self.name,
&url,
r#type.unwrap_or(""),
)
@@ -568,10 +571,6 @@ impl BasePackage for Package {
todo!()
}
- fn as_any(&self) -> &dyn std::any::Any {
- todo!()
- }
-
fn clone_box(&self) -> Box<dyn BasePackage> {
todo!()
}
@@ -584,6 +583,9 @@ impl std::fmt::Display for Package {
}
impl PackageInterface for Package {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
fn get_name(&self) -> &str {
todo!()
}
@@ -674,13 +676,13 @@ impl PackageInterface for Package {
fn get_requires(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_conflicts(&self) -> Vec<Link> {
+ fn get_conflicts(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_provides(&self) -> Vec<Link> {
+ fn get_provides(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_replaces(&self) -> Vec<Link> {
+ fn get_replaces(&self) -> IndexMap<String, Link> {
todo!()
}
fn get_dev_requires(&self) -> IndexMap<String, Link> {
diff --git a/crates/shirabe/src/package/package_interface.rs b/crates/shirabe/src/package/package_interface.rs
index 6a07556..c6ccce8 100644
--- a/crates/shirabe/src/package/package_interface.rs
+++ b/crates/shirabe/src/package/package_interface.rs
@@ -14,7 +14,9 @@ use crate::repository::repository_interface::RepositoryInterface;
/// @phpstan-type AutoloadRules array{psr-0?: array<string, string|string[]>, psr-4?: array<string, string|string[]>, classmap?: list<string>, files?: list<string>, exclude-from-classmap?: list<string>}
/// @phpstan-type DevAutoloadRules array{psr-0?: array<string, string|string[]>, psr-4?: array<string, string|string[]>, classmap?: list<string>, files?: list<string>}
/// @phpstan-type PhpExtConfig array{extension-name?: string, priority?: int, support-zts?: bool, support-nts?: bool, build-path?: string|null, download-url-method?: string|list<string>, os-families?: non-empty-list<non-empty-string>, os-families-exclude?: non-empty-list<non-empty-string>, configure-options?: list<array{name: string, description?: string}>}
-pub trait PackageInterface: std::fmt::Display {
+pub trait PackageInterface: std::fmt::Display + std::fmt::Debug {
+ fn as_any(&self) -> &dyn std::any::Any;
+
/// Returns the package's name without version info, thus not a unique identifier
///
/// @return string package name
@@ -170,20 +172,20 @@ pub trait PackageInterface: std::fmt::Display {
/// Returns a set of links to packages which must not be installed at the
/// same time as this package
///
- /// @return Link[] An array of package links defining conflicting packages
- fn get_conflicts(&self) -> Vec<Link>;
+ /// @return array<string, Link> A map of package links defining conflicting packages
+ fn get_conflicts(&self) -> IndexMap<String, Link>;
/// Returns a set of links to virtual packages that are provided through
/// this package
///
- /// @return Link[] An array of package links defining provided packages
- fn get_provides(&self) -> Vec<Link>;
+ /// @return array<string, Link> A map of package links defining provided packages
+ fn get_provides(&self) -> IndexMap<String, Link>;
/// Returns a set of links to packages which can alternatively be
/// satisfied by installing this package
///
- /// @return Link[] An array of package links defining replaced packages
- fn get_replaces(&self) -> Vec<Link>;
+ /// @return array<string, Link> A map of package links defining replaced packages
+ fn get_replaces(&self) -> IndexMap<String, Link>;
/// Returns a set of links to packages which are required to develop
/// this package. These are installed if in dev mode.
@@ -275,6 +277,30 @@ pub trait PackageInterface: std::fmt::Display {
/// Set dist and source references and update dist URL for ones that contain a reference
fn set_source_dist_references(&mut self, reference: &str);
+
+ // clone_box was moved to BasePackage with a Box<dyn BasePackage> return type;
+ // exposing it here too caused trait-method ambiguity at every BasePackage call site.
+ // Callers holding `&dyn PackageInterface` (rather than `&dyn BasePackage`) can use
+ // `clone_package_box` instead.
+ fn clone_package_box(&self) -> Box<dyn PackageInterface> {
+ todo!()
+ }
+
+ fn as_alias_package(&self) -> Option<&crate::package::alias_package::AliasPackage> {
+ None
+ }
+
+ fn as_complete_package_interface(
+ &self,
+ ) -> Option<&dyn crate::package::complete_package_interface::CompletePackageInterface> {
+ None
+ }
+
+ fn as_complete_package(
+ &self,
+ ) -> Option<&dyn crate::package::complete_package_interface::CompletePackageInterface> {
+ None
+ }
}
impl dyn PackageInterface {
diff --git a/crates/shirabe/src/package/root_alias_package.rs b/crates/shirabe/src/package/root_alias_package.rs
index 703c1f9..cb458f3 100644
--- a/crates/shirabe/src/package/root_alias_package.rs
+++ b/crates/shirabe/src/package/root_alias_package.rs
@@ -231,6 +231,10 @@ impl std::fmt::Display for RootAliasPackage {
}
impl PackageInterface for RootAliasPackage {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
fn get_name(&self) -> &str {
todo!()
}
@@ -321,13 +325,13 @@ impl PackageInterface for RootAliasPackage {
fn get_requires(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_conflicts(&self) -> Vec<Link> {
+ fn get_conflicts(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_provides(&self) -> Vec<Link> {
+ fn get_provides(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_replaces(&self) -> Vec<Link> {
+ fn get_replaces(&self) -> IndexMap<String, Link> {
todo!()
}
fn get_dev_requires(&self) -> IndexMap<String, Link> {
diff --git a/crates/shirabe/src/package/root_package.rs b/crates/shirabe/src/package/root_package.rs
index 2557c8a..af8d1f5 100644
--- a/crates/shirabe/src/package/root_package.rs
+++ b/crates/shirabe/src/package/root_package.rs
@@ -24,6 +24,21 @@ pub struct RootPackage {
impl RootPackage {
pub const DEFAULT_PRETTY_VERSION: &'static str = "1.0.0+no-version-set";
+
+ pub fn new(name: String, version: String, pretty_version: String) -> Self {
+ // TODO(phase-b): CompletePackage::new signature is not yet pinned down
+ let inner: CompletePackage = todo!();
+ let _ = (name, version, pretty_version);
+ Self {
+ inner,
+ minimum_stability: "stable".to_string(),
+ prefer_stable: false,
+ stability_flags: IndexMap::new(),
+ config: IndexMap::new(),
+ references: IndexMap::new(),
+ aliases: Vec::new(),
+ }
+ }
}
impl RootPackageInterface for RootPackage {
@@ -221,6 +236,10 @@ impl std::fmt::Display for RootPackage {
}
impl PackageInterface for RootPackage {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
fn get_name(&self) -> &str {
todo!()
}
@@ -311,13 +330,13 @@ impl PackageInterface for RootPackage {
fn get_requires(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_conflicts(&self) -> Vec<Link> {
+ fn get_conflicts(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_provides(&self) -> Vec<Link> {
+ fn get_provides(&self) -> IndexMap<String, Link> {
todo!()
}
- fn get_replaces(&self) -> Vec<Link> {
+ fn get_replaces(&self) -> IndexMap<String, Link> {
todo!()
}
fn get_dev_requires(&self) -> IndexMap<String, Link> {
diff --git a/crates/shirabe/src/package/root_package_interface.rs b/crates/shirabe/src/package/root_package_interface.rs
index a8634af..a053e28 100644
--- a/crates/shirabe/src/package/root_package_interface.rs
+++ b/crates/shirabe/src/package/root_package_interface.rs
@@ -3,6 +3,8 @@
use indexmap::IndexMap;
use shirabe_php_shim::PhpMixed;
+use crate::package::package_interface::PackageInterface;
+
use crate::package::complete_package_interface::CompletePackageInterface;
use crate::package::link::Link;
@@ -48,4 +50,16 @@ pub trait RootPackageInterface: CompletePackageInterface {
fn set_suggests(&mut self, suggests: IndexMap<String, String>);
fn set_extra(&mut self, extra: IndexMap<String, PhpMixed>);
+
+ fn clone_as_package_interface(&self) -> Box<dyn PackageInterface> {
+ todo!()
+ }
+
+ fn clone_box(&self) -> Box<dyn RootPackageInterface> {
+ todo!()
+ }
+
+ fn as_package_interface(&self) -> &dyn PackageInterface {
+ todo!()
+ }
}
diff --git a/crates/shirabe/src/package/version/stability_filter.rs b/crates/shirabe/src/package/version/stability_filter.rs
index d08492c..5053e9d 100644
--- a/crates/shirabe/src/package/version/stability_filter.rs
+++ b/crates/shirabe/src/package/version/stability_filter.rs
@@ -1,6 +1,6 @@
//! ref: composer/src/Composer/Package/Version/StabilityFilter.php
-use crate::package::base_package::BasePackage;
+use crate::package::base_package::STABILITIES;
use indexmap::IndexMap;
pub struct StabilityFilter;
@@ -15,7 +15,7 @@ impl StabilityFilter {
for name in names {
// allow if package matches the package-specific stability flag
if let Some(&flag) = stability_flags.get(name) {
- if let Some(&stability_value) = BasePackage::STABILITIES.get(stability) {
+ if let Some(&stability_value) = STABILITIES.get(stability) {
if stability_value <= flag {
return true;
}
diff --git a/crates/shirabe/src/package/version/version_bumper.rs b/crates/shirabe/src/package/version/version_bumper.rs
index 5911458..8963fd6 100644
--- a/crates/shirabe/src/package/version/version_bumper.rs
+++ b/crates/shirabe/src/package/version/version_bumper.rs
@@ -26,11 +26,15 @@ impl VersionBumper {
return Ok(pretty_constraint);
}
- let mut version = package.get_version();
- if package.get_version().starts_with("dev-") {
- let loader = ArrayLoader::new(&parser);
+ let mut version = package.get_version().to_string();
+ if version.starts_with("dev-") {
+ // TODO(phase-b): ArrayLoader::new takes Option<VersionParser> by value; pass None until
+ // VersionParser sharing is reconciled.
+ let _ = &parser;
+ let loader = ArrayLoader::new(None, false);
let dumper = ArrayDumper::new();
- let extra = loader.get_branch_alias(dumper.dump(package));
+ let dumped = dumper.dump(package);
+ let extra = loader.get_branch_alias(&dumped)?;
if extra.is_none() || extra.as_deref() == Some(VersionParser::DEFAULT_BRANCH_ALIAS) {
return Ok(pretty_constraint);
diff --git a/crates/shirabe/src/package/version/version_parser.rs b/crates/shirabe/src/package/version/version_parser.rs
index 6bb1004..6dbf425 100644
--- a/crates/shirabe/src/package/version/version_parser.rs
+++ b/crates/shirabe/src/package/version/version_parser.rs
@@ -74,6 +74,28 @@ impl VersionParser {
Ok(result)
}
+ pub fn new() -> Self {
+ Self {
+ inner: SemverVersionParser,
+ }
+ }
+
+ pub fn normalize(&self, version: &str, full_version: Option<&str>) -> anyhow::Result<String> {
+ self.inner.normalize(version, full_version)
+ }
+
+ pub fn normalize_stability(stability: &str) -> anyhow::Result<String> {
+ SemverVersionParser::normalize_stability(stability)
+ }
+
+ pub fn normalize_branch(&self, name: &str) -> anyhow::Result<String> {
+ self.inner.normalize_branch(name)
+ }
+
+ pub fn parse_stability(version: &str) -> String {
+ SemverVersionParser::parse_stability(version)
+ }
+
pub fn is_upgrade(normalized_from: &str, normalized_to: &str) -> anyhow::Result<bool> {
if normalized_from == normalized_to {
return Ok(true);