diff options
Diffstat (limited to 'crates/mozart/src')
| -rw-r--r-- | crates/mozart/src/commands/init.rs | 17 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 152 | ||||
| -rw-r--r-- | crates/mozart/src/console.rs | 2 | ||||
| -rw-r--r-- | crates/mozart/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/mozart/src/package.rs | 92 | ||||
| -rw-r--r-- | crates/mozart/src/packagist.rs | 141 | ||||
| -rw-r--r-- | crates/mozart/src/version.rs | 287 |
7 files changed, 672 insertions, 21 deletions
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" + ); + } +} |
