diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-14 15:12:36 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-14 15:12:36 +0900 |
| commit | 06970e0ff8b24440908e0333204471d4b99781f0 (patch) | |
| tree | b26513756d3f956ac7c2524b8dac0fb2ccc3e01d /crates | |
| parent | f2a946fad67161b65d09ce26bee6604d38b8c42f (diff) | |
| download | php-mozart-06970e0ff8b24440908e0333204471d4b99781f0.tar.gz php-mozart-06970e0ff8b24440908e0333204471d4b99781f0.tar.zst php-mozart-06970e0ff8b24440908e0333204471d4b99781f0.zip | |
define package types, which represent composer.json
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart/src/package.rs | 607 |
1 files changed, 607 insertions, 0 deletions
diff --git a/crates/mozart/src/package.rs b/crates/mozart/src/package.rs new file mode 100644 index 0000000..1823918 --- /dev/null +++ b/crates/mozart/src/package.rs @@ -0,0 +1,607 @@ +use serde::Serialize; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +/// Package stability level. +/// Higher value = less stable. +/// Corresponds to `Composer\Package\BasePackage::STABILITY_*`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(u8)] +pub enum Stability { + #[default] + Stable = 0, + RC = 5, + Beta = 10, + Alpha = 15, + Dev = 20, +} + +/// A versioned relationship between two packages. +/// Corresponds to `Composer\Package\Link`. +#[derive(Debug, Clone)] +pub struct Link { + pub source: String, + pub target: String, + pub constraint: String, + pub pretty_constraint: Option<String>, + pub description: String, +} + +/// Package author metadata. +#[derive(Debug, Clone)] +pub struct Author { + pub name: Option<String>, + pub email: Option<String>, + pub homepage: Option<String>, + pub role: Option<String>, +} + +/// Autoload rule sets (PSR-4, PSR-0, classmap, files). +#[derive(Debug, Clone, Default)] +pub struct AutoloadRules { + pub psr4: BTreeMap<String, Vec<String>>, + pub psr0: BTreeMap<String, Vec<String>>, + pub classmap: Vec<String>, + pub files: Vec<String>, +} + +/// Support channel information. +#[derive(Debug, Clone, Default)] +pub struct Support { + pub email: Option<String>, + pub issues: Option<String>, + pub forum: Option<String>, + pub wiki: Option<String>, + pub source: Option<String>, + pub docs: Option<String>, + pub irc: Option<String>, + pub chat: Option<String>, + pub rss: Option<String>, + pub security: Option<String>, +} + +/// Funding link. +#[derive(Debug, Clone)] +pub struct Funding { + pub url: Option<String>, + pub funding_type: Option<String>, +} + +/// Version alias entry for root packages. +#[derive(Debug, Clone)] +pub struct VersionAlias { + pub package: String, + pub version: String, + pub alias: String, + pub alias_normalized: String, +} + +/// Core package data covering `BasePackage` + `Package` fields. +/// Corresponds to `Composer\Package\Package` (implements `PackageInterface`). +#[derive(Debug, Clone)] +pub struct PackageData { + // BasePackage fields + pub name: String, + pub pretty_name: String, + + // Package fields + pub version: String, + pub pretty_version: String, + pub package_type: String, + pub target_dir: Option<String>, + + // source + pub source_type: Option<String>, + pub source_url: Option<String>, + pub source_reference: Option<String>, + + // dist + pub dist_type: Option<String>, + pub dist_url: Option<String>, + pub dist_reference: Option<String>, + pub dist_sha1_checksum: Option<String>, + + pub release_date: Option<String>, + pub extra: BTreeMap<String, serde_json::Value>, + pub binaries: Vec<String>, + pub dev: bool, + pub stability: Stability, + pub notification_url: Option<String>, + + // dependency links + pub requires: BTreeMap<String, Link>, + pub conflicts: BTreeMap<String, Link>, + pub provides: BTreeMap<String, Link>, + pub replaces: BTreeMap<String, Link>, + pub dev_requires: BTreeMap<String, Link>, + pub suggests: BTreeMap<String, String>, + + // autoload + pub autoload: AutoloadRules, + pub dev_autoload: AutoloadRules, + + pub is_default_branch: bool, +} + +/// Package with full metadata (description, authors, license, etc.). +/// Corresponds to `Composer\Package\CompletePackage`. +#[derive(Debug, Clone)] +pub struct CompletePackageData { + pub package: PackageData, + + pub description: Option<String>, + pub homepage: Option<String>, + pub license: Vec<String>, + pub keywords: Vec<String>, + pub authors: Vec<Author>, + pub scripts: BTreeMap<String, Vec<String>>, + pub support: Support, + pub funding: Vec<Funding>, + pub repositories: Vec<serde_json::Value>, + /// `None` = not abandoned, `Some("")` = abandoned, `Some(pkg)` = replaced by pkg. + pub abandoned: Option<String>, + pub archive_name: Option<String>, + pub archive_excludes: Vec<String>, +} + +/// The root project package with project-level configuration. +/// Corresponds to `Composer\Package\RootPackage`. +#[derive(Debug, Clone)] +pub struct RootPackageData { + pub complete: CompletePackageData, + + pub minimum_stability: Stability, + pub prefer_stable: bool, + pub stability_flags: BTreeMap<String, Stability>, + pub config: BTreeMap<String, serde_json::Value>, + pub references: BTreeMap<String, String>, + pub aliases: Vec<VersionAlias>, +} + +/// Accessor for `PackageData` fields. +/// Corresponds to `Composer\Package\PackageInterface`. +pub trait Package { + fn name(&self) -> &str; + fn pretty_name(&self) -> &str; + fn version(&self) -> &str; + fn pretty_version(&self) -> &str; + fn package_type(&self) -> &str; + fn target_dir(&self) -> Option<&str>; + fn source_type(&self) -> Option<&str>; + fn source_url(&self) -> Option<&str>; + fn source_reference(&self) -> Option<&str>; + fn dist_type(&self) -> Option<&str>; + fn dist_url(&self) -> Option<&str>; + fn dist_reference(&self) -> Option<&str>; + fn dist_sha1_checksum(&self) -> Option<&str>; + fn release_date(&self) -> Option<&str>; + fn extra(&self) -> &BTreeMap<String, serde_json::Value>; + fn binaries(&self) -> &[String]; + fn is_dev(&self) -> bool; + fn stability(&self) -> Stability; + fn notification_url(&self) -> Option<&str>; + fn requires(&self) -> &BTreeMap<String, Link>; + fn conflicts(&self) -> &BTreeMap<String, Link>; + fn provides(&self) -> &BTreeMap<String, Link>; + fn replaces(&self) -> &BTreeMap<String, Link>; + fn dev_requires(&self) -> &BTreeMap<String, Link>; + fn suggests(&self) -> &BTreeMap<String, String>; + fn autoload(&self) -> &AutoloadRules; + fn dev_autoload(&self) -> &AutoloadRules; + fn is_default_branch(&self) -> bool; +} + +/// Accessor for `CompletePackageData` fields. +/// Corresponds to `Composer\Package\CompletePackageInterface`. +pub trait CompletePackage: Package { + fn description(&self) -> Option<&str>; + fn homepage(&self) -> Option<&str>; + fn license(&self) -> &[String]; + fn keywords(&self) -> &[String]; + fn authors(&self) -> &[Author]; + fn scripts(&self) -> &BTreeMap<String, Vec<String>>; + fn support(&self) -> &Support; + fn funding(&self) -> &[Funding]; + fn repositories(&self) -> &[serde_json::Value]; + fn abandoned(&self) -> Option<&str>; + fn archive_name(&self) -> Option<&str>; + fn archive_excludes(&self) -> &[String]; +} + +/// Accessor for `RootPackageData` fields. +/// Corresponds to `Composer\Package\RootPackageInterface`. +pub trait RootPackage: CompletePackage { + fn minimum_stability(&self) -> Stability; + fn prefer_stable(&self) -> bool; + fn stability_flags(&self) -> &BTreeMap<String, Stability>; + fn config(&self) -> &BTreeMap<String, serde_json::Value>; + fn references(&self) -> &BTreeMap<String, String>; + fn aliases(&self) -> &[VersionAlias]; +} + +// ────────────────────────────────────────────── +// Delegation macros +// ────────────────────────────────────────────── + +/// Implements `Package` trait by delegating to an inner `PackageData` field. +macro_rules! delegate_package { + ($type:ty => $($path:ident).+) => { + impl Package for $type { + fn name(&self) -> &str { &self.$($path).+.name } + fn pretty_name(&self) -> &str { &self.$($path).+.pretty_name } + fn version(&self) -> &str { &self.$($path).+.version } + fn pretty_version(&self) -> &str { &self.$($path).+.pretty_version } + fn package_type(&self) -> &str { &self.$($path).+.package_type } + fn target_dir(&self) -> Option<&str> { self.$($path).+.target_dir.as_deref() } + fn source_type(&self) -> Option<&str> { self.$($path).+.source_type.as_deref() } + fn source_url(&self) -> Option<&str> { self.$($path).+.source_url.as_deref() } + fn source_reference(&self) -> Option<&str> { self.$($path).+.source_reference.as_deref() } + fn dist_type(&self) -> Option<&str> { self.$($path).+.dist_type.as_deref() } + fn dist_url(&self) -> Option<&str> { self.$($path).+.dist_url.as_deref() } + fn dist_reference(&self) -> Option<&str> { self.$($path).+.dist_reference.as_deref() } + fn dist_sha1_checksum(&self) -> Option<&str> { self.$($path).+.dist_sha1_checksum.as_deref() } + fn release_date(&self) -> Option<&str> { self.$($path).+.release_date.as_deref() } + fn extra(&self) -> &BTreeMap<String, serde_json::Value> { &self.$($path).+.extra } + fn binaries(&self) -> &[String] { &self.$($path).+.binaries } + fn is_dev(&self) -> bool { self.$($path).+.dev } + fn stability(&self) -> Stability { self.$($path).+.stability } + fn notification_url(&self) -> Option<&str> { self.$($path).+.notification_url.as_deref() } + fn requires(&self) -> &BTreeMap<String, Link> { &self.$($path).+.requires } + fn conflicts(&self) -> &BTreeMap<String, Link> { &self.$($path).+.conflicts } + fn provides(&self) -> &BTreeMap<String, Link> { &self.$($path).+.provides } + fn replaces(&self) -> &BTreeMap<String, Link> { &self.$($path).+.replaces } + fn dev_requires(&self) -> &BTreeMap<String, Link> { &self.$($path).+.dev_requires } + fn suggests(&self) -> &BTreeMap<String, String> { &self.$($path).+.suggests } + fn autoload(&self) -> &AutoloadRules { &self.$($path).+.autoload } + fn dev_autoload(&self) -> &AutoloadRules { &self.$($path).+.dev_autoload } + fn is_default_branch(&self) -> bool { self.$($path).+.is_default_branch } + } + }; +} + +/// Implements `CompletePackage` trait by delegating to an inner `CompletePackageData` field. +macro_rules! delegate_complete_package { + ($type:ty => $($path:ident).+) => { + impl CompletePackage for $type { + fn description(&self) -> Option<&str> { self.$($path).+.description.as_deref() } + fn homepage(&self) -> Option<&str> { self.$($path).+.homepage.as_deref() } + fn license(&self) -> &[String] { &self.$($path).+.license } + fn keywords(&self) -> &[String] { &self.$($path).+.keywords } + fn authors(&self) -> &[Author] { &self.$($path).+.authors } + fn scripts(&self) -> &BTreeMap<String, Vec<String>> { &self.$($path).+.scripts } + fn support(&self) -> &Support { &self.$($path).+.support } + fn funding(&self) -> &[Funding] { &self.$($path).+.funding } + fn repositories(&self) -> &[serde_json::Value] { &self.$($path).+.repositories } + fn abandoned(&self) -> Option<&str> { self.$($path).+.abandoned.as_deref() } + fn archive_name(&self) -> Option<&str> { self.$($path).+.archive_name.as_deref() } + fn archive_excludes(&self) -> &[String] { &self.$($path).+.archive_excludes } + } + }; +} + +impl Package for PackageData { + fn name(&self) -> &str { + &self.name + } + fn pretty_name(&self) -> &str { + &self.pretty_name + } + fn version(&self) -> &str { + &self.version + } + fn pretty_version(&self) -> &str { + &self.pretty_version + } + fn package_type(&self) -> &str { + &self.package_type + } + fn target_dir(&self) -> Option<&str> { + self.target_dir.as_deref() + } + fn source_type(&self) -> Option<&str> { + self.source_type.as_deref() + } + fn source_url(&self) -> Option<&str> { + self.source_url.as_deref() + } + fn source_reference(&self) -> Option<&str> { + self.source_reference.as_deref() + } + fn dist_type(&self) -> Option<&str> { + self.dist_type.as_deref() + } + fn dist_url(&self) -> Option<&str> { + self.dist_url.as_deref() + } + fn dist_reference(&self) -> Option<&str> { + self.dist_reference.as_deref() + } + fn dist_sha1_checksum(&self) -> Option<&str> { + self.dist_sha1_checksum.as_deref() + } + fn release_date(&self) -> Option<&str> { + self.release_date.as_deref() + } + fn extra(&self) -> &BTreeMap<String, serde_json::Value> { + &self.extra + } + fn binaries(&self) -> &[String] { + &self.binaries + } + fn is_dev(&self) -> bool { + self.dev + } + fn stability(&self) -> Stability { + self.stability + } + fn notification_url(&self) -> Option<&str> { + self.notification_url.as_deref() + } + fn requires(&self) -> &BTreeMap<String, Link> { + &self.requires + } + fn conflicts(&self) -> &BTreeMap<String, Link> { + &self.conflicts + } + fn provides(&self) -> &BTreeMap<String, Link> { + &self.provides + } + fn replaces(&self) -> &BTreeMap<String, Link> { + &self.replaces + } + fn dev_requires(&self) -> &BTreeMap<String, Link> { + &self.dev_requires + } + fn suggests(&self) -> &BTreeMap<String, String> { + &self.suggests + } + fn autoload(&self) -> &AutoloadRules { + &self.autoload + } + fn dev_autoload(&self) -> &AutoloadRules { + &self.dev_autoload + } + fn is_default_branch(&self) -> bool { + self.is_default_branch + } +} + +impl CompletePackage for CompletePackageData { + fn description(&self) -> Option<&str> { + self.description.as_deref() + } + fn homepage(&self) -> Option<&str> { + self.homepage.as_deref() + } + fn license(&self) -> &[String] { + &self.license + } + fn keywords(&self) -> &[String] { + &self.keywords + } + fn authors(&self) -> &[Author] { + &self.authors + } + fn scripts(&self) -> &BTreeMap<String, Vec<String>> { + &self.scripts + } + fn support(&self) -> &Support { + &self.support + } + fn funding(&self) -> &[Funding] { + &self.funding + } + fn repositories(&self) -> &[serde_json::Value] { + &self.repositories + } + fn abandoned(&self) -> Option<&str> { + self.abandoned.as_deref() + } + fn archive_name(&self) -> Option<&str> { + self.archive_name.as_deref() + } + fn archive_excludes(&self) -> &[String] { + &self.archive_excludes + } +} + +impl RootPackage for RootPackageData { + fn minimum_stability(&self) -> Stability { + self.minimum_stability + } + fn prefer_stable(&self) -> bool { + self.prefer_stable + } + fn stability_flags(&self) -> &BTreeMap<String, Stability> { + &self.stability_flags + } + fn config(&self) -> &BTreeMap<String, serde_json::Value> { + &self.config + } + fn references(&self) -> &BTreeMap<String, String> { + &self.references + } + fn aliases(&self) -> &[VersionAlias] { + &self.aliases + } +} + +// CompletePackageData delegates Package → inner PackageData +delegate_package!(CompletePackageData => package); + +// RootPackageData delegates Package → inner CompletePackageData → PackageData +delegate_package!(RootPackageData => complete.package); + +// RootPackageData delegates CompletePackage → inner CompletePackageData +delegate_complete_package!(RootPackageData => complete); + +/// Unstructured representation of a composer.json file. +/// 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)] +pub struct RawPackageData { + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<String>, + + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub package_type: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub homepage: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option<String>, + + #[serde(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>, + + pub require: BTreeMap<String, String>, + + #[serde(rename = "require-dev", skip_serializing_if = "BTreeMap::is_empty")] + pub require_dev: BTreeMap<String, String>, + + #[serde(skip_serializing_if = "Vec::is_empty")] + pub repositories: Vec<RawRepository>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub autoload: Option<RawAutoload>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RawAuthor { + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option<String>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RawAutoload { + #[serde(rename = "psr-4")] + pub psr4: BTreeMap<String, String>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RawRepository { + #[serde(rename = "type")] + pub repo_type: String, + pub url: String, +} + +impl RawPackageData { + pub fn new(name: String) -> Self { + Self { + name, + description: None, + package_type: None, + homepage: None, + license: None, + authors: Vec::new(), + minimum_stability: None, + require: BTreeMap::new(), + require_dev: BTreeMap::new(), + repositories: Vec::new(), + autoload: None, + } + } +} + +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(); + let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter); + value.serialize(&mut ser)?; + let mut json = String::from_utf8(buf).expect("serde_json produces valid UTF-8"); + json.push('\n'); + Ok(json) +} + +pub fn write_to_file(value: &impl Serialize, path: &Path) -> anyhow::Result<()> { + let json = to_json_pretty(value)?; + fs::write(path, json)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn raw_minimal_json() { + let raw = RawPackageData::new("test/pkg".to_string()); + let json = to_json_pretty(&raw).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(parsed["name"], "test/pkg"); + assert!(parsed["require"].is_object()); + assert!(parsed.get("description").is_none()); + assert!(parsed.get("type").is_none()); + assert!(parsed.get("authors").is_none()); + assert!(parsed.get("require-dev").is_none()); + assert!(parsed.get("autoload").is_none()); + } + + #[test] + fn raw_full_json() { + let mut raw = RawPackageData::new("acme/full".to_string()); + raw.description = Some("A full package".to_string()); + raw.package_type = Some("library".to_string()); + raw.homepage = Some("https://example.com".to_string()); + raw.license = Some("MIT".to_string()); + raw.authors = vec![RawAuthor { + name: "Jane Doe".to_string(), + email: Some("jane@example.com".to_string()), + }]; + raw.minimum_stability = Some("dev".to_string()); + raw.require.insert("php".to_string(), ">=8.1".to_string()); + raw.require_dev + .insert("phpunit/phpunit".to_string(), "^10.0".to_string()); + raw.repositories = vec![RawRepository { + repo_type: "vcs".to_string(), + url: "https://github.com/acme/repo".to_string(), + }]; + + let mut psr4 = BTreeMap::new(); + psr4.insert("Acme\\Full\\".to_string(), "src/".to_string()); + raw.autoload = Some(RawAutoload { psr4 }); + + let json = to_json_pretty(&raw).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(parsed["name"], "acme/full"); + assert_eq!(parsed["description"], "A full package"); + assert_eq!(parsed["type"], "library"); + assert_eq!(parsed["homepage"], "https://example.com"); + assert_eq!(parsed["license"], "MIT"); + assert_eq!(parsed["minimum-stability"], "dev"); + assert_eq!(parsed["authors"][0]["name"], "Jane Doe"); + assert_eq!(parsed["authors"][0]["email"], "jane@example.com"); + assert_eq!(parsed["require"]["php"], ">=8.1"); + assert_eq!(parsed["require-dev"]["phpunit/phpunit"], "^10.0"); + assert_eq!(parsed["repositories"][0]["type"], "vcs"); + assert_eq!(parsed["autoload"]["psr-4"]["Acme\\Full\\"], "src/"); + } + + #[test] + fn raw_none_fields_omitted() { + let raw = RawPackageData::new("test/empty".to_string()); + let json = to_json_pretty(&raw).unwrap(); + + assert!(!json.contains("\"description\"")); + assert!(!json.contains("\"type\"")); + assert!(!json.contains("\"homepage\"")); + assert!(!json.contains("\"license\"")); + assert!(!json.contains("\"authors\"")); + assert!(!json.contains("\"minimum-stability\"")); + assert!(!json.contains("\"require-dev\"")); + assert!(!json.contains("\"repositories\"")); + assert!(!json.contains("\"autoload\"")); + } +} |
