From fb06e2ac5e248bc7a8bad50cdb7500ed967c8c18 Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Sat, 23 May 2026 16:32:58 +0800 Subject: [PATCH 1/7] feat: init config file in set-connection and change auth to optional --- README.md | 1 + scopeql/src/config.rs | 166 ++++++++++++++++++++++++++++++++---------- 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 81e19f3..5a64743 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ default_connection = "default" [connections.default] endpoint = "https://..scopedb.cloud" +auth = "api_key" api_key = "your-api-key" headers = ["X-Tenant: acme"] ``` diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index aad71ac..296afa4 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -17,6 +17,8 @@ use std::path::Path; use std::path::PathBuf; use std::str::FromStr; +use anyhow::Error; +use anyhow::anyhow; use dialoguer::Confirm; use dialoguer::Input; use dialoguer::Password; @@ -24,7 +26,10 @@ use dialoguer::Select; use serde::Deserialize; use serde::Serialize; use serde::de::IntoDeserializer; +use toml_edit::Array; use toml_edit::DocumentMut; +use toml_edit::Item; +use toml_edit::Table; pub const DEFAULT_URL: &str = "http://127.0.0.1:6543"; @@ -185,6 +190,11 @@ impl Config { } } +fn deserialize_toml(path: &PathBuf, doc: DocumentMut) -> Result { + Config::deserialize(doc.into_deserializer()) + .map_err(|err| anyhow!("failed to deserialize config on {}: {err}", path.display())) +} + impl Default for Config { fn default() -> Self { Self { @@ -194,7 +204,7 @@ impl Default for Config { ConnectionSpec { endpoint: DEFAULT_URL.to_string(), headers: vec![], - auth: ConnectionAuthSpec::Direct, + auth: Some(ConnectionAuthSpec::Direct), }, )]), } @@ -210,7 +220,8 @@ pub struct ConnectionSpec { headers: Vec, #[serde(flatten)] - auth: ConnectionAuthSpec, + #[serde(default)] + auth: Option, } impl ConnectionSpec { @@ -223,7 +234,7 @@ impl ConnectionSpec { } pub fn auth(&self) -> &ConnectionAuthSpec { - &self.auth + self.auth.as_ref().unwrap_or(&ConnectionAuthSpec::Direct) } } @@ -246,13 +257,19 @@ impl ConnectionAuthSpec { } pub(crate) fn get_connections(name: Option<&str>) { - let (_path, doc) = load_document(); - let config = - Config::deserialize(doc.into_deserializer()).expect("failed to deserialize config"); + internal_get_connections(name).unwrap_or_else(|err| { + eprintln!("Failed to get connections: {err}"); + std::process::exit(1); + }); +} + +fn internal_get_connections(name: Option<&str>) -> Result<(), Error> { + let (path, doc) = load_document()?; + let config = deserialize_toml(&path, doc)?; if config.connections.is_empty() { println!("No connections configured."); - return; + return Ok(()); } let connections = if let Some(name) = name { @@ -293,13 +310,20 @@ pub(crate) fn get_connections(name: Option<&str>) { } println!("{table}"); + Ok(()) } pub(crate) fn use_connection(name: &str) { - let (path, mut doc) = load_document(); + internal_use_connection(name).unwrap_or_else(|err| { + eprintln!("Failed to switch connection: {err}"); + std::process::exit(1); + }); +} - let config = - Config::deserialize(doc.clone().into_deserializer()).expect("failed to deserialize config"); +fn internal_use_connection(name: &str) -> Result<(), Error> { + let (path, mut doc) = load_document()?; + + let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { eprintln!("Connection '{name}' not found."); @@ -313,13 +337,38 @@ pub(crate) fn use_connection(name: &str) { }); println!("Switched to connection '{name}'"); + Ok(()) } pub(crate) fn set_connection(name: String) { - let (path, mut doc) = load_document(); + let (path, doc) = load_document().unwrap_or_else(|err| { + let path = candidate_config_paths() + .into_iter() + .next() + .expect("no candidate config paths"); + println!("Creating new config file at {} for {}", path.display(), err); + + let parent = path.parent().unwrap(); + std::fs::create_dir_all(parent).unwrap_or_else(|err| { + panic!( + "failed to create config directory {}: {err}", + parent.display() + ); + }); + + let mut doc = DocumentMut::new(); + doc["default_connection"] = toml_edit::value(&name); + (path, doc) + }); - let mut config = - Config::deserialize(doc.clone().into_deserializer()).expect("failed to deserialize config"); + internal_set_connection(name, path, doc).unwrap_or_else(|err| { + eprintln!("Failed to set connection: {err}"); + std::process::exit(1); + }); +} + +fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> Result<(), Error> { + let mut config = deserialize_toml(&path, doc.clone())?; let conn = if let Some(conn) = config.connections.get_mut(&name) { if Confirm::new() @@ -335,7 +384,7 @@ pub(crate) fn set_connection(name: String) { .expect("failed to read endpoint"); } - conn.auth = prompt_existing_auth(&conn.auth); + conn.auth = Some(prompt_existing_auth(conn.auth.as_ref())); conn.clone() } else { let endpoint = Input::new() @@ -349,7 +398,7 @@ pub(crate) fn set_connection(name: String) { ConnectionSpec { endpoint, headers: vec![], - auth, + auth: Some(auth), } }; @@ -360,9 +409,11 @@ pub(crate) fn set_connection(name: String) { }); println!("Set connection '{name}' in {}", path.display()); + Ok(()) } -fn prompt_existing_auth(current: &ConnectionAuthSpec) -> ConnectionAuthSpec { +fn prompt_existing_auth(current: Option<&ConnectionAuthSpec>) -> ConnectionAuthSpec { + let current = current.unwrap_or(&ConnectionAuthSpec::Direct); let actions = [ "Keep current auth", "Modify current auth fields", @@ -424,29 +475,38 @@ fn prompt_api_key() -> String { } fn write_connection(doc: &mut DocumentMut, name: &str, conn: &ConnectionSpec) { - if !doc["connections"].is_table() { - doc["connections"] = toml_edit::Item::Table(toml_edit::Table::new()); - } - if doc["connections"].get(name).is_none() { - doc["connections"][name] = toml_edit::Item::Table(toml_edit::Table::new()); + let connections = doc + .as_table_mut() + .entry("connections") + .or_insert(Item::Table({ + let mut t = Table::new(); + t.set_implicit(true); + t + })); + if !connections.is_table() { + let mut t = Table::new(); + t.set_implicit(true); + *connections = Item::Table(t); } + let connections = connections.as_table_mut().unwrap(); - let table = doc["connections"][name] - .as_table_mut() - .expect("connection should be a TOML table"); + let table = connections + .entry(name) + .or_insert(Item::Table(toml_edit::Table::new())); + let table = table.as_table_mut().unwrap(); table["endpoint"] = toml_edit::value(&conn.endpoint); if conn.headers.is_empty() { table.remove("headers"); } else { - let mut headers = toml_edit::Array::default(); + let mut headers = Array::default(); for header in &conn.headers { headers.push(header.as_str()); } table["headers"] = toml_edit::value(headers); } - match &conn.auth { + match conn.auth() { ConnectionAuthSpec::Direct => { table["auth"] = toml_edit::value("direct"); table.remove("api_key"); @@ -459,10 +519,16 @@ fn write_connection(doc: &mut DocumentMut, name: &str, conn: &ConnectionSpec) { } pub(crate) fn delete_connection(name: &str) { - let (path, mut doc) = load_document(); + internal_delete_connection(name).unwrap_or_else(|err| { + eprintln!("Failed to delete connection: {err}"); + std::process::exit(1); + }); +} - let config = - Config::deserialize(doc.clone().into_deserializer()).expect("failed to deserialize config"); +fn internal_delete_connection(name: &str) -> Result<(), Error> { + let (path, mut doc) = load_document()?; + + let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { eprintln!("Connection '{name}' not found."); @@ -485,16 +551,22 @@ pub(crate) fn delete_connection(name: &str) { }); println!("Deleted connection '{name}' from {}", path.display()); + Ok(()) } -fn load_document() -> (PathBuf, DocumentMut) { - let path = candidate_config_paths() - .into_iter() +fn load_document() -> Result<(PathBuf, DocumentMut), Error> { + let candidates = candidate_config_paths(); + let path = candidates + .iter() .find(|path| path.exists()) - .unwrap_or_else(|| { - eprintln!("No config file found. Run `scopeql config add-connection` to create one."); - std::process::exit(1); - }); + .ok_or_else(|| { + let paths = candidates + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", "); + anyhow!("config file does not exist in any of [{}]", paths) + })?; let content = std::fs::read_to_string(&path).unwrap_or_else(|err| { panic!("failed to read config file {}: {err}", path.display()); @@ -504,7 +576,8 @@ fn load_document() -> (PathBuf, DocumentMut) { panic!("failed to parse config file {}: {err}", path.display()); }); - (path, doc) + log::info!("loaded config from {}", path.display()); + Ok((path.clone(), doc)) } #[cfg(test)] @@ -629,4 +702,23 @@ headers = ["X-Tenant: acme"] ["X-Tenant: acme", "X-Trace: demo"] ); } + + #[test] + fn test_default_auth() { + let config: Config = toml::from_str( + r#" +default_connection = "local" + +[connections.local] +endpoint = "http://127.0.0.1:9999" +# headers and api_key will be ignored because auth type is direct by default +api_key = "ignored-api-key" +"#, + ) + .unwrap(); + + let conn = config.get_default_connection().unwrap(); + assert_eq!(conn.endpoint(), "http://127.0.0.1:9999"); + assert_matches!(conn.auth(), ConnectionAuthSpec::Direct); + } } From 2659d52db838f35352e65c224ccb11f86ab1ce0b Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Sun, 24 May 2026 20:50:45 +0800 Subject: [PATCH 2/7] chore: propagate the error instead of panic directly in functions that return Result --- scopeql/src/config.rs | 54 +++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index 296afa4..9a8a318 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -190,7 +190,7 @@ impl Config { } } -fn deserialize_toml(path: &PathBuf, doc: DocumentMut) -> Result { +fn deserialize_toml(path: &Path, doc: DocumentMut) -> Result { Config::deserialize(doc.into_deserializer()) .map_err(|err| anyhow!("failed to deserialize config on {}: {err}", path.display())) } @@ -273,9 +273,10 @@ fn internal_get_connections(name: Option<&str>) -> Result<(), Error> { } let connections = if let Some(name) = name { - let conn = config.connections.get(name).unwrap_or_else(|| { - panic!("Connection '{name}' not found in config."); - }); + let conn = config + .connections + .get(name) + .ok_or_else(|| anyhow!("Connection '{name}' not found in config."))?; vec![(name, conn)] } else { config @@ -326,35 +327,34 @@ fn internal_use_connection(name: &str) -> Result<(), Error> { let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { - eprintln!("Connection '{name}' not found."); - std::process::exit(1); + return Err(anyhow!("Connection '{name}' not found.")); } set_toml_path(&mut doc, &["default_connection"], toml_edit::value(name)); - std::fs::write(&path, doc.to_string()).unwrap_or_else(|err| { - panic!("failed to write config file {}: {err}", path.display()); - }); + std::fs::write(&path, doc.to_string()) + .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; println!("Switched to connection '{name}'"); Ok(()) } pub(crate) fn set_connection(name: String) { - let (path, doc) = load_document().unwrap_or_else(|err| { + let (path, doc) = load_document().unwrap_or_else(|_err| { let path = candidate_config_paths() .into_iter() .next() .expect("no candidate config paths"); - println!("Creating new config file at {} for {}", path.display(), err); + println!("Creating new config file at {}", path.display()); let parent = path.parent().unwrap(); - std::fs::create_dir_all(parent).unwrap_or_else(|err| { - panic!( + if let Err(err) = std::fs::create_dir_all(parent) { + eprintln!( "failed to create config directory {}: {err}", parent.display() ); - }); + std::process::exit(1); + } let mut doc = DocumentMut::new(); doc["default_connection"] = toml_edit::value(&name); @@ -404,9 +404,8 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> write_connection(&mut doc, &name, &conn); - std::fs::write(&path, doc.to_string()).unwrap_or_else(|err| { - panic!("failed to write config file {}: {err}", path.display()); - }); + std::fs::write(&path, doc.to_string()) + .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; println!("Set connection '{name}' in {}", path.display()); Ok(()) @@ -531,14 +530,12 @@ fn internal_delete_connection(name: &str) -> Result<(), Error> { let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { - eprintln!("Connection '{name}' not found."); - std::process::exit(1); + return Err(anyhow!("Connection '{name}' not found.")); } if config.default_connection == name { let Some(other) = config.connections.keys().find(|k| *k != name).cloned() else { - eprintln!("Cannot delete the only connection."); - std::process::exit(1); + return Err(anyhow!("Cannot delete the only connection.")); }; set_toml_path(&mut doc, &["default_connection"], toml_edit::value(&other)); println!("Switched to connection '{other}'"); @@ -546,9 +543,8 @@ fn internal_delete_connection(name: &str) -> Result<(), Error> { doc["connections"].as_table_mut().unwrap().remove(name); - std::fs::write(&path, doc.to_string()).unwrap_or_else(|err| { - panic!("failed to write config file {}: {err}", path.display()); - }); + std::fs::write(&path, doc.to_string()) + .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; println!("Deleted connection '{name}' from {}", path.display()); Ok(()) @@ -568,13 +564,11 @@ fn load_document() -> Result<(PathBuf, DocumentMut), Error> { anyhow!("config file does not exist in any of [{}]", paths) })?; - let content = std::fs::read_to_string(&path).unwrap_or_else(|err| { - panic!("failed to read config file {}: {err}", path.display()); - }); + let content = std::fs::read_to_string(path) + .map_err(|err| anyhow!("failed to read config file {}: {err}", path.display()))?; - let doc = DocumentMut::from_str(&content).unwrap_or_else(|err| { - panic!("failed to parse config file {}: {err}", path.display()); - }); + let doc = DocumentMut::from_str(&content) + .map_err(|err| anyhow!("failed to parse config file {}: {err}", path.display()))?; log::info!("loaded config from {}", path.display()); Ok((path.clone(), doc)) From 3d1c665f5041cfbc370f512e64b6390e69267c1d Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Sun, 24 May 2026 21:05:18 +0800 Subject: [PATCH 3/7] chore: improve stdout and log --- scopeql/src/config.rs | 56 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index 9a8a318..cdcb59d 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -257,27 +257,39 @@ impl ConnectionAuthSpec { } pub(crate) fn get_connections(name: Option<&str>) { - internal_get_connections(name).unwrap_or_else(|err| { - eprintln!("Failed to get connections: {err}"); - std::process::exit(1); - }); -} - -fn internal_get_connections(name: Option<&str>) -> Result<(), Error> { - let (path, doc) = load_document()?; - let config = deserialize_toml(&path, doc)?; + let config = match load_document() { + Ok((path, doc)) => match deserialize_toml(&path, doc) { + Ok(c) => c, + Err(err) => { + eprintln!("Failed to load config: {err}"); + std::process::exit(1); + } + }, + Err(_) => { + let candidates = candidate_config_paths() + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", "); + log::info!("no config file found in [{candidates}]; treating as empty connections"); + println!("No connections configured."); + return; + } + }; if config.connections.is_empty() { println!("No connections configured."); - return Ok(()); + return; } let connections = if let Some(name) = name { - let conn = config - .connections - .get(name) - .ok_or_else(|| anyhow!("Connection '{name}' not found in config."))?; - vec![(name, conn)] + match config.connections.get(name) { + Some(conn) => vec![(name, conn)], + None => { + eprintln!("Connection '{name}' not found."); + std::process::exit(1); + } + } } else { config .connections @@ -311,7 +323,6 @@ fn internal_get_connections(name: Option<&str>) -> Result<(), Error> { } println!("{table}"); - Ok(()) } pub(crate) fn use_connection(name: &str) { @@ -322,7 +333,7 @@ pub(crate) fn use_connection(name: &str) { } fn internal_use_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = load_document()?; + let (path, mut doc) = load_document().map_err(|_| anyhow!("Connection '{name}' not found."))?; let config = deserialize_toml(&path, doc.clone())?; @@ -525,7 +536,7 @@ pub(crate) fn delete_connection(name: &str) { } fn internal_delete_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = load_document()?; + let (path, mut doc) = load_document().map_err(|_| anyhow!("Connection '{name}' not found."))?; let config = deserialize_toml(&path, doc.clone())?; @@ -556,12 +567,9 @@ fn load_document() -> Result<(PathBuf, DocumentMut), Error> { .iter() .find(|path| path.exists()) .ok_or_else(|| { - let paths = candidates - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(", "); - anyhow!("config file does not exist in any of [{}]", paths) + anyhow!( + "no config file found; run `scopeql config set-connection ` to create one" + ) })?; let content = std::fs::read_to_string(path) From 3e3a73aff41d4024409c5d5046a95bebfbd74de2 Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Mon, 25 May 2026 12:31:51 +0800 Subject: [PATCH 4/7] revert changes on default auth type --- scopeql/src/config.rs | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index cdcb59d..de764d0 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -204,7 +204,7 @@ impl Default for Config { ConnectionSpec { endpoint: DEFAULT_URL.to_string(), headers: vec![], - auth: Some(ConnectionAuthSpec::Direct), + auth: ConnectionAuthSpec::Direct, }, )]), } @@ -221,7 +221,7 @@ pub struct ConnectionSpec { #[serde(flatten)] #[serde(default)] - auth: Option, + auth: ConnectionAuthSpec, } impl ConnectionSpec { @@ -234,7 +234,7 @@ impl ConnectionSpec { } pub fn auth(&self) -> &ConnectionAuthSpec { - self.auth.as_ref().unwrap_or(&ConnectionAuthSpec::Direct) + &self.auth } } @@ -395,7 +395,7 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> .expect("failed to read endpoint"); } - conn.auth = Some(prompt_existing_auth(conn.auth.as_ref())); + conn.auth = prompt_existing_auth(&conn.auth); conn.clone() } else { let endpoint = Input::new() @@ -409,7 +409,7 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> ConnectionSpec { endpoint, headers: vec![], - auth: Some(auth), + auth: auth, } }; @@ -422,8 +422,7 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> Ok(()) } -fn prompt_existing_auth(current: Option<&ConnectionAuthSpec>) -> ConnectionAuthSpec { - let current = current.unwrap_or(&ConnectionAuthSpec::Direct); +fn prompt_existing_auth(current: &ConnectionAuthSpec) -> ConnectionAuthSpec { let actions = [ "Keep current auth", "Modify current auth fields", @@ -704,23 +703,4 @@ headers = ["X-Tenant: acme"] ["X-Tenant: acme", "X-Trace: demo"] ); } - - #[test] - fn test_default_auth() { - let config: Config = toml::from_str( - r#" -default_connection = "local" - -[connections.local] -endpoint = "http://127.0.0.1:9999" -# headers and api_key will be ignored because auth type is direct by default -api_key = "ignored-api-key" -"#, - ) - .unwrap(); - - let conn = config.get_default_connection().unwrap(); - assert_eq!(conn.endpoint(), "http://127.0.0.1:9999"); - assert_matches!(conn.auth(), ConnectionAuthSpec::Direct); - } } From 87ba474adc8d2b3cc5ea595008e9e0ae744a3430 Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Mon, 25 May 2026 12:37:12 +0800 Subject: [PATCH 5/7] fix lint --- scopeql/src/config.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index de764d0..e56a8b9 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -220,7 +220,6 @@ pub struct ConnectionSpec { headers: Vec, #[serde(flatten)] - #[serde(default)] auth: ConnectionAuthSpec, } @@ -409,7 +408,7 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> ConnectionSpec { endpoint, headers: vec![], - auth: auth, + auth, } }; From 90503788cf2991855ecfadc5bc8a89c0432767d4 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 25 May 2026 12:51:33 +0800 Subject: [PATCH 6/7] fine tune styles Signed-off-by: tison --- scopeql/src/client/connection.rs | 8 ++- scopeql/src/client/protocol.rs | 2 +- scopeql/src/config.rs | 83 +++++++++++++++++++++----------- scopeql/src/main.rs | 4 +- scopeql/src/output.rs | 8 +-- 5 files changed, 65 insertions(+), 40 deletions(-) diff --git a/scopeql/src/client/connection.rs b/scopeql/src/client/connection.rs index a2fd948..56f0416 100644 --- a/scopeql/src/client/connection.rs +++ b/scopeql/src/client/connection.rs @@ -46,9 +46,7 @@ impl Client { ) -> Result { let authorization = match api_key.filter(|api_key| !api_key.is_empty()) { Some(api_key) => Some(HeaderValue::from_str(&format!("Bearer {api_key}")).map_err( - |err| { - Error::new("failed to build authorization header".to_string()).set_source(err) - }, + |err| Error::new("failed to build authorization header").set_source(err), )?), None => None, }; @@ -60,7 +58,7 @@ impl Client { authorization, extra_headers, }), - Err(err) => Err(Error::new("failed to parse endpoint".to_string()).set_source(err)), + Err(err) => Err(Error::new("failed to parse endpoint").set_source(err)), } } @@ -144,7 +142,7 @@ impl Client { fn make_url(&self, path: &str) -> Result { self.endpoint .join(path) - .map_err(|err| Error::new("failed to construct URL".to_string()).set_source(err)) + .map_err(|err| Error::new("failed to construct URL").set_source(err)) } fn request_headers(&self) -> HeaderMap { diff --git a/scopeql/src/client/protocol.rs b/scopeql/src/client/protocol.rs index 5b51b8f..07eb416 100644 --- a/scopeql/src/client/protocol.rs +++ b/scopeql/src/client/protocol.rs @@ -32,7 +32,7 @@ pub enum Response { impl Response { pub async fn from_http_response(r: reqwest::Response) -> Result { - let make_error = |err| Error::new("failed to make response".to_string()).set_source(err); + let make_error = |err| Error::new("failed to make response").set_source(err); let code = r.status(); if code.is_success() { diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index e56a8b9..cfc8a44 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -17,8 +17,6 @@ use std::path::Path; use std::path::PathBuf; use std::str::FromStr; -use anyhow::Error; -use anyhow::anyhow; use dialoguer::Confirm; use dialoguer::Input; use dialoguer::Password; @@ -31,6 +29,8 @@ use toml_edit::DocumentMut; use toml_edit::Item; use toml_edit::Table; +use crate::Error; + pub const DEFAULT_URL: &str = "http://127.0.0.1:6543"; fn candidate_config_paths() -> Vec { @@ -191,8 +191,12 @@ impl Config { } fn deserialize_toml(path: &Path, doc: DocumentMut) -> Result { - Config::deserialize(doc.into_deserializer()) - .map_err(|err| anyhow!("failed to deserialize config on {}: {err}", path.display())) + Config::deserialize(doc.into_deserializer()).map_err(|err| { + Error::new(format!( + "failed to deserialize config on {}: {err}", + path.display() + )) + }) } impl Default for Config { @@ -325,25 +329,30 @@ pub(crate) fn get_connections(name: Option<&str>) { } pub(crate) fn use_connection(name: &str) { - internal_use_connection(name).unwrap_or_else(|err| { + do_use_connection(name).unwrap_or_else(|err| { eprintln!("Failed to switch connection: {err}"); std::process::exit(1); }); } -fn internal_use_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = load_document().map_err(|_| anyhow!("Connection '{name}' not found."))?; +fn do_use_connection(name: &str) -> Result<(), Error> { + let (path, mut doc) = + load_document().map_err(|_| Error::new(format!("Connection '{name}' not found.")))?; let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { - return Err(anyhow!("Connection '{name}' not found.")); + return Err(Error::new(format!("Connection '{name}' not found."))); } set_toml_path(&mut doc, &["default_connection"], toml_edit::value(name)); - std::fs::write(&path, doc.to_string()) - .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; + std::fs::write(&path, doc.to_string()).map_err(|err| { + Error::new(format!( + "failed to write config file {}: {err}", + path.display() + )) + })?; println!("Switched to connection '{name}'"); Ok(()) @@ -355,6 +364,7 @@ pub(crate) fn set_connection(name: String) { .into_iter() .next() .expect("no candidate config paths"); + println!("Creating new config file at {}", path.display()); let parent = path.parent().unwrap(); @@ -371,13 +381,13 @@ pub(crate) fn set_connection(name: String) { (path, doc) }); - internal_set_connection(name, path, doc).unwrap_or_else(|err| { + do_set_connection(name, path, doc).unwrap_or_else(|err| { eprintln!("Failed to set connection: {err}"); std::process::exit(1); }); } -fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> Result<(), Error> { +fn do_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> Result<(), Error> { let mut config = deserialize_toml(&path, doc.clone())?; let conn = if let Some(conn) = config.connections.get_mut(&name) { @@ -414,8 +424,12 @@ fn internal_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> write_connection(&mut doc, &name, &conn); - std::fs::write(&path, doc.to_string()) - .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; + std::fs::write(&path, doc.to_string()).map_err(|err| { + Error::new(format!( + "failed to write config file {}: {err}", + path.display() + )) + })?; println!("Set connection '{name}' in {}", path.display()); Ok(()) @@ -527,24 +541,25 @@ fn write_connection(doc: &mut DocumentMut, name: &str, conn: &ConnectionSpec) { } pub(crate) fn delete_connection(name: &str) { - internal_delete_connection(name).unwrap_or_else(|err| { + do_delete_connection(name).unwrap_or_else(|err| { eprintln!("Failed to delete connection: {err}"); std::process::exit(1); }); } -fn internal_delete_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = load_document().map_err(|_| anyhow!("Connection '{name}' not found."))?; +fn do_delete_connection(name: &str) -> Result<(), Error> { + let (path, mut doc) = + load_document().map_err(|_| Error::new(format!("Connection '{name}' not found.")))?; let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { - return Err(anyhow!("Connection '{name}' not found.")); + return Err(Error::new(format!("Connection '{name}' not found."))); } if config.default_connection == name { let Some(other) = config.connections.keys().find(|k| *k != name).cloned() else { - return Err(anyhow!("Cannot delete the only connection.")); + return Err(Error::new("Cannot delete the only connection.")); }; set_toml_path(&mut doc, &["default_connection"], toml_edit::value(&other)); println!("Switched to connection '{other}'"); @@ -552,8 +567,12 @@ fn internal_delete_connection(name: &str) -> Result<(), Error> { doc["connections"].as_table_mut().unwrap().remove(name); - std::fs::write(&path, doc.to_string()) - .map_err(|err| anyhow!("failed to write config file {}: {err}", path.display()))?; + std::fs::write(&path, doc.to_string()).map_err(|err| { + Error::new(format!( + "failed to write config file {}: {err}", + path.display() + )) + })?; println!("Deleted connection '{name}' from {}", path.display()); Ok(()) @@ -565,16 +584,24 @@ fn load_document() -> Result<(PathBuf, DocumentMut), Error> { .iter() .find(|path| path.exists()) .ok_or_else(|| { - anyhow!( - "no config file found; run `scopeql config set-connection ` to create one" + Error::new( + "no config file found; run `scopeql config set-connection ` to create one", ) })?; - let content = std::fs::read_to_string(path) - .map_err(|err| anyhow!("failed to read config file {}: {err}", path.display()))?; - - let doc = DocumentMut::from_str(&content) - .map_err(|err| anyhow!("failed to parse config file {}: {err}", path.display()))?; + let content = std::fs::read_to_string(path).map_err(|err| { + Error::new(format!( + "failed to read config file {}: {err}", + path.display() + )) + })?; + + let doc = DocumentMut::from_str(&content).map_err(|err| { + Error::new(format!( + "failed to parse config file {}: {err}", + path.display() + )) + })?; log::info!("loaded config from {}", path.display()); Ok((path.clone(), doc)) diff --git a/scopeql/src/main.rs b/scopeql/src/main.rs index 1afb25c..b078858 100644 --- a/scopeql/src/main.rs +++ b/scopeql/src/main.rs @@ -134,9 +134,9 @@ struct Error { } impl Error { - fn new(message: String) -> Self { + fn new(message: impl Into) -> Self { Self { - message, + message: message.into(), source: None, } } diff --git a/scopeql/src/output.rs b/scopeql/src/output.rs index 16a15f0..0b3dfd1 100644 --- a/scopeql/src/output.rs +++ b/scopeql/src/output.rs @@ -61,7 +61,7 @@ fn format_table( let rows = result_set .into_values() - .or_raise(|| Error::new("failed to convert result rows".to_string()))?; + .or_raise(|| Error::new("failed to convert result rows"))?; const TABLE_STYLE_PRESET: &str = "||--+-++| ++++++"; let mut table = comfy_table::Table::new(); @@ -124,7 +124,7 @@ fn format_json(result_set: ResultSet) -> Result { .collect::>(); let rows = result_set .into_values() - .or_raise(|| Error::new("failed to convert result rows".to_string()))?; + .or_raise(|| Error::new("failed to convert result rows"))?; let json_rows = rows .into_iter() @@ -150,7 +150,7 @@ fn format_csv(result_set: ResultSet) -> Result { .collect::>(); let rows = result_set .into_values() - .or_raise(|| Error::new("failed to convert result rows".to_string()))?; + .or_raise(|| Error::new("failed to convert result rows"))?; let mut output = String::new(); for (index, field) in fields.iter().enumerate() { @@ -184,7 +184,7 @@ fn format_jsonl(result_set: ResultSet) -> Result { .collect::>(); let rows = result_set .into_values() - .or_raise(|| Error::new("failed to convert result rows".to_string()))?; + .or_raise(|| Error::new("failed to convert result rows"))?; let mut output = String::new(); for row in rows { From 90da61fcddaa4f77b1a66695a950344ab6cc1870 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 25 May 2026 12:56:48 +0800 Subject: [PATCH 7/7] simplify Signed-off-by: tison --- scopeql/src/config.rs | 84 ++++++++++++------------------------------- 1 file changed, 23 insertions(+), 61 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index cfc8a44..d106180 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -24,10 +24,7 @@ use dialoguer::Select; use serde::Deserialize; use serde::Serialize; use serde::de::IntoDeserializer; -use toml_edit::Array; use toml_edit::DocumentMut; -use toml_edit::Item; -use toml_edit::Table; use crate::Error; @@ -260,37 +257,26 @@ impl ConnectionAuthSpec { } pub(crate) fn get_connections(name: Option<&str>) { - let config = match load_document() { - Ok((path, doc)) => match deserialize_toml(&path, doc) { - Ok(c) => c, - Err(err) => { - eprintln!("Failed to load config: {err}"); - std::process::exit(1); - } - }, - Err(_) => { - let candidates = candidate_config_paths() - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(", "); - log::info!("no config file found in [{candidates}]; treating as empty connections"); - println!("No connections configured."); - return; - } - }; + do_get_connections(name).unwrap_or_else(|err| { + eprintln!("Failed to get connections: {err}"); + std::process::exit(1); + }); +} + +fn do_get_connections(name: Option<&str>) -> Result<(), Error> { + let (path, doc) = load_document()?; + let config = deserialize_toml(&path, doc)?; if config.connections.is_empty() { println!("No connections configured."); - return; + return Ok(()); } let connections = if let Some(name) = name { match config.connections.get(name) { Some(conn) => vec![(name, conn)], None => { - eprintln!("Connection '{name}' not found."); - std::process::exit(1); + return Err(Error::new(format!("Connection '{name}' not found."))); } } } else { @@ -326,6 +312,7 @@ pub(crate) fn get_connections(name: Option<&str>) { } println!("{table}"); + Ok(()) } pub(crate) fn use_connection(name: &str) { @@ -336,9 +323,7 @@ pub(crate) fn use_connection(name: &str) { } fn do_use_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = - load_document().map_err(|_| Error::new(format!("Connection '{name}' not found.")))?; - + let (path, mut doc) = load_document()?; let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) { @@ -359,35 +344,14 @@ fn do_use_connection(name: &str) -> Result<(), Error> { } pub(crate) fn set_connection(name: String) { - let (path, doc) = load_document().unwrap_or_else(|_err| { - let path = candidate_config_paths() - .into_iter() - .next() - .expect("no candidate config paths"); - - println!("Creating new config file at {}", path.display()); - - let parent = path.parent().unwrap(); - if let Err(err) = std::fs::create_dir_all(parent) { - eprintln!( - "failed to create config directory {}: {err}", - parent.display() - ); - std::process::exit(1); - } - - let mut doc = DocumentMut::new(); - doc["default_connection"] = toml_edit::value(&name); - (path, doc) - }); - - do_set_connection(name, path, doc).unwrap_or_else(|err| { + do_set_connection(name).unwrap_or_else(|err| { eprintln!("Failed to set connection: {err}"); std::process::exit(1); }); } -fn do_set_connection(name: String, path: PathBuf, mut doc: DocumentMut) -> Result<(), Error> { +fn do_set_connection(name: String) -> Result<(), Error> { + let (path, mut doc) = load_document()?; let mut config = deserialize_toml(&path, doc.clone())?; let conn = if let Some(conn) = config.connections.get_mut(&name) { @@ -500,28 +464,28 @@ fn write_connection(doc: &mut DocumentMut, name: &str, conn: &ConnectionSpec) { let connections = doc .as_table_mut() .entry("connections") - .or_insert(Item::Table({ - let mut t = Table::new(); + .or_insert(toml_edit::Item::Table({ + let mut t = toml_edit::Table::new(); t.set_implicit(true); t })); if !connections.is_table() { - let mut t = Table::new(); + let mut t = toml_edit::Table::new(); t.set_implicit(true); - *connections = Item::Table(t); + *connections = toml_edit::Item::Table(t); } let connections = connections.as_table_mut().unwrap(); let table = connections .entry(name) - .or_insert(Item::Table(toml_edit::Table::new())); + .or_insert(toml_edit::Item::Table(toml_edit::Table::new())); let table = table.as_table_mut().unwrap(); table["endpoint"] = toml_edit::value(&conn.endpoint); if conn.headers.is_empty() { table.remove("headers"); } else { - let mut headers = Array::default(); + let mut headers = toml_edit::Array::default(); for header in &conn.headers { headers.push(header.as_str()); } @@ -548,9 +512,7 @@ pub(crate) fn delete_connection(name: &str) { } fn do_delete_connection(name: &str) -> Result<(), Error> { - let (path, mut doc) = - load_document().map_err(|_| Error::new(format!("Connection '{name}' not found.")))?; - + let (path, mut doc) = load_document()?; let config = deserialize_toml(&path, doc.clone())?; if !config.connections.contains_key(name) {