diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:09:58 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:09:58 +0900 |
| commit | 0b5d333083f1317391338d3aa67b1290e93922cc (patch) | |
| tree | 3842907fc1b2ac64c925b477b4daf33f529a5869 /crates/mozart/src/packagist.rs | |
| parent | 999fc4157cf631f967a5adedeccb83ae6d0cb0f8 (diff) | |
| download | php-mozart-0b5d333083f1317391338d3aa67b1290e93922cc.tar.gz php-mozart-0b5d333083f1317391338d3aa67b1290e93922cc.tar.zst php-mozart-0b5d333083f1317391338d3aa67b1290e93922cc.zip | |
feat(require): implement require command with Packagist version resolution
Add the require command that updates composer.json with new package
dependencies. When no version constraint is specified, the best version
is resolved from the Packagist p2 API based on minimum-stability.
Includes packagist API client, version comparison/stability detection,
and RawPackageData deserialization support for roundtrip editing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/packagist.rs')
| -rw-r--r-- | crates/mozart/src/packagist.rs | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/crates/mozart/src/packagist.rs b/crates/mozart/src/packagist.rs new file mode 100644 index 0000000..912ec60 --- /dev/null +++ b/crates/mozart/src/packagist.rs @@ -0,0 +1,141 @@ +use serde::Deserialize; +use std::collections::BTreeMap; + +#[derive(Debug, Clone, Deserialize)] +pub struct PackagistDist { + #[serde(rename = "type")] + pub dist_type: String, + pub url: String, + pub reference: Option<String>, + pub shasum: Option<String>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackagistSource { + #[serde(rename = "type")] + pub source_type: String, + pub url: String, + pub reference: Option<String>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackagistVersion { + pub version: String, + pub version_normalized: String, + #[serde(default)] + pub require: BTreeMap<String, String>, + pub dist: Option<PackagistDist>, + pub source: Option<PackagistSource>, +} + +/// Parse a Packagist p2 API JSON response. +/// +/// The response format is: `{"packages": {"vendor/package": [...]}}`. +pub fn parse_p2_response(json: &str, package_name: &str) -> anyhow::Result<Vec<PackagistVersion>> { + #[derive(Deserialize)] + struct P2Response { + packages: BTreeMap<String, Vec<PackagistVersion>>, + } + + let response: P2Response = serde_json::from_str(json)?; + response + .packages + .into_iter() + .find(|(key, _)| key == package_name) + .map(|(_, versions)| versions) + .ok_or_else(|| anyhow::anyhow!("Package \"{package_name}\" not found in response")) +} + +/// Fetch package version metadata from the Packagist p2 API. +pub fn fetch_package_versions(package_name: &str) -> anyhow::Result<Vec<PackagistVersion>> { + let url = format!("https://repo.packagist.org/p2/{package_name}.json"); + let response = reqwest::blocking::get(&url)?; + + if !response.status().is_success() { + anyhow::bail!( + "Failed to fetch package \"{package_name}\" from Packagist (HTTP {})", + response.status() + ); + } + + let body = response.text()?; + parse_p2_response(&body, package_name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_p2_response_basic() { + let json = r#"{ + "packages": { + "monolog/monolog": [ + { + "version": "3.8.0", + "version_normalized": "3.8.0.0", + "require": {"php": ">=8.1"}, + "dist": { + "type": "zip", + "url": "https://example.com/monolog-3.8.0.zip", + "reference": "abc123", + "shasum": "" + }, + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "abc123" + } + }, + { + "version": "3.7.0", + "version_normalized": "3.7.0.0", + "require": {"php": ">=8.1"} + } + ] + } + }"#; + + let versions = parse_p2_response(json, "monolog/monolog").unwrap(); + assert_eq!(versions.len(), 2); + assert_eq!(versions[0].version, "3.8.0"); + assert_eq!(versions[0].version_normalized, "3.8.0.0"); + assert_eq!(versions[0].require.get("php").unwrap(), ">=8.1"); + assert!(versions[0].dist.is_some()); + assert!(versions[0].source.is_some()); + assert_eq!(versions[1].version, "3.7.0"); + assert!(versions[1].dist.is_none()); + } + + #[test] + fn parse_p2_response_not_found() { + let json = r#"{"packages": {"other/pkg": []}}"#; + let result = parse_p2_response(json, "monolog/monolog"); + assert!(result.is_err()); + } + + #[test] + fn parse_p2_response_with_dev_version() { + let json = r#"{ + "packages": { + "test/pkg": [ + { + "version": "dev-master", + "version_normalized": "dev-master", + "require": {} + }, + { + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "require": {} + } + ] + } + }"#; + + let versions = parse_p2_response(json, "test/pkg").unwrap(); + assert_eq!(versions.len(), 2); + assert_eq!(versions[0].version, "dev-master"); + assert_eq!(versions[1].version, "1.0.0"); + } +} |
