From 9b33a3c4eca44726b4602f1a2d4046982584ef93 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 20 Apr 2026 12:58:52 +0300 Subject: [PATCH] Add local DB manifest URL CLI --- .../src/commands/local_db/manifest_urls.rs | 152 ++++++++++++++++++ crates/cli/src/commands/local_db/mod.rs | 6 + crates/settings/src/local_db_remotes.rs | 46 +++++- 3 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 crates/cli/src/commands/local_db/manifest_urls.rs diff --git a/crates/cli/src/commands/local_db/manifest_urls.rs b/crates/cli/src/commands/local_db/manifest_urls.rs new file mode 100644 index 0000000000..4c300d0736 --- /dev/null +++ b/crates/cli/src/commands/local_db/manifest_urls.rs @@ -0,0 +1,152 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use raindex_app_settings::yaml::{ + raindex::{RaindexYaml, RaindexYamlValidation}, + YamlParsable, +}; +use std::io::{self, Write}; +use url::Url; + +#[derive(Debug, Clone, Parser)] +#[command(about = "Print local DB remote manifest URLs from settings YAML")] +pub struct ManifestUrls { + #[clap( + long, + help = "Full YAML document that configures local DB remotes", + value_name = "YAML" + )] + pub settings_yaml: String, +} + +impl ManifestUrls { + pub fn execute(self) -> Result<()> { + let mut stdout = io::stdout(); + self.execute_to(&mut stdout) + } + + fn execute_to(&self, writer: &mut W) -> Result<()> { + let urls = manifest_urls_from_settings(&self.settings_yaml)?; + + for url in urls { + writeln!(writer, "{url}")?; + } + + Ok(()) + } +} + +fn manifest_urls_from_settings(settings_yaml: &str) -> Result> { + let raindex_yaml = RaindexYaml::new( + vec![settings_yaml.to_string()], + RaindexYamlValidation { + local_db_remotes: true, + ..Default::default() + }, + ) + .context("failed to parse settings YAML")?; + let remotes = raindex_yaml + .get_local_db_remotes() + .context("failed to parse local-db-remotes from settings YAML")?; + + Ok(remotes + .into_iter() + .collect::>() + .into_values() + .map(|remote| remote.url) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn render(args: ManifestUrls) -> Result { + let mut buffer = Vec::new(); + args.execute_to(&mut buffer)?; + Ok(String::from_utf8(buffer).unwrap()) + } + + #[test] + fn single_remote_url_prints_one_line() { + let yaml = r#" +version: 6 +local-db-remotes: + raindex: https://example.com/manifest.yaml +"#; + + let output = render(ManifestUrls { + settings_yaml: yaml.to_string(), + }) + .unwrap(); + + assert_eq!(output, "https://example.com/manifest.yaml\n"); + } + + #[test] + fn duplicate_remote_urls_fail_settings_parsing() { + let yaml = r#" +version: 6 +local-db-remotes: + raindex-a: https://example.com/manifest.yaml + raindex-b: https://example.com/manifest.yaml +"#; + + let err = render(ManifestUrls { + settings_yaml: yaml.to_string(), + }) + .unwrap_err(); + let message = err.to_string(); + + assert!(message.contains("failed to parse settings YAML")); + } + + #[test] + fn no_remotes_prints_nothing() { + let yaml = r#" +version: 6 +"#; + + let output = render(ManifestUrls { + settings_yaml: yaml.to_string(), + }) + .unwrap(); + + assert_eq!(output, ""); + } + + #[test] + fn default_output_is_only_one_url_per_line() { + let yaml = r#" +version: 6 +local-db-remotes: + raindex-a: https://example.com/a.yaml + raindex-b: https://example.com/b.yaml +"#; + + let output = render(ManifestUrls { + settings_yaml: yaml.to_string(), + }) + .unwrap(); + + assert_eq!( + output, + "https://example.com/a.yaml\nhttps://example.com/b.yaml\n" + ); + } + + #[test] + fn missing_version_fails_before_printing_urls() { + let yaml = r#" +local-db-remotes: + raindex: https://example.com/manifest.yaml +"#; + + let err = render(ManifestUrls { + settings_yaml: yaml.to_string(), + }) + .unwrap_err(); + let message = err.to_string(); + + assert!(message.contains("failed to parse settings YAML")); + } +} diff --git a/crates/cli/src/commands/local_db/mod.rs b/crates/cli/src/commands/local_db/mod.rs index 5a64d13d93..c03074f366 100644 --- a/crates/cli/src/commands/local_db/mod.rs +++ b/crates/cli/src/commands/local_db/mod.rs @@ -1,22 +1,28 @@ pub mod cli; pub mod executor; +pub mod manifest_urls; pub mod pipeline; use anyhow::Result; use clap::Subcommand; use cli::RunPipeline; +use manifest_urls::ManifestUrls; #[derive(Subcommand)] #[command(about = "Local database operations")] pub enum LocalDbCommands { #[command(name = "sync")] Sync(RunPipeline), + + #[command(name = "manifest-urls")] + ManifestUrls(ManifestUrls), } impl LocalDbCommands { pub async fn execute(self) -> Result<()> { match self { LocalDbCommands::Sync(cmd) => cmd.execute().await, + LocalDbCommands::ManifestUrls(cmd) => cmd.execute(), } } } diff --git a/crates/settings/src/local_db_remotes.rs b/crates/settings/src/local_db_remotes.rs index c1c3cb8ba2..2f5ab73017 100644 --- a/crates/settings/src/local_db_remotes.rs +++ b/crates/settings/src/local_db_remotes.rs @@ -37,6 +37,7 @@ impl YamlParsableHash for LocalDbRemoteCfg { _: Option<&Context>, ) -> Result, YamlError> { let mut remotes = HashMap::new(); + let mut remote_keys_by_url = HashMap::new(); for document in documents { let document_read = document.read().map_err(|_| YamlError::ReadLockError)?; @@ -65,18 +66,29 @@ impl YamlParsableHash for LocalDbRemoteCfg { location: location.clone(), })?; - let remote = LocalDbRemoteCfg { - document: document.clone(), - key: remote_key.clone(), - url, - }; - if remotes.contains_key(&remote_key) { return Err(YamlError::KeyShadowing( remote_key, "local-db-remotes".to_string(), )); } + if let Some(existing_key) = remote_keys_by_url.get(&url) { + return Err(YamlError::Field { + kind: FieldErrorKind::InvalidValue { + field: "url".to_string(), + reason: format!("duplicates local-db-remotes[{}]", existing_key), + }, + location, + }); + } + + let remote = LocalDbRemoteCfg { + document: document.clone(), + key: remote_key.clone(), + url, + }; + + remote_keys_by_url.insert(remote.url.clone(), remote.key.clone()); remotes.insert(remote.key.clone(), remote); } } @@ -156,6 +168,28 @@ local-db-remotes: ); } + #[test] + fn test_parse_local_db_remotes_from_yaml_duplicate_url() { + let yaml = r#" +local-db-remotes: + mainnet: https://example.com/localdb/mainnet + duplicate: https://example.com/localdb/mainnet +"#; + let err = + LocalDbRemoteCfg::parse_all_from_yaml(vec![get_document(yaml)], None).unwrap_err(); + + assert_eq!( + err, + YamlError::Field { + kind: FieldErrorKind::InvalidValue { + field: "url".to_string(), + reason: "duplicates local-db-remotes[mainnet]".to_string(), + }, + location: "local-db-remotes[duplicate]".to_string(), + } + ); + } + #[test] fn test_parse_local_db_remotes_optional_absent_is_ok() { // No local-db-remotes key