Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
19 changes: 13 additions & 6 deletions crates/configuration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::validation::validate_config;
use anyhow::{Context, Result, anyhow};
use camino::Utf8PathBuf;
use scarb_metadata::{Metadata, PackageId};
use serde_json::{Map, Number};
use std::fs::File;
use std::{env, fs};
use tempfile::{TempDir, tempdir};
use toml::Value;

mod validation;

pub const CONFIG_FILENAME: &str = "snfoundry.toml";

/// Defined in snfoundry.toml
Expand Down Expand Up @@ -75,20 +78,24 @@ pub fn load_config<T: Config + Default>(

match config_path {
Some(path) => {
let raw_config_toml = fs::read_to_string(path)
.context("Failed to read snfoundry.toml config file")?
.parse::<Value>()
let raw_config_toml =
fs::read_to_string(path).context("Failed to read snfoundry.toml config file")?;

let config_toml: toml::Value = toml::from_str(&raw_config_toml)
.context("Failed to parse snfoundry.toml config file")?;

let raw_config_json = serde_json::to_value(raw_config_toml)
validate_config(&config_toml)?;

let config_json = serde_json::to_value(config_toml)
.context("Conversion from TOML value to JSON value should not fail.")?;

let profile = get_profile(raw_config_json, T::tool_name(), profile)?;
let profile = get_profile(config_json, T::tool_name(), profile)?;
T::from_raw(resolve_env_variables(profile)?)
}
None => Ok(T::default()),
}
}

/// Loads config for a specific package from the `Scarb.toml` file
/// # Arguments
/// * `metadata` - Scarb metadata object
Expand Down
130 changes: 130 additions & 0 deletions crates/configuration/src/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use anyhow::{Result, anyhow};

pub fn validate_config(config_toml: &toml::Value) -> Result<()> {
let root = config_toml
.as_table()
.ok_or_else(|| anyhow!("Root of TOML file must be a table"))?;

let allowed_keys = [
"url",
"accounts-file",
"account",
"keystore",
"wait-params",
"block-explorer",
"show-explorer-links",
];

for (section_name, section_value) in root {
// Only [sncast.<profile>] sections are allowed
if section_name != "sncast" {
return Err(anyhow!(
"Invalid section [{section_name}]. All top-level sections must start with 'sncast.' (e.g. [sncast.default])"
));
}

let section = section_value.as_table().ok_or_else(|| {
anyhow!(
"Section [{section_name}] must be a table (e.g. key-value pairs inside [sncast.default])"
)
})?;

for profile_name in section.keys() {
let profile = section
.get(profile_name)
.and_then(|v| v.as_table())
.ok_or_else(|| {
anyhow!(
"Profile [{profile_name}] in [{section_name}] must be a table (e.g. key-value pairs)"
)
})?;

for key in profile.keys() {
if !allowed_keys.contains(&key.as_str()) {
return Err(anyhow!(
"Invalid key `{}` in [{}.{}]. Allowed keys are: {}",
key,
section_name,
profile_name,
allowed_keys.join(", ")
));
}
}
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::validate_config;
#[test]
fn test_correct_config() {
let toml_str = r#"
[sncast.default]
url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_9"
accounts-file = "../account-file"
account = "mainuser"
keystore = "~/keystore"
wait-params = { timeout = 300, retry-interval = 10 }
block-explorer = "StarkScan"
show-explorer-links = true

[sncast.profile1]
url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_9"
accounts-file = "../account-file"
account = "mainuser"
keystore = "~/keystore"
wait-params = { timeout = 300, retry-interval = 10 }
block-explorer = "StarkScan"
show-explorer-links = true
"#;

let parsed_toml: toml::Value = toml::from_str(toml_str).unwrap();
assert!(validate_config(&parsed_toml).is_ok());
}

#[test]
fn test_wrong_top_level_section() {
let toml_str = r#"
[xyz]
url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_9"
accounts-file = "../account-file"
account = "mainuser"
keystore = "~/keystore"
wait-params = { timeout = 300, retry-interval = 10 }
block-explorer = "StarkScan"
show-explorer-links = true
"#;

let parsed_toml: toml::Value = toml::from_str(toml_str).unwrap();
let result = validate_config(&parsed_toml);

assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains(
"Invalid section [xyz]. All top-level sections must start with 'sncast.'"
)
);
}

#[test]
fn test_wrong_key() {
let toml_str = r#"
[sncast.default]
some-key = "some value"
"#;

let parsed_toml: toml::Value = toml::from_str(toml_str).unwrap();
let result = validate_config(&parsed_toml);

assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid key `some-key` in [sncast.default]. Allowed keys are:")
);
}
}
Loading