diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-08 23:03:11 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-08 23:03:11 +0900 |
| commit | eeb845f2f8629e3ccfb8ee1a1ec0602c0f186427 (patch) | |
| tree | 5076422cf70ac92e1bc1e0424f0373100be5fd4c /crates/mozart | |
| parent | 392cf71170bf2550aa657158e9efd2b890381a94 (diff) | |
| download | php-mozart-eeb845f2f8629e3ccfb8ee1a1ec0602c0f186427.tar.gz php-mozart-eeb845f2f8629e3ccfb8ee1a1ec0602c0f186427.tar.zst php-mozart-eeb845f2f8629e3ccfb8ee1a1ec0602c0f186427.zip | |
fix(repository): align with Composer's RepositoryCommand pipeline
Introduce JsonConfigSource in mozart-core mirroring Composer's
JsonConfigSource fallback logic (add/insert/set-url/remove repository),
and BaseConfigContext mirroring BaseConfigCommand's initialize().
Key behaviour fixes:
- list: synthesise [packagist.org] <disabled> only when no composer-type
repo with a packagist.org host is present (was: always show enabled default)
- disable: idempotent via add_repository(false) matching Composer's branch;
now requires a name (no silent default to packagist.org)
- enable: calls remove_repository only, no extra empty-array cleanup
- set-url: preserves assoc-keyed format instead of converting to list
- get-url: assoc fast-path + unquoted error message matching Composer
- add: use regex pre-check (starts_with '{') instead of trial-parse
- error messages reworded to match Composer verbatim (mozart brand kept)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart')
| -rw-r--r-- | crates/mozart/src/commands.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/base_config.rs | 35 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config_helpers.rs | 55 | ||||
| -rw-r--r-- | crates/mozart/src/commands/repository.rs | 488 |
4 files changed, 294 insertions, 285 deletions
diff --git a/crates/mozart/src/commands.rs b/crates/mozart/src/commands.rs index 44ca07d..bf98bee 100644 --- a/crates/mozart/src/commands.rs +++ b/crates/mozart/src/commands.rs @@ -1,5 +1,6 @@ pub mod about; pub mod archive; +pub(crate) mod base_config; pub mod audit; pub mod browse; pub mod bump; diff --git a/crates/mozart/src/commands/base_config.rs b/crates/mozart/src/commands/base_config.rs new file mode 100644 index 0000000..be663d5 --- /dev/null +++ b/crates/mozart/src/commands/base_config.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; + +use mozart_core::config_source::JsonConfigSource; + +use super::config_helpers::composer_home; + +/// Mirrors Composer's `BaseConfigCommand`: resolves the target config file path +/// and enforces the `--file` ↔ `--global` mutual exclusivity. +pub(crate) struct BaseConfigContext { + pub config_source: JsonConfigSource, +} + +impl BaseConfigContext { + pub fn initialize( + global: bool, + file: Option<&str>, + cli: &super::Cli, + ) -> anyhow::Result<Self> { + if global && file.is_some() { + anyhow::bail!("--file and --global can not be combined"); + } + + let path: PathBuf = if global { + composer_home().join("config.json") + } else if let Some(f) = file { + PathBuf::from(f) + } else { + cli.working_dir()?.join("composer.json") + }; + + Ok(Self { + config_source: JsonConfigSource::new(path, false), + }) + } +} diff --git a/crates/mozart/src/commands/config_helpers.rs b/crates/mozart/src/commands/config_helpers.rs index c5cd187..f0aacbb 100644 --- a/crates/mozart/src/commands/config_helpers.rs +++ b/crates/mozart/src/commands/config_helpers.rs @@ -112,28 +112,6 @@ pub(crate) fn normalize_repositories(value: &serde_json::Value) -> Vec<serde_jso } } -/// Convert a Composer-style associative `repositories` object to Mozart's -/// array-of-objects format in-place. If `repositories` is already an array -/// (or absent), this is a no-op. -pub(crate) fn ensure_repositories_array(json: &mut serde_json::Value) { - if json["repositories"].is_object() { - let normalized = normalize_repositories(&json["repositories"].clone()); - json["repositories"] = serde_json::Value::Array(normalized); - } -} - -/// Find the index of a repository entry by name in a slice of normalized -/// repository values. Matches against the `"name"` field. -pub(crate) fn find_repo_by_name(repos: &[serde_json::Value], name: &str) -> Option<usize> { - repos.iter().position(|entry| { - entry - .get("name") - .and_then(|n| n.as_str()) - .map(|n| n == name) - .unwrap_or(false) - }) -} - /// Add a repository entry to the `repositories` array in json. /// If `append` is true, push to end; otherwise insert at beginning. /// Removes any existing entry with the same name first. @@ -174,39 +152,6 @@ pub(crate) fn remove_repository(json: &mut serde_json::Value, name: &str) { } } -/// Insert a repository entry before or after a named repository. -/// Returns an error if the target repository is not found. -pub(crate) fn insert_repository( - json: &mut serde_json::Value, - name: &str, - config: serde_json::Value, - target: &str, - before: bool, -) -> anyhow::Result<()> { - if !json["repositories"].is_array() { - json["repositories"] = serde_json::json!([]); - } - - remove_repository(json, name); - - let repos = json["repositories"].as_array_mut().unwrap(); - - let pos = repos - .iter() - .position(|entry| { - entry.get("name").and_then(|n| n.as_str()) == Some(target) - || entry - .as_object() - .map(|obj| obj.contains_key(target)) - .unwrap_or(false) - }) - .ok_or_else(|| anyhow!("Repository \"{target}\" not found"))?; - - let insert_pos = if before { pos } else { pos + 1 }; - repos.insert(insert_pos, config); - Ok(()) -} - /// Render a `serde_json::Value` as a human-readable string suitable for /// single-line display (matching Composer's behaviour). pub(crate) fn render_value(v: &serde_json::Value) -> String { diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index c2d2bd8..318450a 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -1,12 +1,9 @@ use anyhow::anyhow; use clap::Args; use mozart_core::console_writeln; -use std::path::PathBuf; -use super::config_helpers::{ - add_repository, composer_home, ensure_repositories_array, find_repo_by_name, insert_repository, - normalize_repositories, read_json_file, remove_repository, render_value, write_json_file, -}; +use super::base_config::BaseConfigContext; +use super::config_helpers::{normalize_repositories, render_value}; #[derive(Args)] pub struct RepositoryArgs { @@ -43,61 +40,75 @@ pub struct RepositoryArgs { pub after: Option<String>, } -fn resolve_file_path(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<PathBuf> { - if args.global && args.file.is_some() { - anyhow::bail!("Cannot combine --global and --file"); - } - if args.global { - return Ok(composer_home().join("config.json")); - } - if let Some(ref file) = args.file { - return Ok(PathBuf::from(file)); - } - Ok(cli.working_dir()?.join("composer.json")) -} - pub async fn execute( args: &RepositoryArgs, cli: &super::Cli, console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let action = args.action.as_deref().unwrap_or("list"); + let ctx = BaseConfigContext::initialize(args.global, args.file.as_deref(), cli)?; match action { - "list" | "ls" | "show" => execute_list(args, cli, console), - "add" => execute_add(args, cli), - "remove" | "rm" | "delete" => execute_remove(args, cli), - "set-url" | "seturl" => execute_set_url(args, cli), - "get-url" | "geturl" => execute_get_url(args, cli, console), - "disable" => execute_disable(args, cli), - "enable" => execute_enable(args, cli), + "list" | "ls" | "show" => list_repositories(&ctx, console), + "add" => execute_add(&ctx, args), + "remove" | "rm" | "delete" => execute_remove(&ctx, args), + "set-url" | "seturl" => execute_set_url(&ctx, args), + "get-url" | "geturl" => execute_get_url(&ctx, args, console), + "disable" => execute_disable(&ctx, args), + "enable" => execute_enable(&ctx, args), _ => Err(anyhow!( - "Unknown action \"{action}\". Expected one of: list, add, remove, set-url, get-url, enable, disable" + "Unknown action \"{action}\". Use list, add, remove, set-url, get-url, enable, disable" )), } } -fn execute_list( - args: &RepositoryArgs, - cli: &super::Cli, +/// Mirror of Composer's `RepositoryCommand::listRepositories`. +/// +/// Synthesises a `[packagist.org] <disabled>` line only when no `composer`-type +/// repository with a host ending in `packagist.org` is already in the list. +fn list_repositories( + ctx: &BaseConfigContext, console: &mozart_core::console::Console, ) -> anyhow::Result<()> { - let file_path = resolve_file_path(args, cli)?; - let json = read_json_file(&file_path, args.global)?; + let json = ctx.config_source.read()?; + let repos_raw = &json["repositories"]; + let repos = normalize_repositories(repos_raw); - let mut has_packagist_disable = false; + let packagist_present = repos.iter().any(|entry| { + entry.get("type").and_then(|t| t.as_str()) == Some("composer") + && entry + .get("url") + .and_then(|u| u.as_str()) + .map(host_ends_with_packagist_org) + .unwrap_or(false) + }); - let repos = normalize_repositories(&json["repositories"]); - for entry in &repos { - if let Some(obj) = entry.as_object() { - // Check for disabled repo entry like {"packagist.org": false} - if let Some((key, _)) = obj.iter().find(|(_, v)| v == &&serde_json::json!(false)) { - console_writeln!(console, &format!("[{key}] disabled"),); - if key == "packagist.org" { - has_packagist_disable = true; - } - continue; - } + // When no packagist.org-hosted composer repo is present, synthesise the + // disabled-packagist line exactly as Composer does (appending it to the list + // for display purposes only — not written to disk). + let mut display_repos = repos; + if !packagist_present { + let mut m = serde_json::Map::new(); + m.insert( + "packagist.org".to_string(), + serde_json::Value::Bool(false), + ); + display_repos.push(serde_json::Value::Object(m)); + } + + if display_repos.is_empty() { + console_writeln!(console, "No repositories configured"); + return Ok(()); + } + + for entry in &display_repos { + if let Some(obj) = entry.as_object() + && obj.len() == 1 + && let Some((key, val)) = obj.iter().next() + && val == &serde_json::Value::Bool(false) + { + console_writeln!(console, &format!("[{key}] disabled")); + continue; } let name = entry @@ -108,220 +119,159 @@ fn execute_list( .get("type") .and_then(|t| t.as_str()) .unwrap_or("unknown"); - let url = entry.get("url").and_then(|u| u.as_str()).unwrap_or(""); - - console_writeln!(console, &format!("[{name}] {repo_type} {url}"),); - } + let url = entry + .get("url") + .map(render_value) + .unwrap_or_default(); - if !has_packagist_disable { - console_writeln!( - console, - "[packagist.org] composer https://repo.packagist.org", - ); + console_writeln!(console, &format!("[{name}] {repo_type} {url}")); } Ok(()) } -fn execute_add(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { - let name = args - .name - .as_deref() - .ok_or_else(|| anyhow!("Repository name is required for \"add\""))?; +fn host_ends_with_packagist_org(url: &str) -> bool { + let after_scheme = url.split("://").nth(1).unwrap_or(url); + let host_port = after_scheme.split('/').next().unwrap_or(""); + let host = host_port.split(':').next().unwrap_or(""); + host == "packagist.org" || host.ends_with(".packagist.org") +} - if args.before.is_some() && args.after.is_some() { - anyhow::bail!("Cannot combine --before and --after"); - } +fn execute_add(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Result<()> { + let name = args.name.as_deref().ok_or_else(|| { + anyhow!("You must pass a repository name. Example: mozart repo add foo vcs https://example.org") + })?; - let entry = match (&args.arg1, &args.arg2) { - (Some(type_or_json), Some(url)) => { - // type + url - serde_json::json!({ - "name": name, - "type": type_or_json, - "url": url, - }) - } - (Some(json_str), None) => { - // Try to parse as JSON - match serde_json::from_str::<serde_json::Value>(json_str) { - Ok(mut parsed) => { - // Inject the name if not already present - if let Some(obj) = parsed.as_object_mut() - && !obj.contains_key("name") - { - obj.insert("name".to_string(), serde_json::json!(name)); - } - parsed - } - Err(_) => { - anyhow::bail!( - "Invalid JSON for repository config. Expected: <type> <url> or a JSON string" - ); - } - } - } - _ => { - anyhow::bail!( - "Missing arguments for \"add\". Expected: <name> <type> <url> or <name> <json>" - ); - } + let arg1 = args.arg1.as_deref().ok_or_else(|| { + anyhow!("You must pass the type and a url, or a JSON string.") + })?; + + // Mirror Composer's `Preg::isMatch('{^\s*\{}', $arg1)` check. + let repo_config = if arg1.trim_start().starts_with('{') { + serde_json::from_str::<serde_json::Value>(arg1) + .map_err(|e| anyhow!("Invalid JSON: {}", e))? + } else { + let url = args.arg2.as_deref().ok_or_else(|| { + anyhow!("You must pass the type and a url. Example: mozart repo add foo vcs https://example.org") + })?; + serde_json::json!({"type": arg1, "url": url}) }; - let file_path = resolve_file_path(args, cli)?; - let mut json = read_json_file(&file_path, args.global)?; + if args.before.is_some() && args.after.is_some() { + anyhow::bail!("You can not combine --before and --after"); + } if let Some(ref target) = args.before { - insert_repository(&mut json, name, entry, target, true)?; + ctx.config_source + .insert_repository(name, &repo_config, target, 0)?; } else if let Some(ref target) = args.after { - insert_repository(&mut json, name, entry, target, false)?; + ctx.config_source + .insert_repository(name, &repo_config, target, 1)?; } else { - add_repository(&mut json, name, entry, args.append); + ctx.config_source + .add_repository(name, &repo_config, args.append)?; } - write_json_file(&file_path, &json)?; Ok(()) } -fn execute_remove(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { +fn execute_remove(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Result<()> { let name = args .name .as_deref() - .ok_or_else(|| anyhow!("Repository name is required for \"remove\""))?; - - let file_path = resolve_file_path(args, cli)?; - let mut json = read_json_file(&file_path, args.global)?; - - ensure_repositories_array(&mut json); + .ok_or_else(|| anyhow!("You must pass the repository name to remove."))?; + ctx.config_source.remove_repository(name)?; if name == "packagist.org" || name == "packagist" { - // Removing packagist.org means disabling it - remove_repository(&mut json, "packagist.org"); - let disable_entry = serde_json::json!({"packagist.org": false}); - add_repository(&mut json, "packagist.org", disable_entry, args.append); - } else { - remove_repository(&mut json, name); - } - - // Clean up empty repositories array - if json["repositories"] - .as_array() - .map(|a| a.is_empty()) - .unwrap_or(false) - && let Some(obj) = json.as_object_mut() - { - obj.remove("repositories"); + // Removing packagist means disabling it (Composer behaviour). + // Default append=false so the disable entry goes to the front when + // the user didn't pass --append. + ctx.config_source + .add_repository("packagist.org", &serde_json::Value::Bool(false), args.append)?; } - write_json_file(&file_path, &json)?; Ok(()) } -fn execute_set_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { +fn execute_set_url(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Result<()> { let name = args .name .as_deref() - .ok_or_else(|| anyhow!("Repository name is required for \"set-url\""))?; + .ok_or_else(|| anyhow!("Usage: mozart repo set-url <name> <new-url>"))?; let new_url = args .arg1 .as_deref() - .ok_or_else(|| anyhow!("New URL is required for \"set-url\""))?; + .ok_or_else(|| anyhow!("Usage: mozart repo set-url <name> <new-url>"))?; - let file_path = resolve_file_path(args, cli)?; - let mut json = read_json_file(&file_path, args.global)?; - - ensure_repositories_array(&mut json); - - let found = json["repositories"].as_array_mut().and_then(|repos| { - repos - .iter_mut() - .find(|entry| entry.get("name").and_then(|n| n.as_str()) == Some(name)) - }); - - match found { - Some(entry) => { - entry["url"] = serde_json::json!(new_url); - write_json_file(&file_path, &json)?; - Ok(()) - } - None => Err(anyhow!("Repository \"{name}\" not found")), - } + ctx.config_source.set_repository_url(name, new_url)?; + Ok(()) } fn execute_get_url( + ctx: &BaseConfigContext, args: &RepositoryArgs, - cli: &super::Cli, console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let name = args .name .as_deref() - .ok_or_else(|| anyhow!("Repository name is required for \"get-url\""))?; + .ok_or_else(|| anyhow!("Usage: mozart repo get-url <name>"))?; - let file_path = resolve_file_path(args, cli)?; - let json = read_json_file(&file_path, args.global)?; + let json = ctx.config_source.read()?; + let repos_raw = &json["repositories"]; - let repos = normalize_repositories(&json["repositories"]); - - match find_repo_by_name(&repos, name) { - Some(idx) => { - let entry = &repos[idx]; - match entry.get("url") { - Some(url_val) => { - console_writeln!(console, &render_value(url_val),); - Ok(()) - } - None => Err(anyhow!("The \"{name}\" repository does not have a URL")), - } + // Assoc-keyed fast path (mirrors Composer's `isset($repos[$name])` check). + if let Some(repo) = repos_raw.as_object().and_then(|obj| obj.get(name)) { + if let Some(url) = repo.get("url").and_then(|u| u.as_str()) { + console_writeln!(console, url); + return Ok(()); } - None => Err(anyhow!("There is no \"{name}\" repository defined")), + anyhow::bail!("The {} repository does not have a URL", name); } -} -fn execute_disable(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { - let name = args.name.as_deref().unwrap_or("packagist.org"); - - if name != "packagist.org" && name != "packagist" { - anyhow::bail!("Only \"packagist.org\" can be disabled with this action"); + // List-format scan (mirrors Composer's fallback `foreach ($repos as $val)`). + let repos = normalize_repositories(repos_raw); + for repo in &repos { + if repo.get("name").and_then(|n| n.as_str()) == Some(name) { + if let Some(url) = repo.get("url").and_then(|u| u.as_str()) { + console_writeln!(console, url); + return Ok(()); + } + anyhow::bail!("The {} repository does not have a URL", name); + } } - let file_path = resolve_file_path(args, cli)?; - let mut json = read_json_file(&file_path, args.global)?; - - // Remove any existing packagist.org disable entry first - remove_repository(&mut json, "packagist.org"); - - let disable_entry = serde_json::json!({"packagist.org": false}); - add_repository(&mut json, "packagist.org", disable_entry, args.append); - - write_json_file(&file_path, &json)?; - Ok(()) + Err(anyhow!("There is no {} repository defined", name)) } -fn execute_enable(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { - let name = args.name.as_deref().unwrap_or("packagist.org"); +fn execute_disable(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Usage: mozart repo disable packagist.org"))?; - if name != "packagist.org" && name != "packagist" { - anyhow::bail!("Only \"packagist.org\" can be enabled with this action"); + if name == "packagist.org" || name == "packagist" { + ctx.config_source + .add_repository("packagist.org", &serde_json::Value::Bool(false), args.append)?; + return Ok(()); } - let file_path = resolve_file_path(args, cli)?; - let mut json = read_json_file(&file_path, args.global)?; + anyhow::bail!("Only packagist.org can be enabled/disabled using this command. Use add/remove for other repositories."); +} - remove_repository(&mut json, "packagist.org"); +fn execute_enable(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Usage: mozart repo enable packagist.org"))?; - // Clean up empty repositories array - if json["repositories"] - .as_array() - .map(|a| a.is_empty()) - .unwrap_or(false) - && let Some(obj) = json.as_object_mut() - { - obj.remove("repositories"); + if name == "packagist.org" || name == "packagist" { + // Just remove the disable override; Composer does nothing else here. + ctx.config_source.remove_repository("packagist.org")?; + return Ok(()); } - write_json_file(&file_path, &json)?; - Ok(()) + anyhow::bail!("Only packagist.org can be enabled/disabled using this command."); } #[cfg(test)] @@ -363,7 +313,7 @@ mod tests { let cli = make_cli(); let console = mozart_core::console::Console::new(0, false, false, false, false); - // Should succeed and print packagist.org + // Empty repos → synthesises [packagist.org] disabled let result = execute(&args, &cli, &console).await; assert!(result.is_ok()); } @@ -402,6 +352,27 @@ mod tests { } #[tokio::test] + async fn test_list_no_packagist_synth_when_composer_type_present() { + // When a composer-type repo pointing at packagist.org is present, + // no synthesised [packagist.org] disabled line should appear. + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "packagist.org", "type": "composer", "url": "https://repo.packagist.org"}]}"#, + ) + .unwrap(); + + let mut args = make_args(Some("list"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_ok()); + } + + #[tokio::test] async fn test_add_type_url() { let dir = tempfile::TempDir::new().unwrap(); let file = dir.path().join("composer.json"); @@ -825,12 +796,13 @@ mod tests { } #[tokio::test] - async fn test_disable_without_name_defaults_to_packagist() { + async fn test_disable_packagist_idempotent() { + // Calling disable twice should not create a duplicate entry. let dir = tempfile::TempDir::new().unwrap(); let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); + std::fs::write(&file, r#"{"repositories": [{"packagist.org": false}]}"#).unwrap(); - let mut args = make_args(Some("disable"), None, None, None); + let mut args = make_args(Some("disable"), Some("packagist.org"), None, None); args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); @@ -840,6 +812,7 @@ mod tests { let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos.len(), 1, "should still be just one disable entry"); assert_eq!(repos[0]["packagist.org"], false); } @@ -859,6 +832,22 @@ mod tests { } #[tokio::test] + async fn test_disable_without_name_error() { + // Composer requires a name for disable; Mozart mirrors that. + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("disable"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + #[tokio::test] async fn test_enable_packagist() { let dir = tempfile::TempDir::new().unwrap(); let file = dir.path().join("composer.json"); @@ -971,7 +960,8 @@ mod tests { } #[tokio::test] - async fn test_set_url_composer_format_converts_and_updates() { + async fn test_set_url_composer_format_keeps_assoc_shape() { + // Composer's setRepositoryUrl mutates in place without converting assoc → list. let dir = tempfile::TempDir::new().unwrap(); let file = dir.path().join("composer.json"); std::fs::write( @@ -994,10 +984,9 @@ mod tests { let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - // After conversion it should be an array - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["url"], "https://new.com"); - assert_eq!(repos[0]["name"], "my-repo"); + // Format is preserved: still an assoc object. + let repos = json["repositories"].as_object().unwrap(); + assert_eq!(repos["my-repo"]["url"], "https://new.com"); } #[tokio::test] @@ -1082,47 +1071,86 @@ mod tests { assert!(result.is_err()); } - #[test] - fn test_insert_before() { - let mut json = serde_json::json!({ - "repositories": [ - {"name": "a", "type": "vcs", "url": "https://a.com"}, - {"name": "b", "type": "vcs", "url": "https://b.com"}, - ] - }); + #[tokio::test] + async fn test_insert_before() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories":[{"name":"a","type":"vcs","url":"https://a.com"},{"name":"b","type":"vcs","url":"https://b.com"}]}"#, + ) + .unwrap(); + + let mut args = make_args( + Some("add"), + Some("new"), + Some("vcs"), + Some("https://new.com"), + ); + args.file = Some(file.to_str().unwrap().to_string()); + args.before = Some("b".to_string()); - let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); - insert_repository(&mut json, "new", entry, "b", true).unwrap(); + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + let json: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); let repos = json["repositories"].as_array().unwrap(); assert_eq!(repos[0]["name"], "a"); assert_eq!(repos[1]["name"], "new"); assert_eq!(repos[2]["name"], "b"); } - #[test] - fn test_insert_after() { - let mut json = serde_json::json!({ - "repositories": [ - {"name": "a", "type": "vcs", "url": "https://a.com"}, - {"name": "b", "type": "vcs", "url": "https://b.com"}, - ] - }); + #[tokio::test] + async fn test_insert_after() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories":[{"name":"a","type":"vcs","url":"https://a.com"},{"name":"b","type":"vcs","url":"https://b.com"}]}"#, + ) + .unwrap(); - let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); - insert_repository(&mut json, "new", entry, "a", false).unwrap(); + let mut args = make_args( + Some("add"), + Some("new"), + Some("vcs"), + Some("https://new.com"), + ); + args.file = Some(file.to_str().unwrap().to_string()); + args.after = Some("a".to_string()); + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); let repos = json["repositories"].as_array().unwrap(); assert_eq!(repos[0]["name"], "a"); assert_eq!(repos[1]["name"], "new"); assert_eq!(repos[2]["name"], "b"); } - #[test] - fn test_insert_target_not_found() { - let mut json = serde_json::json!({"repositories": []}); - let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); - let result = insert_repository(&mut json, "new", entry, "nonexistent", true); + #[tokio::test] + async fn test_insert_target_not_found() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); + + let mut args = make_args( + Some("add"), + Some("new"), + Some("vcs"), + Some("https://new.com"), + ); + args.file = Some(file.to_str().unwrap().to_string()); + args.before = Some("nonexistent".to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; assert!(result.is_err()); } } |
