aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core
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 /crates/mozart-core
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.
Diffstat (limited to 'crates/mozart-core')
-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
5 files changed, 264 insertions, 38 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());
}