aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-10 01:18:44 +0900
committernsfisis <nsfisis@gmail.com>2026-05-10 01:18:44 +0900
commit4caf72463de598ea9b6454f3b7b7332dd0071318 (patch)
tree62f99c8a48e5b21d75ab70bfd374eb67215542c9
parent8cc1ba8a02c0318b65658f1634de378c780392b9 (diff)
downloadphp-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.rs6
-rw-r--r--crates/mozart-core/src/package.rs228
-rw-r--r--crates/mozart-core/src/repository/browse_repos.rs36
-rw-r--r--crates/mozart-core/src/repository/lockfile.rs24
-rw-r--r--crates/mozart-core/src/repository_utils.rs8
-rw-r--r--crates/mozart/src/commands/browse.rs3
-rw-r--r--crates/mozart/src/commands/bump.rs16
-rw-r--r--crates/mozart/src/commands/diagnose.rs7
-rw-r--r--crates/mozart/src/commands/exec.rs3
-rw-r--r--crates/mozart/src/commands/install.rs45
-rw-r--r--crates/mozart/src/commands/licenses.rs28
-rw-r--r--crates/mozart/src/commands/validate.rs4
-rw-r--r--crates/mozart/src/composer.rs18
-rw-r--r--crates/mozart/src/factory.rs11
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"),
);
}