diff --git a/crates/configuration/src/core.rs b/crates/configuration/src/core.rs index 1467c9dcfc..d5b04f9272 100644 --- a/crates/configuration/src/core.rs +++ b/crates/configuration/src/core.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use serde_json::Number; use std::env; -fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result { +pub fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result { match config { serde_json::Value::Object(map) => { let val = map diff --git a/crates/configuration/src/lib.rs b/crates/configuration/src/lib.rs index 32e85d65eb..2e8c62acb4 100644 --- a/crates/configuration/src/lib.rs +++ b/crates/configuration/src/lib.rs @@ -1,9 +1,11 @@ -use crate::core::Profile; +use crate::core::resolve_env_variables; use anyhow::{Context, Result, anyhow}; use camino::Utf8PathBuf; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fs::File; use std::{env, fs}; -use toml::Value; pub mod core; pub mod test_utils; @@ -20,6 +22,18 @@ pub trait Config { Self: Sized; } +#[derive(Debug, Serialize, Deserialize)] +struct ConfigSchema { + #[serde(flatten)] + pub tools: HashMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ToolProfiles { + #[serde(flatten)] + pub profiles: HashMap, +} + #[must_use] pub fn resolve_config_file() -> Utf8PathBuf { find_config_file().unwrap_or_else(|_| { @@ -31,32 +45,38 @@ pub fn resolve_config_file() -> Utf8PathBuf { }) } -pub fn load_config( - path: Option<&Utf8PathBuf>, - profile: Option<&str>, -) -> Result { - let config_path = path +pub fn load_config(path: Option<&Utf8PathBuf>, profile: Option<&str>) -> Result +where + T: Config + Default + Serialize + DeserializeOwned + Clone, +{ + let path = path .as_ref() .and_then(|p| search_config_upwards_relative_to(p).ok()) .or_else(|| find_config_file().ok()); - match config_path { - Some(path) => { - let raw_config_toml = fs::read_to_string(path) - .context("Failed to read snfoundry.toml config file")? - .parse::() - .context("Failed to parse snfoundry.toml config file")?; - - let raw_config_json = serde_json::to_value(raw_config_toml) - .context("Conversion from TOML value to JSON value should not fail.")?; - - core::load_config( - raw_config_json, - profile.map_or_else(|| Profile::Default, |p| Profile::Some(p.to_string())), - ) - } - None => Ok(T::default()), - } + let Some(config_path) = path else { + return Ok(T::default()); + }; + + let raw = fs::read_to_string(config_path).context("Failed to read snfoundry.toml")?; + let toml_value: toml::Value = + toml::from_str(&raw).context("Failed to parse snfoundry.toml config file")?; + let json_value = serde_json::to_value(toml_value)?; + let resolved_json = resolve_env_variables(json_value)?; + let parsed: ConfigSchema = serde_json::from_value(resolved_json) + .context("Failed to deserialize resolved config into ConfigSchema")?; + let tool_name = T::tool_name(); + + let Some(tool_profiles) = parsed.tools.get(tool_name) else { + return Ok(T::default()); + }; + + let profile_name = profile.unwrap_or("default"); + let Some(profile_config) = tool_profiles.profiles.get(profile_name) else { + return Err(anyhow!("Profile [{profile_name}] not found in config")); + }; + + Ok(profile_config.clone()) } pub fn search_config_upwards_relative_to(current_dir: &Utf8PathBuf) -> Result { @@ -79,11 +99,10 @@ pub fn find_config_file() -> Result { #[cfg(test)] mod tests { - use std::fs::{self, File}; - use super::*; use crate::test_utils::copy_config_to_tempdir; use serde::{Deserialize, Serialize}; + use std::fs::{self, File}; use tempfile::tempdir; #[test] @@ -149,7 +168,8 @@ mod tests { ); } - #[derive(Debug, Default, Serialize, Deserialize)] + #[derive(Debug, Default, Serialize, Deserialize, Clone)] + #[serde(deny_unknown_fields)] pub struct StubConfig { #[serde(default)] pub url: String, @@ -202,7 +222,7 @@ mod tests { assert_eq!(config.url, String::new()); } - #[derive(Debug, Default, Serialize, Deserialize)] + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct StubComplexConfig { #[serde(default)] pub url: String, @@ -212,7 +232,7 @@ mod tests { pub nested: StubComplexConfigNested, } - #[derive(Debug, Default, Serialize, Deserialize)] + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct StubComplexConfigNested { #[serde( default, @@ -250,10 +270,12 @@ mod tests { #[test] #[expect(clippy::float_cmp)] fn resolve_env_vars() { - let tempdir = - copy_config_to_tempdir("tests/data/stubtool_snfoundry.toml", Some("childdir1")); + let tempdir = copy_config_to_tempdir( + "tests/data/stubtool_complex_snfoundry.toml", + Some("childdir1"), + ); fs::copy( - "tests/data/stubtool_snfoundry.toml", + "tests/data/stubtool_complex_snfoundry.toml", tempdir.path().join("childdir1").join(CONFIG_FILENAME), ) .expect("Failed to copy config file to temp dir"); @@ -292,4 +314,29 @@ mod tests { String::from("nfsasnsidnnsailfbsbksdabdkdkl") ); } + + #[test] + fn config_with_unknown_field() { + let tempdir = copy_config_to_tempdir( + "tests/data/stubtool_with_unknown_field_snfoundry.toml", + None, + ); + + let config = load_config::( + Some(&Utf8PathBuf::try_from(tempdir.path().to_path_buf()).unwrap()), + Some(&String::from("user1")), + ); + assert!(config.is_err()); + + let err = config.unwrap_err(); + assert!( + err.to_string() + .contains("Failed to deserialize resolved config into ConfigSchema") + ); + assert!( + err.root_cause() + .to_string() + .contains("unknown field `non-existing-field`") + ); + } } diff --git a/crates/configuration/tests/data/stubtool_complex_snfoundry.toml b/crates/configuration/tests/data/stubtool_complex_snfoundry.toml new file mode 100644 index 0000000000..7c574c8db3 --- /dev/null +++ b/crates/configuration/tests/data/stubtool_complex_snfoundry.toml @@ -0,0 +1,8 @@ +[stubtool.with-envs] +url = "$VALUE_STRING123132" +account = "$VALUE_INT123132" + +[stubtool.with-envs.nested] +list-example = [ "$VALUE_BOOL1231321", "$VALUE_BOOL1231322" ] +url-nested = "$VALUE_FLOAT123132" +url-alt = "${VALUE_STRING123142}" diff --git a/crates/configuration/tests/data/stubtool_snfoundry.toml b/crates/configuration/tests/data/stubtool_snfoundry.toml index fee0f6a55e..cd9fcc1c02 100644 --- a/crates/configuration/tests/data/stubtool_snfoundry.toml +++ b/crates/configuration/tests/data/stubtool_snfoundry.toml @@ -1,6 +1,5 @@ [stubtool.default] url = "http://127.0.0.1:5055/rpc" -accounts-file = "../account-file" account = "user1" [stubtool.profile1] @@ -9,27 +8,16 @@ account = "user3" [stubtool.profile2] url = "http://127.0.0.1:5055/rpc" -accounts-file = "../account-file" account = "user100" [stubtool.profile3] url = "http://127.0.0.1:5055/rpc" account = "/path/to/account.json" -keystore = "../keystore" [stubtool.profile4] url = "http://127.0.0.1:5055/rpc" -accounts-file = "../account-file" account = "user3" [stubtool.profile5] url = "http://127.0.0.1:5055/rpc" account = "user8" - -[stubtool.with-envs] -url = "$VALUE_STRING123132" -account = "$VALUE_INT123132" -[stubtool.with-envs.nested] -list-example = [ "$VALUE_BOOL1231321", "$VALUE_BOOL1231322" ] -url-nested = "$VALUE_FLOAT123132" -url-alt = "${VALUE_STRING123142}" diff --git a/crates/configuration/tests/data/stubtool_with_unknown_field_snfoundry.toml b/crates/configuration/tests/data/stubtool_with_unknown_field_snfoundry.toml new file mode 100644 index 0000000000..bc6b378ccb --- /dev/null +++ b/crates/configuration/tests/data/stubtool_with_unknown_field_snfoundry.toml @@ -0,0 +1,4 @@ +[stubtool.user1] +url = "http://127.0.0.1:5055/rpc" +account = "user1" +non-existing-field = "xyz" diff --git a/crates/sncast/src/helpers/configuration.rs b/crates/sncast/src/helpers/configuration.rs index 63bb209dc3..6e9ccfd1de 100644 --- a/crates/sncast/src/helpers/configuration.rs +++ b/crates/sncast/src/helpers/configuration.rs @@ -42,6 +42,7 @@ impl NetworksConfig { } #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] pub struct CastConfig { #[serde(default)] /// RPC url diff --git a/crates/sncast/tests/e2e/show_config.rs b/crates/sncast/tests/e2e/show_config.rs index bfe54d4383..6ea7096d71 100644 --- a/crates/sncast/tests/e2e/show_config.rs +++ b/crates/sncast/tests/e2e/show_config.rs @@ -46,6 +46,7 @@ async fn test_show_config_from_cli() { Wait Timeout: 2s Wait Retry Interval: 1s Show Explorer Links: true + Block Explorer: StarkScan ", URL}); }