-
Notifications
You must be signed in to change notification settings - Fork 240
Validate snfoundry.toml #3869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Validate snfoundry.toml #3869
Changes from all commits
7c30c85
070e167
4bab61a
9919b98
689ce36
606a9c8
43e9bca
d03e7c3
06c902d
3bd4586
fdde1f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
franciszekjob marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T> { | ||
| #[serde(flatten)] | ||
| pub tools: HashMap<String, ToolProfiles<T>>, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| struct ToolProfiles<T> { | ||
| #[serde(flatten)] | ||
| pub profiles: HashMap<String, T>, | ||
| } | ||
|
|
||
| #[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<T: Config + Default>( | ||
| path: Option<&Utf8PathBuf>, | ||
| profile: Option<&str>, | ||
| ) -> Result<T> { | ||
| let config_path = path | ||
| pub fn load_config<T>(path: Option<&Utf8PathBuf>, profile: Option<&str>) -> Result<T> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe rename this something it suggests more it's loading form snfoundry_toml? |
||
| 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::<Value>() | ||
| .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())), | ||
|
Comment on lines
-53
to
-55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code was specifically refactored so that If we cannot share logic between cast and forge at all, let's get rid of |
||
| ) | ||
| } | ||
| 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<T> = 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<Utf8PathBuf> { | ||
|
|
@@ -79,11 +99,10 @@ pub fn find_config_file() -> Result<Utf8PathBuf> { | |
|
|
||
| #[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::<StubConfig>( | ||
| 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`") | ||
| ); | ||
| } | ||
| } | ||
franciszekjob marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [stubtool.user1] | ||
| url = "http://127.0.0.1:5055/rpc" | ||
| account = "user1" | ||
| non-existing-field = "xyz" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need this in other modules
pubis okay, but if it remains just for uses in this crate, let's not publish it.