From 0b5d333083f1317391338d3aa67b1290e93922cc Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 21 Feb 2026 10:09:58 +0900 Subject: 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 --- crates/mozart/src/packagist.rs | 141 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 crates/mozart/src/packagist.rs (limited to 'crates/mozart/src/packagist.rs') 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, + pub shasum: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackagistSource { + #[serde(rename = "type")] + pub source_type: String, + pub url: String, + pub reference: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackagistVersion { + pub version: String, + pub version_normalized: String, + #[serde(default)] + pub require: BTreeMap, + pub dist: Option, + pub source: Option, +} + +/// 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> { + #[derive(Deserialize)] + struct P2Response { + packages: BTreeMap>, + } + + 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> { + 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"); + } +} -- cgit v1.3.1