aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/packagist.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 10:09:58 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 10:09:58 +0900
commit0b5d333083f1317391338d3aa67b1290e93922cc (patch)
tree3842907fc1b2ac64c925b477b4daf33f529a5869 /crates/mozart/src/packagist.rs
parent999fc4157cf631f967a5adedeccb83ae6d0cb0f8 (diff)
downloadphp-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.rs141
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");
+ }
+}