aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/package.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/package.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/package.rs')
-rw-r--r--crates/mozart/src/package.rs92
1 files changed, 84 insertions, 8 deletions
diff --git a/crates/mozart/src/package.rs b/crates/mozart/src/package.rs
index 1823918..e0e8c6c 100644
--- a/crates/mozart/src/package.rs
+++ b/crates/mozart/src/package.rs
@@ -1,4 +1,4 @@
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
@@ -440,7 +440,7 @@ delegate_complete_package!(RootPackageData => complete);
/// Used by `init` and `create-project` to write a new composer.json.
/// Unlike the typed hierarchy above, all fields live at a single level
/// and map directly to the JSON keys via serde.
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawPackageData {
pub name: String,
@@ -456,25 +456,33 @@ pub struct RawPackageData {
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
- #[serde(skip_serializing_if = "Vec::is_empty")]
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<RawAuthor>,
#[serde(rename = "minimum-stability", skip_serializing_if = "Option::is_none")]
pub minimum_stability: Option<String>,
+ #[serde(default)]
pub require: BTreeMap<String, String>,
- #[serde(rename = "require-dev", skip_serializing_if = "BTreeMap::is_empty")]
+ #[serde(
+ rename = "require-dev",
+ default,
+ skip_serializing_if = "BTreeMap::is_empty"
+ )]
pub require_dev: BTreeMap<String, String>,
- #[serde(skip_serializing_if = "Vec::is_empty")]
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub repositories: Vec<RawRepository>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autoload: Option<RawAutoload>,
+
+ #[serde(flatten)]
+ pub extra_fields: BTreeMap<String, serde_json::Value>,
}
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawAuthor {
pub name: String,
@@ -482,13 +490,13 @@ pub struct RawAuthor {
pub email: Option<String>,
}
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawAutoload {
#[serde(rename = "psr-4")]
pub psr4: BTreeMap<String, String>,
}
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawRepository {
#[serde(rename = "type")]
pub repo_type: String,
@@ -509,10 +517,17 @@ impl RawPackageData {
require_dev: BTreeMap::new(),
repositories: Vec::new(),
autoload: None,
+ extra_fields: BTreeMap::new(),
}
}
}
+pub fn read_from_file(path: &Path) -> anyhow::Result<RawPackageData> {
+ let content = fs::read_to_string(path)?;
+ let data: RawPackageData = serde_json::from_str(&content)?;
+ Ok(data)
+}
+
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();
@@ -590,6 +605,67 @@ mod tests {
}
#[test]
+ fn raw_deserialize_minimal() {
+ let json = r#"{"name": "test/pkg"}"#;
+ let raw: RawPackageData = serde_json::from_str(json).unwrap();
+ assert_eq!(raw.name, "test/pkg");
+ assert!(raw.description.is_none());
+ assert!(raw.require.is_empty());
+ assert!(raw.require_dev.is_empty());
+ assert!(raw.authors.is_empty());
+ assert!(raw.extra_fields.is_empty());
+ }
+
+ #[test]
+ fn raw_roundtrip_preserves_all_fields() {
+ let mut raw = RawPackageData::new("acme/roundtrip".to_string());
+ raw.description = Some("Test roundtrip".to_string());
+ raw.require.insert("php".to_string(), ">=8.1".to_string());
+ raw.require_dev
+ .insert("phpunit/phpunit".to_string(), "^10.0".to_string());
+
+ let json1 = to_json_pretty(&raw).unwrap();
+ let deserialized: RawPackageData = serde_json::from_str(&json1).unwrap();
+ let json2 = to_json_pretty(&deserialized).unwrap();
+ assert_eq!(json1, json2);
+ }
+
+ #[test]
+ fn raw_extra_fields_preserved() {
+ let json = r#"{
+ "name": "test/extra",
+ "require": {},
+ "scripts": {"post-install-cmd": ["echo hello"]},
+ "config": {"sort-packages": true},
+ "extra": {"custom-key": "custom-value"}
+ }"#;
+ let raw: RawPackageData = serde_json::from_str(json).unwrap();
+ assert_eq!(raw.name, "test/extra");
+ assert!(raw.extra_fields.contains_key("scripts"));
+ assert!(raw.extra_fields.contains_key("config"));
+ assert!(raw.extra_fields.contains_key("extra"));
+
+ // Roundtrip: extra fields should be preserved in output
+ let output = to_json_pretty(&raw).unwrap();
+ let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
+ assert!(parsed["scripts"].is_object());
+ assert!(parsed["config"].is_object());
+ assert!(parsed["extra"].is_object());
+ }
+
+ #[test]
+ fn raw_read_from_file() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().join("composer.json");
+ let content = r#"{"name": "test/file", "require": {"php": ">=8.0"}}"#;
+ std::fs::write(&path, content).unwrap();
+
+ let raw = read_from_file(&path).unwrap();
+ assert_eq!(raw.name, "test/file");
+ assert_eq!(raw.require.get("php").unwrap(), ">=8.0");
+ }
+
+ #[test]
fn raw_none_fields_omitted() {
let raw = RawPackageData::new("test/empty".to_string());
let json = to_json_pretty(&raw).unwrap();