use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct DevKitConfig {
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub decode: DecodeConfig,
#[serde(default)]
pub storage: StorageConfig,
#[serde(default)]
pub contracts: std::collections::HashMap<String, ContractConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub package: Option<PackageConfig>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub dependencies: std::collections::HashMap<String, Dependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ContractConfig {
pub path: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deploy_after: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PackageConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum GitReference {
Tag(String),
Branch(String),
Rev(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct Dependency {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
pub type LocalDependency = Dependency;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkConfig {
pub rpc_url: String,
pub passphrase: String,
pub timeout_secs: Option<u64>,
pub pool_max_idle_per_host: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecodeConfig {
pub max_depth: usize,
pub allow_fallback_hex: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StorageConfig {
pub max_entries: usize,
pub ttl_warning_days: u32,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
max_entries: 200,
ttl_warning_days: 30,
}
}
}
impl Default for DecodeConfig {
fn default() -> Self {
Self {
max_depth: 32,
allow_fallback_hex: true,
}
}
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
rpc_url: "https://soroban-testnet.stellar.org".to_string(),
passphrase: "Test SDF Network ; September 2015".to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
}
}
}
impl DevKitConfig {
pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
toml::from_str(content)
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let path = path.as_ref();
if path.exists() {
let content = fs::read_to_string(path)?;
let config = Self::from_toml(&content)?;
Ok(config)
} else {
Ok(Self::default())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let default_config = DevKitConfig::default();
assert_eq!(default_config.decode.max_depth, 32);
assert!(default_config.decode.allow_fallback_hex);
assert_eq!(default_config.storage.max_entries, 200);
assert_eq!(default_config.storage.ttl_warning_days, 30);
}
#[test]
fn test_parse_valid_toml() {
let toml_data = r#"
[network]
rpc_url = "http://localhost:8000"
passphrase = "Standalone Network"
[decode]
max_depth = 16
allow_fallback_hex = false
[storage]
max_entries = 100
ttl_warning_days = 7
"#;
let parsed = DevKitConfig::from_toml(toml_data).unwrap();
assert_eq!(parsed.network.rpc_url, "http://localhost:8000");
assert_eq!(parsed.decode.max_depth, 16);
assert!(!parsed.decode.allow_fallback_hex);
assert_eq!(parsed.storage.max_entries, 100);
assert_eq!(parsed.storage.ttl_warning_days, 7);
}
#[test]
fn test_old_toml_without_storage() {
let toml_data = r#"
[network]
rpc_url = "http://localhost:8000"
passphrase = "Standalone Network"
[decode]
max_depth = 16
allow_fallback_hex = false
"#;
let parsed = DevKitConfig::from_toml(toml_data).unwrap();
assert_eq!(parsed.storage.max_entries, 200);
}
#[test]
fn test_duplicate_contract_name_rejected() {
let toml_data = r#"
[contracts.token]
path = "contracts/token"
[contracts.token]
path = "contracts/token2"
"#;
let parsed = DevKitConfig::from_toml(toml_data);
assert!(
parsed.is_err(),
"duplicate contract name must fail to parse"
);
}
#[test]
fn test_depends_on_field_parses() {
let toml_data = r#"
[contracts.token]
path = "contracts/token"
[contracts.router]
path = "contracts/router"
depends_on = ["token"]
"#;
let parsed = DevKitConfig::from_toml(toml_data).unwrap();
assert_eq!(
parsed.contracts.get("router").unwrap().depends_on,
vec!["token".to_string()]
);
assert!(parsed
.contracts
.get("router")
.unwrap()
.deploy_after
.is_empty());
}
}