use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, info, warn};
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct ProjectBuildConfig {
pub version: Option<u32>,
#[serde(default)]
pub project: Option<ProjectConfig>,
#[serde(default)]
pub build: Option<BuildConfig>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ProjectConfig {
pub name: String,
#[serde(default = "default_access_class", alias = "visibility")]
pub access_class: String,
#[serde(default)]
pub custom_domains: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
fn default_access_class() -> String {
"public".to_string()
}
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct BuildConfig {
pub backend: Option<String>,
pub builder: Option<String>,
pub buildpacks: Option<Vec<String>>,
pub env: Option<Vec<String>>,
pub container_cli: Option<String>,
pub managed_buildkit: Option<bool>,
pub dockerfile: Option<String>,
pub build_context: Option<String>,
#[serde(default)]
pub build_contexts: Option<HashMap<String, String>>,
pub no_cache: Option<bool>,
}
pub fn load_full_project_config(app_path: &str) -> Result<Option<ProjectBuildConfig>> {
let rise_toml = Path::new(app_path).join("rise.toml");
let dot_rise_toml = Path::new(app_path).join(".rise.toml");
if rise_toml.exists() && dot_rise_toml.exists() {
warn!("Both rise.toml and .rise.toml found. Using rise.toml.");
}
let config_path = if rise_toml.exists() {
Some(rise_toml)
} else if dot_rise_toml.exists() {
Some(dot_rise_toml)
} else {
None
};
if let Some(path) = config_path {
info!("Loading project config from {}", path.display());
let content = std::fs::read_to_string(&path)?;
let mut unused_fields = Vec::new();
let deserializer = toml::Deserializer::new(&content);
let config: ProjectBuildConfig = serde_ignored::deserialize(deserializer, |path| {
unused_fields.push(path.to_string());
})?;
for field in &unused_fields {
warn!(
"Unknown configuration field in {}: {}",
path.display(),
field
);
}
if let Some(version) = config.version {
if version != 1 {
anyhow::bail!(
"Unsupported rise.toml version: {}. This CLI supports version 1.",
version
);
}
} else {
debug!("No version specified in rise.toml, using latest");
}
Ok(Some(config))
} else {
Ok(None)
}
}
pub fn write_project_config(app_path: &str, config: &ProjectBuildConfig) -> Result<()> {
let rise_toml_path = Path::new(app_path).join("rise.toml");
let toml_string = toml::to_string_pretty(config)?;
std::fs::write(&rise_toml_path, toml_string)?;
info!("Wrote project config to {}", rise_toml_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_config_with_unused_fields() {
let temp_dir = tempfile::tempdir().unwrap();
let rise_toml_path = temp_dir.path().join("rise.toml");
std::fs::write(
&rise_toml_path,
r#"
version = 1
[project]
name = "test-project"
access_class = "private"
[build]
backend = "docker"
# Unknown fields that should trigger warnings
unknown_field = "test"
another_unknown = 123
[unknown_section]
foo = "bar"
"#,
)
.unwrap();
let result = load_full_project_config(temp_dir.path().to_str().unwrap());
assert!(result.is_ok(), "Config should load despite unknown fields");
let config = result.unwrap();
assert!(config.is_some(), "Config should be present");
let config = config.unwrap();
assert_eq!(config.version, Some(1));
assert!(config.project.is_some());
assert_eq!(config.project.unwrap().name, "test-project");
}
#[test]
fn test_load_config_without_unknown_fields() {
let temp_dir = tempfile::tempdir().unwrap();
let rise_toml_path = temp_dir.path().join("rise.toml");
std::fs::write(
&rise_toml_path,
r#"
version = 1
[project]
name = "clean-project"
access_class = "public"
[build]
backend = "pack"
builder = "heroku/builder:24"
"#,
)
.unwrap();
let result = load_full_project_config(temp_dir.path().to_str().unwrap());
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.is_some());
let config = config.unwrap();
assert_eq!(config.version, Some(1));
assert!(config.project.is_some());
assert_eq!(config.project.as_ref().unwrap().name, "clean-project");
assert_eq!(config.project.as_ref().unwrap().access_class, "public");
}
}