aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart/Cargo.toml2
-rw-r--r--crates/mozart/src/commands/init.rs17
-rw-r--r--crates/mozart/src/commands/require.rs152
-rw-r--r--crates/mozart/src/console.rs2
-rw-r--r--crates/mozart/src/lib.rs2
-rw-r--r--crates/mozart/src/package.rs92
-rw-r--r--crates/mozart/src/packagist.rs141
-rw-r--r--crates/mozart/src/version.rs287
8 files changed, 673 insertions, 22 deletions
diff --git a/crates/mozart/Cargo.toml b/crates/mozart/Cargo.toml
index e54249f..f4d160b 100644
--- a/crates/mozart/Cargo.toml
+++ b/crates/mozart/Cargo.toml
@@ -9,7 +9,7 @@ clap = { version = "4.5.57", features = ["derive"] }
colored = "3.1.1"
dialoguer = "0.12.0"
regex = "1.12.3"
-reqwest = "0.13.2"
+reqwest = { version = "0.13.2", features = ["blocking", "json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tokio = { version = "1.49.0", features = ["full"] }
diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs
index 64a0263..fb7520f 100644
--- a/crates/mozart/src/commands/init.rs
+++ b/crates/mozart/src/commands/init.rs
@@ -1,14 +1,12 @@
-use std::collections::BTreeMap;
-use std::path::{Path, PathBuf};
-use std::process::Command;
+use crate::console;
+use crate::package::{self, RawAuthor, RawAutoload, RawPackageData, RawRepository};
+use crate::validation;
use anyhow::{Context, bail};
use clap::Args;
use colored::Colorize;
-use crate::console;
-use crate::package::{
- self, RawAuthor, RawAutoload, RawPackageData, RawRepository,
-};
-use crate::validation;
+use std::collections::BTreeMap;
+use std::path::{Path, PathBuf};
+use std::process::Command;
#[derive(Args)]
pub struct InitArgs {
@@ -103,8 +101,7 @@ pub fn execute(args: &InitArgs, cli: &super::Cli) -> anyhow::Result<()> {
console.info(&format!("Writing {}", composer_file.display()));
}
- package::write_to_file(&composer, &composer_file)
- .context("Failed to write composer.json")?;
+ package::write_to_file(&composer, &composer_file).context("Failed to write composer.json")?;
// Create autoload directory if specified
if let Some(ref autoload) = composer.autoload {
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 923a77c..603e6e8 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -1,3 +1,8 @@
+use crate::console;
+use crate::package::{self, Stability};
+use crate::packagist;
+use crate::validation;
+use crate::version;
use clap::Args;
#[derive(Args)]
@@ -118,6 +123,149 @@ pub struct RequireArgs {
pub apcu_autoloader_prefix: Option<String>,
}
-pub fn execute(_args: &RequireArgs, _cli: &super::Cli) -> anyhow::Result<()> {
- todo!()
+pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
+ if args.packages.is_empty() {
+ anyhow::bail!("Not enough arguments (missing: \"packages\").");
+ }
+
+ // Resolve working directory
+ let working_dir = if let Some(ref dir) = cli.working_dir {
+ std::path::PathBuf::from(dir)
+ } else {
+ std::env::current_dir()?
+ };
+
+ let composer_path = working_dir.join("composer.json");
+ if !composer_path.exists() {
+ anyhow::bail!(
+ "composer.json not found in {}. Run `mozart init` to create one.",
+ working_dir.display()
+ );
+ }
+
+ // Read existing composer.json
+ let mut raw = package::read_from_file(&composer_path)?;
+
+ // Determine preferred stability from composer.json's minimum-stability
+ let preferred_stability = raw
+ .minimum_stability
+ .as_deref()
+ .map(|s| match s.to_lowercase().as_str() {
+ "dev" => Stability::Dev,
+ "alpha" => Stability::Alpha,
+ "beta" => Stability::Beta,
+ "rc" | "RC" => Stability::RC,
+ _ => Stability::Stable,
+ })
+ .unwrap_or(Stability::Stable);
+
+ // Process each package argument
+ let mut additions: Vec<(String, String, bool)> = Vec::new(); // (name, constraint, is_dev)
+
+ for pkg_arg in &args.packages {
+ // Try to parse as "vendor/package:constraint"
+ let (name, constraint) = match validation::parse_require_string(pkg_arg) {
+ Ok((n, v)) => (n.to_lowercase(), v),
+ Err(_) => {
+ // No version specified — resolve from Packagist
+ let name = pkg_arg.trim().to_lowercase();
+ if !validation::validate_package_name(&name) {
+ anyhow::bail!("Invalid package name: \"{name}\"");
+ }
+
+ println!(
+ "{}",
+ console::info(&format!(
+ "Using version constraint for {name} from Packagist..."
+ ))
+ );
+
+ let versions = packagist::fetch_package_versions(&name)?;
+ let best = version::find_best_candidate(&versions, preferred_stability)
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "Could not find a version of package \"{name}\" matching your minimum-stability ({preferred_stability:?}). \
+ Try requiring it with an explicit version constraint."
+ )
+ })?;
+
+ let stability = version::stability_of(&best.version_normalized);
+ let constraint = if args.fixed {
+ best.version.clone()
+ } else {
+ version::find_recommended_require_version(
+ &best.version,
+ &best.version_normalized,
+ stability,
+ )
+ };
+
+ println!(
+ "{}",
+ console::info(&format!("Using version {constraint} for {name}"))
+ );
+
+ (name, constraint)
+ }
+ };
+
+ additions.push((name, constraint, args.dev));
+ }
+
+ // Apply changes
+ for (name, constraint, is_dev) in &additions {
+ let section_name = if *is_dev { "require-dev" } else { "require" };
+ let target = if *is_dev {
+ &mut raw.require_dev
+ } else {
+ &mut raw.require
+ };
+
+ if let Some(existing) = target.get(name) {
+ println!(
+ "{}",
+ console::comment(&format!(
+ "Updating {name} from {existing} to {constraint} in {section_name}"
+ ))
+ );
+ } else {
+ println!(
+ "{}",
+ console::info(&format!("Adding {name} ({constraint}) to {section_name}"))
+ );
+ }
+
+ target.insert(name.clone(), constraint.clone());
+ }
+
+ // Sort packages if requested
+ if args.sort_packages {
+ let sorted_require: std::collections::BTreeMap<_, _> = raw.require.clone();
+ raw.require = sorted_require;
+ let sorted_dev: std::collections::BTreeMap<_, _> = raw.require_dev.clone();
+ raw.require_dev = sorted_dev;
+ }
+
+ // Write back
+ if args.dry_run {
+ println!(
+ "{}",
+ console::comment("Dry run: composer.json not modified.")
+ );
+ } else {
+ package::write_to_file(&raw, &composer_path)?;
+ }
+
+ // Dependency resolution / install notice
+ if !args.no_update && !args.no_install {
+ println!(
+ "{}",
+ console::comment(
+ "Dependency resolution and installation are not yet implemented. \
+ The composer.json has been updated."
+ )
+ );
+ }
+
+ Ok(())
}
diff --git a/crates/mozart/src/console.rs b/crates/mozart/src/console.rs
index 07eaf67..863e9ba 100644
--- a/crates/mozart/src/console.rs
+++ b/crates/mozart/src/console.rs
@@ -51,7 +51,7 @@ impl Console {
}
pub fn error(&self, msg: &str) {
- eprintln!("{}", console::error(msg));
+ eprintln!("{}", error(msg));
}
pub fn ask(&self, prompt: &str, default: &str) -> String {
diff --git a/crates/mozart/src/lib.rs b/crates/mozart/src/lib.rs
index 6506d11..5e5487a 100644
--- a/crates/mozart/src/lib.rs
+++ b/crates/mozart/src/lib.rs
@@ -1,4 +1,6 @@
pub mod commands;
pub mod console;
pub mod package;
+pub mod packagist;
pub mod validation;
+pub mod version;
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();
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");
+ }
+}
diff --git a/crates/mozart/src/version.rs b/crates/mozart/src/version.rs
new file mode 100644
index 0000000..a5eca13
--- /dev/null
+++ b/crates/mozart/src/version.rs
@@ -0,0 +1,287 @@
+use crate::package::Stability;
+use crate::packagist::PackagistVersion;
+use std::cmp::Ordering;
+
+/// Determine the stability of a normalized version string.
+pub fn stability_of(version_normalized: &str) -> Stability {
+ let v = version_normalized.to_lowercase();
+ if v.starts_with("dev-") || v.ends_with("-dev") {
+ return Stability::Dev;
+ }
+ // Check for pre-release suffixes: alpha, beta, RC
+ // Normalized versions use formats like "1.0.0.0-alpha1", "1.0.0.0-beta2", "1.0.0.0-RC1"
+ if let Some(pos) = v.rfind('-') {
+ let suffix = &v[pos + 1..];
+ if suffix.starts_with("alpha") {
+ return Stability::Alpha;
+ }
+ if suffix.starts_with("beta") {
+ return Stability::Beta;
+ }
+ if suffix.starts_with("rc") || suffix.starts_with("RC") {
+ return Stability::RC;
+ }
+ }
+ Stability::Stable
+}
+
+/// Compare two normalized version strings (e.g. "1.2.3.0" vs "1.2.4.0").
+///
+/// Each version is split into numeric parts. Non-numeric suffixes (like "-beta1")
+/// are handled by treating the base parts as numeric and the suffix separately.
+pub fn compare_normalized_versions(a: &str, b: &str) -> Ordering {
+ let parse = |v: &str| -> (Vec<u64>, Option<String>) {
+ // Split off any pre-release suffix
+ let (base, suffix) = if let Some(pos) = v.find('-') {
+ (&v[..pos], Some(v[pos + 1..].to_string()))
+ } else {
+ (v, None)
+ };
+ let parts: Vec<u64> = base.split('.').filter_map(|p| p.parse().ok()).collect();
+ (parts, suffix)
+ };
+
+ let (a_parts, a_suffix) = parse(a);
+ let (b_parts, b_suffix) = parse(b);
+
+ // Compare numeric parts
+ let max_len = a_parts.len().max(b_parts.len());
+ for i in 0..max_len {
+ let a_val = a_parts.get(i).copied().unwrap_or(0);
+ let b_val = b_parts.get(i).copied().unwrap_or(0);
+ match a_val.cmp(&b_val) {
+ Ordering::Equal => continue,
+ other => return other,
+ }
+ }
+
+ // If numeric parts are equal, compare stability
+ // A stable version (no suffix) is greater than a pre-release
+ match (&a_suffix, &b_suffix) {
+ (None, None) => Ordering::Equal,
+ (None, Some(_)) => Ordering::Greater, // stable > pre-release
+ (Some(_), None) => Ordering::Less, // pre-release < stable
+ (Some(a_s), Some(b_s)) => {
+ let stab_a = stability_of(&format!("0.0.0.0-{a_s}"));
+ let stab_b = stability_of(&format!("0.0.0.0-{b_s}"));
+ // Lower stability value = more stable = greater version
+ match stab_a.cmp(&stab_b) {
+ Ordering::Equal => a_s.cmp(b_s),
+ // Stability enum: Stable(0) < RC(5) < Beta(10) < Alpha(15) < Dev(20)
+ // But more stable = higher version, so we reverse
+ Ordering::Less => Ordering::Greater,
+ Ordering::Greater => Ordering::Less,
+ }
+ }
+ }
+}
+
+/// Find the best version candidate given a preferred minimum stability.
+///
+/// Returns the highest version whose stability is at least as stable as
+/// the preferred stability (i.e., stability value <= preferred value).
+pub fn find_best_candidate(
+ versions: &[PackagistVersion],
+ preferred_stability: Stability,
+) -> Option<&PackagistVersion> {
+ versions
+ .iter()
+ .filter(|v| stability_of(&v.version_normalized) <= preferred_stability)
+ .max_by(|a, b| compare_normalized_versions(&a.version_normalized, &b.version_normalized))
+}
+
+/// Generate a recommended version constraint string from a concrete version.
+///
+/// Examples:
+/// - `"1.2.1"` (stable) → `"^1.2"`
+/// - `"0.3.5"` (stable) → `"^0.3"`
+/// - `"2.0.0-beta.1"` (beta) → `"^2.0@beta"`
+/// - `"dev-master"` (dev) → `"dev-master"`
+pub fn find_recommended_require_version(
+ version: &str,
+ version_normalized: &str,
+ stability: Stability,
+) -> String {
+ // dev branches are returned as-is
+ if stability == Stability::Dev {
+ return version.to_string();
+ }
+
+ // Extract major.minor from the normalized version (e.g. "1.2.3.0" → "1.2")
+ let base = if let Some(pos) = version_normalized.find('-') {
+ &version_normalized[..pos]
+ } else {
+ version_normalized
+ };
+
+ let parts: Vec<&str> = base.split('.').collect();
+ let major = parts.first().copied().unwrap_or("0");
+ let minor = parts.get(1).copied().unwrap_or("0");
+
+ let constraint = format!("^{major}.{minor}");
+
+ match stability {
+ Stability::Stable => constraint,
+ Stability::RC => format!("{constraint}@RC"),
+ Stability::Beta => format!("{constraint}@beta"),
+ Stability::Alpha => format!("{constraint}@alpha"),
+ Stability::Dev => unreachable!(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_stability_of() {
+ assert_eq!(stability_of("1.0.0.0"), Stability::Stable);
+ assert_eq!(stability_of("2.3.1.0"), Stability::Stable);
+ assert_eq!(stability_of("1.0.0.0-alpha1"), Stability::Alpha);
+ assert_eq!(stability_of("1.0.0.0-beta2"), Stability::Beta);
+ assert_eq!(stability_of("1.0.0.0-RC1"), Stability::RC);
+ assert_eq!(stability_of("dev-master"), Stability::Dev);
+ assert_eq!(stability_of("dev-feature/foo"), Stability::Dev);
+ assert_eq!(stability_of("1.0.0.0-dev"), Stability::Dev);
+ }
+
+ #[test]
+ fn test_compare_normalized_versions() {
+ assert_eq!(
+ compare_normalized_versions("1.0.0.0", "1.0.0.0"),
+ Ordering::Equal
+ );
+ assert_eq!(
+ compare_normalized_versions("2.0.0.0", "1.0.0.0"),
+ Ordering::Greater
+ );
+ assert_eq!(
+ compare_normalized_versions("1.0.0.0", "2.0.0.0"),
+ Ordering::Less
+ );
+ assert_eq!(
+ compare_normalized_versions("1.2.0.0", "1.1.0.0"),
+ Ordering::Greater
+ );
+ assert_eq!(
+ compare_normalized_versions("1.0.0.0", "1.0.0.0-beta1"),
+ Ordering::Greater
+ );
+ assert_eq!(
+ compare_normalized_versions("1.0.0.0-RC1", "1.0.0.0-beta1"),
+ Ordering::Greater
+ );
+ }
+
+ #[test]
+ fn test_find_best_candidate_stable() {
+ let versions = vec![
+ PackagistVersion {
+ version: "dev-master".to_string(),
+ version_normalized: "dev-master".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ PackagistVersion {
+ version: "2.0.0-beta.1".to_string(),
+ version_normalized: "2.0.0.0-beta1".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ PackagistVersion {
+ version: "1.5.0".to_string(),
+ version_normalized: "1.5.0.0".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ PackagistVersion {
+ version: "1.4.0".to_string(),
+ version_normalized: "1.4.0.0".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ ];
+
+ let best = find_best_candidate(&versions, Stability::Stable).unwrap();
+ assert_eq!(best.version, "1.5.0");
+ }
+
+ #[test]
+ fn test_find_best_candidate_beta() {
+ let versions = vec![
+ PackagistVersion {
+ version: "dev-master".to_string(),
+ version_normalized: "dev-master".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ PackagistVersion {
+ version: "2.0.0-beta.1".to_string(),
+ version_normalized: "2.0.0.0-beta1".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ PackagistVersion {
+ version: "1.5.0".to_string(),
+ version_normalized: "1.5.0.0".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ },
+ ];
+
+ let best = find_best_candidate(&versions, Stability::Beta).unwrap();
+ assert_eq!(best.version, "2.0.0-beta.1");
+ }
+
+ #[test]
+ fn test_find_best_candidate_no_match() {
+ let versions = vec![PackagistVersion {
+ version: "dev-master".to_string(),
+ version_normalized: "dev-master".to_string(),
+ require: Default::default(),
+ dist: None,
+ source: None,
+ }];
+
+ let best = find_best_candidate(&versions, Stability::Stable);
+ assert!(best.is_none());
+ }
+
+ #[test]
+ fn test_find_recommended_require_version() {
+ // Stable
+ assert_eq!(
+ find_recommended_require_version("1.2.1", "1.2.1.0", Stability::Stable),
+ "^1.2"
+ );
+ assert_eq!(
+ find_recommended_require_version("0.3.5", "0.3.5.0", Stability::Stable),
+ "^0.3"
+ );
+
+ // Beta
+ assert_eq!(
+ find_recommended_require_version("2.0.0-beta.1", "2.0.0.0-beta1", Stability::Beta),
+ "^2.0@beta"
+ );
+
+ // RC
+ assert_eq!(
+ find_recommended_require_version("3.0.0-RC1", "3.0.0.0-RC1", Stability::RC),
+ "^3.0@RC"
+ );
+
+ // Dev
+ assert_eq!(
+ find_recommended_require_version("dev-master", "dev-master", Stability::Dev),
+ "dev-master"
+ );
+ }
+}