Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/configuration/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anyhow::anyhow;
use serde_json::Number;
use std::env;

fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result<serde_json::Value> {
pub fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result<serde_json::Value> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result<serde_json::Value> {
pub(crate) fn resolve_env_variables(config: serde_json::Value) -> anyhow::Result<serde_json::Value> {

If you need this in other modules pub is okay, but if it remains just for uses in this crate, let's not publish it.

match config {
serde_json::Value::Object(map) => {
let val = map
Expand Down
111 changes: 79 additions & 32 deletions crates/configuration/src/lib.rs
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;
Expand All @@ -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(|_| {
Expand All @@ -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>
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was specifically refactored so that load_config can actually be shared between this top level function and forge which uses it directly.

If we cannot share logic between cast and forge at all, let's get rid of core::load_config entirely and use the primitives from core directly in forge logic.

)
}
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> {
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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`")
);
}
}
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}"
12 changes: 0 additions & 12 deletions crates/configuration/tests/data/stubtool_snfoundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[stubtool.default]
url = "http://127.0.0.1:5055/rpc"
accounts-file = "../account-file"
account = "user1"

[stubtool.profile1]
Expand All @@ -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}"
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"
1 change: 1 addition & 0 deletions crates/sncast/src/helpers/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl NetworksConfig {
}

#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct CastConfig {
#[serde(default)]
/// RPC url
Expand Down
1 change: 1 addition & 0 deletions crates/sncast/tests/e2e/show_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

Expand Down
Loading