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/package.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/package.rs')
| -rw-r--r-- | crates/mozart/src/package.rs | 92 |
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(); |
