use miette::{IntoDiagnostic, Result, WrapErr};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub project: ProjectConfig,
#[serde(default)]
pub contract: ContractConfig,
#[serde(default)]
pub build: BuildConfig,
#[serde(default)]
pub solana: SolanaConfig,
#[serde(default)]
pub dependencies: BTreeMap<String, Dependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub repository: Option<String>,
}
fn default_version() -> String {
"0.1.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContractConfig {
#[serde(default = "default_main")]
pub main: String,
#[serde(default)]
pub name: Option<String>,
}
fn default_main() -> String {
"src/main.sol".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BuildConfig {
#[serde(default = "default_output")]
pub output: String,
}
fn default_output() -> String {
"output".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SolanaConfig {
#[serde(default = "default_cluster")]
pub cluster: String,
}
fn default_cluster() -> String {
"devnet".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Version(String),
Detailed(DependencySpec),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DependencySpec {
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub git: Option<String>,
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub rev: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub github: Option<String>,
}
impl Dependency {
pub fn version(&self) -> Option<&str> {
match self {
Dependency::Version(v) => Some(v),
Dependency::Detailed(spec) => spec.version.as_deref(),
}
}
pub fn is_git(&self) -> bool {
match self {
Dependency::Version(_) => false,
Dependency::Detailed(spec) => spec.git.is_some() || spec.github.is_some(),
}
}
pub fn git_url(&self) -> Option<String> {
match self {
Dependency::Version(_) => None,
Dependency::Detailed(spec) => {
if let Some(url) = &spec.git {
Some(url.clone())
} else {
spec.github
.as_ref()
.map(|repo| format!("https://github.com/{}.git", repo))
}
}
}
}
pub fn git_ref(&self) -> Option<String> {
match self {
Dependency::Version(_) => None,
Dependency::Detailed(spec) => spec
.tag
.clone()
.or_else(|| spec.branch.clone())
.or_else(|| spec.rev.clone()),
}
}
pub fn is_path(&self) -> bool {
match self {
Dependency::Version(_) => false,
Dependency::Detailed(spec) => spec.path.is_some(),
}
}
pub fn local_path(&self) -> Option<&str> {
match self {
Dependency::Version(_) => None,
Dependency::Detailed(spec) => spec.path.as_deref(),
}
}
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to read config file: {}", path.display()))?;
toml::from_str(&content)
.into_diagnostic()
.wrap_err("Failed to parse solscript.toml")
}
pub fn save(&self, path: &Path) -> Result<()> {
let content = toml::to_string_pretty(self)
.into_diagnostic()
.wrap_err("Failed to serialize configuration")?;
std::fs::write(path, content)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to write config file: {}", path.display()))
}
pub fn find(start: &Path) -> Option<std::path::PathBuf> {
let mut current = start.to_path_buf();
loop {
let config_path = current.join("solscript.toml");
if config_path.exists() {
return Some(config_path);
}
if !current.pop() {
return None;
}
}
}
pub fn add_dependency(&mut self, name: String, dep: Dependency) {
self.dependencies.insert(name, dep);
}
pub fn remove_dependency(&mut self, name: &str) -> Option<Dependency> {
self.dependencies.remove(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_dependency() {
let toml_str = r#"
[project]
name = "test"
[dependencies]
token = "1.0.0"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert!(config.dependencies.contains_key("token"));
assert_eq!(config.dependencies["token"].version(), Some("1.0.0"));
}
#[test]
fn test_parse_git_dependency() {
let toml_str = r#"
[project]
name = "test"
[dependencies]
token = { github = "cryptuon/token-lib", tag = "v1.0.0" }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let dep = &config.dependencies["token"];
assert!(dep.is_git());
assert_eq!(
dep.git_url(),
Some("https://github.com/cryptuon/token-lib.git".to_string())
);
}
#[test]
fn test_parse_path_dependency() {
let toml_str = r#"
[project]
name = "test"
[dependencies]
mylib = { path = "../mylib" }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let dep = &config.dependencies["mylib"];
assert!(dep.is_path());
assert_eq!(dep.local_path(), Some("../mylib"));
}
}