diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 01:18:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 01:18:44 +0900 |
| commit | 4caf72463de598ea9b6454f3b7b7332dd0071318 (patch) | |
| tree | 62f99c8a48e5b21d75ab70bfd374eb67215542c9 | |
| parent | 8cc1ba8a02c0318b65658f1634de378c780392b9 (diff) | |
| download | php-mozart-4caf72463de598ea9b6454f3b7b7332dd0071318.tar.gz php-mozart-4caf72463de598ea9b6454f3b7b7332dd0071318.tar.zst php-mozart-4caf72463de598ea9b6454f3b7b7332dd0071318.zip | |
refactor(package): port RootPackageLoader into RootPackageData::from_raw
Mirrors Composer\Package\Loader\RootPackageLoader::load(): converts the
parsed RawPackageData into fully typed RootPackageData with Link objects,
defaulted fields, and trait-based accessors. Composer::package() now
returns RootPackageData instead of RawPackageData, eliminating the
pre-normalised JSON workaround noted in the previous comment.
| -rw-r--r-- | crates/mozart-core/src/autoload.rs | 6 | ||||
| -rw-r--r-- | crates/mozart-core/src/package.rs | 228 | ||||
| -rw-r--r-- | crates/mozart-core/src/repository/browse_repos.rs | 36 | ||||
| -rw-r--r-- | crates/mozart-core/src/repository/lockfile.rs | 24 | ||||
| -rw-r--r-- | crates/mozart-core/src/repository_utils.rs | 8 | ||||
| -rw-r--r-- | crates/mozart/src/commands/browse.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/bump.rs | 16 | ||||
| -rw-r--r-- | crates/mozart/src/commands/diagnose.rs | 7 | ||||
| -rw-r--r-- | crates/mozart/src/commands/exec.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 45 | ||||
| -rw-r--r-- | crates/mozart/src/commands/licenses.rs | 28 | ||||
| -rw-r--r-- | crates/mozart/src/commands/validate.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/composer.rs | 18 | ||||
| -rw-r--r-- | crates/mozart/src/factory.rs | 11 |
14 files changed, 336 insertions, 101 deletions
diff --git a/crates/mozart-core/src/autoload.rs b/crates/mozart-core/src/autoload.rs index 0d15900..7e8ead6 100644 --- a/crates/mozart-core/src/autoload.rs +++ b/crates/mozart-core/src/autoload.rs @@ -3,7 +3,7 @@ use crate::composer::{ PlatformRequirementFilter, }; use crate::config::Config; -use crate::package::RawPackageData; +use crate::package::RootPackageData; use crate::repository::installed::InstalledPackages; use crate::repository::lockfile::LockedPackage; use indexmap::IndexSet; @@ -188,7 +188,7 @@ pub trait AutoloadGeneratorExt { options: &AutoloadDumpOptions, config: &Config, local_repo: &LocalRepository, - root_package: &RawPackageData, + root_package: &RootPackageData, installation_manager: &InstallationManager, target_dir: &str, scan_psr_packages: bool, @@ -204,7 +204,7 @@ impl AutoloadGeneratorExt for AutoloadGenerator { options: &AutoloadDumpOptions, config: &Config, _local_repo: &LocalRepository, - _root_package: &RawPackageData, + _root_package: &RootPackageData, installation_manager: &InstallationManager, _target_dir: &str, scan_psr_packages: bool, diff --git a/crates/mozart-core/src/package.rs b/crates/mozart-core/src/package.rs index 64974fd..0d5c482 100644 --- a/crates/mozart-core/src/package.rs +++ b/crates/mozart-core/src/package.rs @@ -686,6 +686,234 @@ pub fn read_from_file(path: &Path) -> anyhow::Result<RawPackageData> { Ok(data) } +impl RootPackageData { + /// Mirrors `Composer\Package\Loader\RootPackageLoader::load()`: + /// converts the raw, untyped `RawPackageData` (the parsed-JSON form) + /// into the fully typed `RootPackageData`, applying field defaults and + /// constructing [`Link`] objects from the raw constraint strings. + pub fn from_raw(raw: RawPackageData) -> Self { + fn make_links( + source: &str, + deps: BTreeMap<String, String>, + description: &str, + ) -> BTreeMap<String, Link> { + deps.into_iter() + .map(|(target, constraint)| { + let normalized = target.to_lowercase(); + let link = Link { + source: source.to_string(), + target: normalized.clone(), + constraint, + pretty_constraint: None, + description: description.to_string(), + }; + (normalized, link) + }) + .collect() + } + + let pretty_name = raw.name.clone(); + let name = raw.name.to_lowercase(); + + let pretty_version = raw + .version + .unwrap_or_else(|| "1.0.0+no-version-set".to_string()); + let version = pretty_version.clone(); + + let package_type = raw + .package_type + .map(|t| t.to_lowercase()) + .unwrap_or_else(|| "library".to_string()); + + let requires = make_links(&name, raw.require, "requires"); + let dev_requires = make_links(&name, raw.require_dev, "requires (for development)"); + let conflicts = make_links(&name, raw.conflict, "conflicts"); + let provides = make_links(&name, raw.provide, "provides"); + let replaces = make_links(&name, raw.replace, "replaces"); + + let autoload = raw + .autoload + .map(|a| AutoloadRules { + psr4: a + .psr4 + .into_iter() + .map(|(ns, path)| (ns, vec![path])) + .collect(), + ..Default::default() + }) + .unwrap_or_default(); + + let extra: BTreeMap<String, serde_json::Value> = raw + .extra_fields + .get("extra") + .and_then(|v| v.as_object()) + .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + + let license = raw.license.into_iter().collect(); + + let authors = raw + .authors + .into_iter() + .map(|a| Author { + name: Some(a.name), + email: a.email, + homepage: None, + role: None, + }) + .collect(); + + let repositories = raw + .repositories + .into_iter() + .filter_map(|r| serde_json::to_value(r).ok()) + .collect(); + + let keywords = raw + .extra_fields + .get("keywords") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect() + }) + .unwrap_or_default(); + + let scripts: BTreeMap<String, Vec<String>> = raw + .extra_fields + .get("scripts") + .and_then(|v| v.as_object()) + .map(|obj| { + obj.iter() + .map(|(k, v)| { + let handlers = match v { + serde_json::Value::String(s) => vec![s.clone()], + serde_json::Value::Array(arr) => arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(), + _ => vec![], + }; + (k.clone(), handlers) + }) + .collect() + }) + .unwrap_or_default(); + + let support = raw + .extra_fields + .get("support") + .and_then(|v| v.as_object()) + .map(|obj| { + let get_str = |key: &str| obj.get(key).and_then(|v| v.as_str()).map(String::from); + Support { + email: get_str("email"), + issues: get_str("issues"), + forum: get_str("forum"), + wiki: get_str("wiki"), + source: get_str("source"), + docs: get_str("docs"), + irc: get_str("irc"), + chat: get_str("chat"), + rss: get_str("rss"), + security: get_str("security"), + } + }) + .unwrap_or_default(); + + let funding = raw + .extra_fields + .get("funding") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_object()) + .map(|obj| Funding { + url: obj.get("url").and_then(|v| v.as_str()).map(String::from), + funding_type: obj.get("type").and_then(|v| v.as_str()).map(String::from), + }) + .collect() + }) + .unwrap_or_default(); + + let config: BTreeMap<String, serde_json::Value> = raw + .extra_fields + .get("config") + .and_then(|v| v.as_object()) + .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + + let prefer_stable = raw + .extra_fields + .get("prefer-stable") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let minimum_stability = + Stability::parse(raw.minimum_stability.as_deref().unwrap_or("stable")); + + let package_data = PackageData { + name, + pretty_name, + version, + pretty_version, + package_type, + target_dir: None, + source_type: None, + source_url: None, + source_reference: None, + dist_type: None, + dist_url: None, + dist_reference: None, + dist_sha1_checksum: None, + release_date: None, + extra, + binaries: raw.bin, + dev: false, + stability: Stability::Stable, + notification_url: None, + requires, + conflicts, + provides, + replaces, + dev_requires, + suggests: BTreeMap::new(), + autoload, + dev_autoload: AutoloadRules::default(), + is_default_branch: false, + }; + + let complete_data = CompletePackageData { + package: package_data, + description: raw.description, + homepage: raw.homepage, + license, + keywords, + authors, + scripts, + support, + funding, + repositories, + abandoned: None, + archive_name: None, + archive_excludes: Vec::new(), + }; + + RootPackageData { + complete: complete_data, + minimum_stability, + prefer_stable, + stability_flags: BTreeMap::new(), + config, + references: BTreeMap::new(), + aliases: Vec::new(), + } + } +} + pub fn to_json_pretty(value: &impl Serialize) -> serde_json::Result<String> { let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut buf = Vec::new(); diff --git a/crates/mozart-core/src/repository/browse_repos.rs b/crates/mozart-core/src/repository/browse_repos.rs index d54465f..9cb8a9f 100644 --- a/crates/mozart-core/src/repository/browse_repos.rs +++ b/crates/mozart-core/src/repository/browse_repos.rs @@ -8,7 +8,8 @@ //! `CompletePackageInterface` (`getSupport()['source']`, //! `getSourceUrl()`, `getHomepage()`). -use super::super::package::RawPackageData; +use super::super::package::Package; +use super::super::package::{CompletePackage, RootPackageData}; use super::cache::Cache; use super::installed::{InstalledPackageEntry, InstalledPackages}; use super::lockfile::LockedPackage; @@ -77,19 +78,11 @@ impl From<&PackagistVersion> for CompletePackageView { } } -/// `RawPackageData` lacks a typed `support` field — the root package's -/// `support` block lives inside `extra_fields` because the schema is not -/// yet ported. Read it manually here. -pub fn view_from_raw(pkg: &RawPackageData) -> CompletePackageView { +pub fn view_from_root(pkg: &RootPackageData) -> CompletePackageView { CompletePackageView { - support_source: pkg - .extra_fields - .get("support") - .and_then(|s| s.get("source")) - .and_then(|s| s.as_str()) - .map(str::to_string), + support_source: pkg.support().source.clone(), source_url: None, - homepage: pkg.homepage.clone(), + homepage: pkg.homepage().map(str::to_string), } } @@ -99,9 +92,9 @@ pub fn view_from_raw(pkg: &RawPackageData) -> CompletePackageView { pub enum BrowseRepo { /// Stand-in for `Composer\Repository\RootPackageRepository` — /// a one-package array containing the root composer.json. - /// Boxed because `RawPackageData` is much larger than the other + /// Boxed because `RootPackageData` is much larger than the other /// variants (clippy::large_enum_variant). - Root(Box<RawPackageData>), + Root(Box<RootPackageData>), /// Stand-in for `RepositoryManager::getLocalRepository()` — /// the installed.json view of `vendor/`. Installed(InstalledPackages), @@ -116,8 +109,8 @@ impl BrowseRepo { pub async fn find_packages(&self, name: &str) -> anyhow::Result<Vec<CompletePackageView>> { match self { BrowseRepo::Root(pkg) => { - if pkg.name.eq_ignore_ascii_case(name) { - Ok(vec![view_from_raw(pkg)]) + if pkg.name().eq_ignore_ascii_case(name) { + Ok(vec![view_from_root(pkg)]) } else { Ok(Vec::new()) } @@ -148,7 +141,7 @@ impl BrowseRepos { /// them from `Composer` (when composer.json is present) or skip /// them entirely (the `defaultReposWithDefaultManager` fallback). pub fn new( - root: Option<RawPackageData>, + root: Option<RootPackageData>, installed: Option<InstalledPackages>, packagist_cache: Cache, ) -> Self { @@ -173,6 +166,7 @@ impl BrowseRepos { #[cfg(test)] mod tests { use super::*; + use crate::package::RawPackageData; use std::collections::BTreeMap; fn locked( @@ -267,14 +261,15 @@ mod tests { } #[test] - fn view_from_raw_reads_support_via_extra_fields() { + fn view_from_root_reads_support_and_homepage() { let mut raw = RawPackageData::new("vendor/root".to_string()); raw.homepage = Some("https://vendor.example.com".to_string()); raw.extra_fields.insert( "support".to_string(), serde_json::json!({"source": "https://github.com/vendor/root"}), ); - let view = view_from_raw(&raw); + let root = RootPackageData::from_raw(raw); + let view = view_from_root(&root); assert_eq!( view.support_source.as_deref(), Some("https://github.com/vendor/root") @@ -286,7 +281,8 @@ mod tests { #[tokio::test] async fn root_repo_matches_case_insensitively() { let raw = RawPackageData::new("Vendor/Root".to_string()); - let repo = BrowseRepo::Root(Box::new(raw)); + let root = RootPackageData::from_raw(raw); + let repo = BrowseRepo::Root(Box::new(root)); assert_eq!(repo.find_packages("vendor/root").await.unwrap().len(), 1); assert_eq!(repo.find_packages("other/pkg").await.unwrap().len(), 0); } diff --git a/crates/mozart-core/src/repository/lockfile.rs b/crates/mozart-core/src/repository/lockfile.rs index 4c41bbb..6aae4df 100644 --- a/crates/mozart-core/src/repository/lockfile.rs +++ b/crates/mozart-core/src/repository/lockfile.rs @@ -2,7 +2,8 @@ use super::packagist::{PackagistDist, PackagistSource, PackagistVersion}; use super::repository::RepositorySet; use super::resolver::ResolvedPackage; use crate::installer::HasSuggests; -use crate::package::{RawPackageData, to_json_pretty}; +use crate::package::Package; +use crate::package::{Link, RawPackageData, to_json_pretty}; use indexmap::IndexMap; use indexmap::IndexSet; use serde::{Deserialize, Serialize}; @@ -260,7 +261,7 @@ impl LockFile { /// Mirrors `Composer\Package\Locker::getMissingRequirementInfo()`. pub fn get_missing_requirement_info( &self, - root: &crate::package::RawPackageData, + root: &crate::package::RootPackageData, include_dev: bool, ) -> Vec<String> { let mut messages = Vec::new(); @@ -280,7 +281,7 @@ impl LockFile { } check_requirement_set( - &root.require, + root.requires(), "Required", &base_pool, &mut messages, @@ -288,7 +289,7 @@ impl LockFile { ); if include_dev { check_requirement_set( - &root.require_dev, + root.dev_requires(), "Required (in require-dev)", &dev_pool, &mut messages, @@ -395,16 +396,17 @@ pub fn locked_package_branch_aliases(pkg: &LockedPackage) -> Vec<LockAlias> { } fn check_requirement_set( - requires: &BTreeMap<String, String>, + requires: &BTreeMap<String, Link>, description: &str, pool: &[LockedSearchEntry], messages: &mut Vec<String>, any_missing: &mut bool, ) { - for (name, constraint_str) in requires { + for (name, link) in requires { if crate::platform::is_platform_package(name) { continue; } + let constraint_str = link.constraint.as_str(); if constraint_str.trim() == "self.version" { continue; } @@ -1931,15 +1933,15 @@ mod tests { fn root_with_require( require: &[(&str, &str)], require_dev: &[(&str, &str)], - ) -> crate::package::RawPackageData { - let mut root = crate::package::RawPackageData::new("__root__".to_string()); + ) -> crate::package::RootPackageData { + let mut raw = crate::package::RawPackageData::new("__root__".to_string()); for (k, v) in require { - root.require.insert((*k).to_string(), (*v).to_string()); + raw.require.insert((*k).to_string(), (*v).to_string()); } for (k, v) in require_dev { - root.require_dev.insert((*k).to_string(), (*v).to_string()); + raw.require_dev.insert((*k).to_string(), (*v).to_string()); } - root + crate::package::RootPackageData::from_raw(raw) } #[test] diff --git a/crates/mozart-core/src/repository_utils.rs b/crates/mozart-core/src/repository_utils.rs index ecd5dd7..b16a0d6 100644 --- a/crates/mozart-core/src/repository_utils.rs +++ b/crates/mozart-core/src/repository_utils.rs @@ -36,10 +36,10 @@ pub trait Required { /// /// The returned vector preserves the order in which packages were /// discovered, matching PHP's `$bucket[] = $candidate;` push pattern. -pub fn filter_required_packages<P>( +pub fn filter_required_packages<P, V>( packages: &[P], - requirer_requires: &std::collections::BTreeMap<String, String>, - requirer_dev_requires: Option<&std::collections::BTreeMap<String, String>>, + requirer_requires: &std::collections::BTreeMap<String, V>, + requirer_dev_requires: Option<&std::collections::BTreeMap<String, V>>, ) -> Vec<usize> where P: Required, @@ -167,7 +167,7 @@ mod tests { #[test] fn empty_requires_yields_nothing() { let packages = vec![pkg("a/a", &[]), pkg("b/b", &[])]; - let root = BTreeMap::new(); + let root: BTreeMap<String, String> = BTreeMap::new(); let kept = filter_required_packages(&packages, &root, None); assert!(kept.is_empty()); } diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs index f646577..d7c9ce2 100644 --- a/crates/mozart/src/commands/browse.rs +++ b/crates/mozart/src/commands/browse.rs @@ -4,6 +4,7 @@ use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; +use mozart_core::package::Package; use mozart_core::repository::browse_repos::{BrowseRepos, CompletePackageView}; use mozart_core::repository::cache::{Cache, build_cache_config}; use mozart_core::repository::installed::InstalledPackages; @@ -42,7 +43,7 @@ pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> working_dir.display() ) })?; - vec![composer.package().name.clone()] + vec![composer.package().name().to_string()] } else { args.packages.clone() }; diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs index ee611d1..f7c8142 100644 --- a/crates/mozart/src/commands/bump.rs +++ b/crates/mozart/src/commands/bump.rs @@ -3,6 +3,7 @@ use clap::Args; use indexmap::IndexMap; use mozart_core::composer::LocalRepository; use mozart_core::console::Console; +use mozart_core::package::{Link, Package}; use mozart_core::{console_writeln, console_writeln_error}; use std::collections::BTreeMap; use std::path::Path; @@ -128,13 +129,13 @@ pub async fn do_bump( build_locked_versions_from_local(composer.repository_manager().local_repository()) }; - let package_type = composer.package().package_type.as_deref(); - if package_type != Some("project") && !dev_only { + let package_type = composer.package().package_type(); + if package_type != "project" && !dev_only { console_writeln_error!( io, "<warning>Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.</warning>", ); - if package_type.is_none() { + if package_type == "library" { console_writeln_error!( io, "<warning>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</warning>", @@ -146,12 +147,12 @@ pub async fn do_bump( } } - let mut tasks: Vec<(&'static str, &BTreeMap<String, String>)> = Vec::new(); + let mut tasks: Vec<(&'static str, &BTreeMap<String, Link>)> = Vec::new(); if !dev_only { - tasks.push(("require", &composer.package().require)); + tasks.push(("require", composer.package().requires())); } if !no_dev_only { - tasks.push(("require-dev", &composer.package().require_dev)); + tasks.push(("require-dev", composer.package().dev_requires())); } let stripped_filter: Option<Vec<String>> = if packages_filter.is_empty() { @@ -169,7 +170,8 @@ pub async fn do_bump( let mut updates: BTreeMap<&'static str, BTreeMap<String, String>> = BTreeMap::new(); for (key, reqs) in &tasks { - for (pkg_name, constraint) in reqs.iter() { + for (pkg_name, link) in reqs.iter() { + let constraint = &link.constraint; if mozart_core::platform::is_platform_package(pkg_name) { continue; } diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs index 17a8b78..2e171e5 100644 --- a/crates/mozart/src/commands/diagnose.rs +++ b/crates/mozart/src/commands/diagnose.rs @@ -8,6 +8,7 @@ use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::factory::create_config; use mozart_core::http::HttpDownloader; +use mozart_core::package::CompletePackage; use std::borrow::Cow; use std::path::Path; @@ -430,11 +431,11 @@ pub async fn execute( // Step 13: every additional `composer`-type repo. if let Some(composer) = &composer { - for repo in composer.package().repositories.iter() { - if repo.repo_type != "composer" { + for repo in composer.package().repositories().iter() { + if repo.get("type").and_then(|v| v.as_str()) != Some("composer") { continue; } - let Some(url) = repo.url.as_deref() else { + let Some(url) = repo.get("url").and_then(|v| v.as_str()) else { continue; }; if !url.starts_with("http") { diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index 2b3c836..63c29c9 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -1,6 +1,7 @@ use crate::composer::Composer; use clap::Args; use mozart_core::console_writeln; +use mozart_core::package::Package; use std::path::{Path, PathBuf}; #[derive(Args)] @@ -125,7 +126,7 @@ fn get_binaries(composer: &Composer, bin_dir: &Path) -> Vec<(String, bool)> { let local_bins: Vec<(String, bool)> = composer .package() - .bin + .binaries() .iter() .filter_map(|e| Some(PathBuf::from(e).file_name()?.to_string_lossy().into_owned())) .map(|e| (e, true)) diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index 59d400b..4a997dd 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -2,6 +2,7 @@ use clap::Args; use indexmap::IndexSet; use mozart_core::console; use mozart_core::console_format; +use mozart_core::package::{Package, RootPackage, RootPackageData}; use mozart_core::repository::installed; use mozart_core::repository::installer_executor::{ Action, ExecuteContext, FilesystemExecutor, InstallerExecutor, PackageOperation, @@ -151,7 +152,7 @@ impl Default for InstallConfig { /// strings combined so a future "swap for SAT-verify" change is a single /// function replacement. fn verify_lock( - root: &mozart_core::package::RawPackageData, + root: &RootPackageData, lock: &lockfile::LockFile, dev_mode: bool, ignore_platform_reqs: bool, @@ -181,7 +182,7 @@ fn verify_lock( /// Returns the list of "Root composer.json requires …" diagnostic lines (one /// per failing requirement). An empty vec means everything is satisfied. fn verify_lock_platform_problems( - root: &mozart_core::package::RawPackageData, + root: &RootPackageData, lock: &lockfile::LockFile, dev_mode: bool, ignore_platform_reqs: bool, @@ -260,18 +261,16 @@ fn verify_lock_same_name_problems(lock: &lockfile::LockFile, dev_mode: bool) -> /// must bail with exit-code 2 before any package operations run. fn verify_lock_root_require_problems( lock: &lockfile::LockFile, - root: &mozart_core::package::RawPackageData, + root: &RootPackageData, dev_mode: bool, ) -> Vec<String> { use mozart_semver::{Version, VersionConstraint}; - let Some(root_version) = root.version.as_deref() else { - return Vec::new(); - }; - if root.name.is_empty() || root_version.is_empty() { + let root_version = root.pretty_version(); + if root_version == "1.0.0+no-version-set" || root.name().is_empty() || root_version.is_empty() { return Vec::new(); } - let root_name_lower = root.name.to_lowercase(); + let root_name_lower = root.name().to_string(); let Ok(parsed_root_version) = Version::parse(root_version) else { return Vec::new(); }; @@ -395,7 +394,7 @@ fn verify_lock_conflict_problems(lock: &lockfile::LockFile, dev_mode: bool) -> V /// composer.json overrides the lock on duplicate keys (matching Composer's /// "composer.json as source of truth" rule for shared platform reqs). fn combine_platform_requirements( - root: &mozart_core::package::RawPackageData, + root: &RootPackageData, lock: &lockfile::LockFile, dev_mode: bool, ) -> BTreeMap<String, String> { @@ -416,17 +415,17 @@ fn combine_platform_requirements( } } - for (name, constraint) in &root.require { + for (name, link) in root.requires() { let lower = name.to_lowercase(); if mozart_core::platform::is_platform_package(&lower) { - combined.insert(lower, constraint.clone()); + combined.insert(lower, link.constraint.clone()); } } if dev_mode { - for (name, constraint) in &root.require_dev { + for (name, link) in root.dev_requires() { let lower = name.to_lowercase(); if mozart_core::platform::is_platform_package(&lower) { - combined.insert(lower, constraint.clone()); + combined.insert(lower, link.constraint.clone()); } } } @@ -991,8 +990,9 @@ pub async fn run( )); } - let root_pkg = mozart_core::package::read_from_file(&composer_json_path)?; - root_pkg.validate_root_does_not_self_require()?; + let raw_pkg = mozart_core::package::read_from_file(&composer_json_path)?; + raw_pkg.validate_root_does_not_self_require()?; + let root_pkg = RootPackageData::from_raw(raw_pkg); let missing = lock.get_missing_requirement_info(&root_pkg, dev_mode); if !missing.is_empty() { for line in &missing { @@ -1003,9 +1003,8 @@ pub async fn run( // but proceed with what the lock already covers instead of // bailing with ERROR_LOCK_FILE_INVALID. let allow_missing = root_pkg - .extra_fields - .get("config") - .and_then(|v| v.get("allow-missing-requirements")) + .config() + .get("allow-missing-requirements") .and_then(|v| v.as_bool()) .unwrap_or(false); if !allow_missing { @@ -1432,15 +1431,15 @@ mod tests { fn root_with_require( require: &[(&str, &str)], require_dev: &[(&str, &str)], - ) -> mozart_core::package::RawPackageData { - let mut root = mozart_core::package::RawPackageData::new("__root__".to_string()); + ) -> RootPackageData { + let mut raw = mozart_core::package::RawPackageData::new("__root__".to_string()); for (k, v) in require { - root.require.insert((*k).to_string(), (*v).to_string()); + raw.require.insert((*k).to_string(), (*v).to_string()); } for (k, v) in require_dev { - root.require_dev.insert((*k).to_string(), (*v).to_string()); + raw.require_dev.insert((*k).to_string(), (*v).to_string()); } - root + RootPackageData::from_raw(raw) } fn lock_with_platform( diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs index 344a5fa..7a3847f 100644 --- a/crates/mozart/src/commands/licenses.rs +++ b/crates/mozart/src/commands/licenses.rs @@ -4,6 +4,7 @@ use indexmap::IndexMap; use mozart_core::console::Console; use mozart_core::console::hyperlink; use mozart_core::console_writeln; +use mozart_core::package::Package; use mozart_core::package_info; use mozart_core::package_info::PackageUrls; use mozart_core::package_sorter::sort_packages_alphabetically; @@ -84,21 +85,24 @@ pub async fn execute( let root = composer.package(); - // RawPackageData stores `license` as `Option<String>` only, so we - // re-parse the composer.json to also accept the array form Composer - // recognises via `RootPackageLoader`'s `(array) $config['license']` - // coercion. Track widening `RawPackageData::license` separately. + // Re-parse composer.json to handle the array form of `license` — + // `RawPackageData` only deserializes the string form, so both string + // and array values must be read from the raw JSON here. let root_licenses = read_root_licenses(&working_dir.join("composer.json"))?; - let root_pretty_name = root.name.clone(); - let root_version = root - .version - .clone() - .unwrap_or_else(|| "No version set".to_string()); + let root_pretty_name = root.name().to_string(); + let root_version = { + let v = root.pretty_version(); + if v == "1.0.0+no-version-set" { + "No version set".to_string() + } else { + v.to_string() + } + }; let mut entries = if args.locked { load_locked_entries(&working_dir, args.no_dev)? } else { - load_installed_entries(&working_dir, &root.require, args.no_dev)? + load_installed_entries(&working_dir, root.requires(), args.no_dev)? }; sort_packages_alphabetically(&mut entries, |e| e.name.as_str()); @@ -138,9 +142,9 @@ fn read_root_licenses(composer_json_path: &std::path::Path) -> anyhow::Result<Ve }) } -fn load_installed_entries( +fn load_installed_entries<V>( working_dir: &std::path::Path, - root_requires: &BTreeMap<String, String>, + root_requires: &BTreeMap<String, V>, no_dev: bool, ) -> anyhow::Result<Vec<LicenseEntry>> { let vendor_dir = working_dir.join("vendor"); diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs index 853eb2b..7595ee5 100644 --- a/crates/mozart/src/commands/validate.rs +++ b/crates/mozart/src/commands/validate.rs @@ -3,7 +3,7 @@ use clap::Args; use mozart_core::config_validator::{ValidationResult, ValidatorOptions, validate_manifest}; use mozart_core::console_format; use mozart_core::console_writeln; -use mozart_core::package::RawPackageData; +use mozart_core::package::RootPackageData; use std::path::{Path, PathBuf}; #[derive(Args)] @@ -324,7 +324,7 @@ fn validate_dependencies_vendor_walk( fn check_lock_freshness( composer_json_content: &str, composer_json_path: &Path, - root_package: Option<&RawPackageData>, + root_package: Option<&RootPackageData>, lock_errors: &mut Vec<String>, ) { let lock_path = composer_json_path diff --git a/crates/mozart/src/composer.rs b/crates/mozart/src/composer.rs index 337b053..0484344 100644 --- a/crates/mozart/src/composer.rs +++ b/crates/mozart/src/composer.rs @@ -16,7 +16,7 @@ use std::path::{Path, PathBuf}; use crate::factory::create_composer; use mozart_core::composer::{AutoloadGenerator, InstallationManager, Locker, RepositoryManager}; use mozart_core::config::Config; -use mozart_core::package::RawPackageData; +use mozart_core::package::RootPackageData; use mozart_core::repository::download_manager::DownloadManager; /// Project-level Composer state. Mirrors `Composer\PartialComposer` / @@ -27,7 +27,7 @@ use mozart_core::repository::download_manager::DownloadManager; pub struct Composer { project_dir: PathBuf, config: Config, - package: RawPackageData, + package: RootPackageData, repository_manager: RepositoryManager, installation_manager: InstallationManager, download_manager: DownloadManager, @@ -45,7 +45,7 @@ impl Composer { pub fn new( project_dir: PathBuf, config: Config, - package: RawPackageData, + package: RootPackageData, repository_manager: RepositoryManager, installation_manager: InstallationManager, download_manager: DownloadManager, @@ -111,12 +111,12 @@ impl Composer { &self.config } - /// Root package loaded from the project's `composer.json`. Mirrors - /// `Composer::getPackage()`; ideally this would return a fully - /// resolved `RootPackageInterface` equivalent, but Mozart does not - /// yet have a `RootPackageLoader` port — for now callers see the - /// raw, pre-normalised JSON shape. - pub fn package(&self) -> &RawPackageData { + /// Root package loaded from the project's `composer.json`. + /// Mirrors `Composer::getPackage()` — returns a fully typed + /// [`RootPackageData`] that implements the [`RootPackage`] trait + /// hierarchy (`Package` → `CompletePackage` → `RootPackage`), + /// equivalent to PHP's `RootPackageInterface`. + pub fn package(&self) -> &RootPackageData { &self.package } diff --git a/crates/mozart/src/factory.rs b/crates/mozart/src/factory.rs index ca46671..29faa42 100644 --- a/crates/mozart/src/factory.rs +++ b/crates/mozart/src/factory.rs @@ -16,7 +16,7 @@ use mozart_core::composer::{ }; use mozart_core::config::resolve_references; use mozart_core::factory::create_config; -use mozart_core::package::read_from_file; +use mozart_core::package::{RootPackageData, read_from_file}; use mozart_core::repository::download_manager::DownloadManager; /// Rust port of `Factory::createComposer()`. @@ -62,7 +62,7 @@ pub fn create_composer( } resolve_references(&mut config); - let package = read_from_file(composer_json)?; + let package = RootPackageData::from_raw(read_from_file(composer_json)?); // Mirrors `Factory::createComposer`'s `vendorDir` handling. The // value out of `Config::get('vendor-dir')` already had `{$...}` @@ -321,14 +321,15 @@ mod tests { r#"{"name": "acme/app", "require": {"vendor/pkg": "^1.0"}}"#, ); + use mozart_core::package::Package; let composer = Composer::require(dir.path()).unwrap(); - assert_eq!(composer.package().name, "acme/app"); + assert_eq!(composer.package().name(), "acme/app"); assert_eq!( composer .package() - .require + .requires() .get("vendor/pkg") - .map(String::as_str), + .map(|l| l.constraint.as_str()), Some("^1.0"), ); } |
