databuild/databuild/config.rs

164 lines
5.4 KiB
Rust

use crate::JobConfig;
use crate::job::JobConfiguration;
use crate::util::DatabuildError;
use std::fs;
use std::path::Path;
/// Default idle timeout in seconds (1 hour)
pub const DEFAULT_IDLE_TIMEOUT_SECONDS: u64 = 3600;
/// Configuration file format for DataBuild application
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct DatabuildConfig {
/// Unique identifier for this graph, used for `.databuild/${graph_label}/` directory
pub graph_label: String,
/// Server auto-shutdown after this many seconds of inactivity (default: 3600)
#[serde(default = "default_idle_timeout")]
pub idle_timeout_seconds: u64,
/// BEL storage URI. Defaults to `.databuild/${graph_label}/bel.sqlite` if not specified.
///
/// Supported formats:
/// - Relative path: `path/to/bel.sqlite` (resolved relative to config file)
/// - Absolute path: `/var/data/bel.sqlite`
/// - SQLite URI: `sqlite:///path/to/bel.sqlite` or `sqlite::memory:`
/// - Future: `postgresql://user:pass@host/db`
#[serde(default)]
pub bel_uri: Option<String>,
/// List of job configurations
#[serde(default)]
pub jobs: Vec<JobConfig>,
}
fn default_idle_timeout() -> u64 {
DEFAULT_IDLE_TIMEOUT_SECONDS
}
impl DatabuildConfig {
/// Load configuration from a file, auto-detecting format from extension
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DatabuildError> {
let path = path.as_ref();
let contents = fs::read_to_string(path)
.map_err(|e| DatabuildError::from(format!("Failed to read config file: {}", e)))?;
// Determine format from file extension
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
match extension {
"json" => Self::from_json(&contents),
"toml" => Self::from_toml(&contents),
_ => Err(DatabuildError::from(format!(
"Unknown config file extension: {}. Use .json or .toml",
extension
))),
}
}
/// Parse configuration from JSON string
pub fn from_json(s: &str) -> Result<Self, DatabuildError> {
serde_json::from_str(s)
.map_err(|e| DatabuildError::from(format!("Failed to parse JSON config: {}", e)))
}
/// Parse configuration from TOML string
pub fn from_toml(s: &str) -> Result<Self, DatabuildError> {
toml::from_str(s)
.map_err(|e| DatabuildError::from(format!("Failed to parse TOML config: {}", e)))
}
/// Convert to a list of JobConfiguration
pub fn into_job_configurations(self) -> Vec<JobConfiguration> {
self.jobs.into_iter().map(|jc| jc.into()).collect()
}
/// Get the effective BEL URI, resolving defaults based on graph_label.
///
/// If `bel_uri` is not set, returns the default path `.databuild/${graph_label}/bel.sqlite`.
/// Relative paths are not resolved here - that's the caller's responsibility.
pub fn effective_bel_uri(&self) -> String {
self.bel_uri
.clone()
.unwrap_or_else(|| format!(".databuild/{}/bel.sqlite", self.graph_label))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_config() {
let json = r#"
{
"graph_label": "test_graph",
"jobs": [
{
"label": "//test:job_alpha",
"entrypoint": "/usr/bin/python3",
"environment": {"FOO": "bar"},
"partition_patterns": ["data/alpha/.*"]
}
]
}
"#;
let config = DatabuildConfig::from_json(json).unwrap();
assert_eq!(config.graph_label, "test_graph");
assert_eq!(config.idle_timeout_seconds, DEFAULT_IDLE_TIMEOUT_SECONDS);
assert_eq!(config.jobs.len(), 1);
assert_eq!(config.jobs[0].label, "//test:job_alpha");
}
#[test]
fn test_parse_json_config_with_idle_timeout() {
let json = r#"
{
"graph_label": "test_graph",
"idle_timeout_seconds": 7200,
"jobs": []
}
"#;
let config = DatabuildConfig::from_json(json).unwrap();
assert_eq!(config.graph_label, "test_graph");
assert_eq!(config.idle_timeout_seconds, 7200);
assert_eq!(config.jobs.len(), 0);
}
#[test]
fn test_effective_bel_uri() {
// Default: derives from graph_label
let config = DatabuildConfig::from_json(r#"{ "graph_label": "my_graph" }"#).unwrap();
assert_eq!(config.effective_bel_uri(), ".databuild/my_graph/bel.sqlite");
// Custom: uses provided value
let config = DatabuildConfig::from_json(
r#"{ "graph_label": "my_graph", "bel_uri": "postgresql://localhost/db" }"#,
)
.unwrap();
assert_eq!(config.effective_bel_uri(), "postgresql://localhost/db");
}
#[test]
fn test_parse_toml_config() {
let toml = r#"
graph_label = "test_graph"
[[jobs]]
label = "//test:job_alpha"
entrypoint = "/usr/bin/python3"
partition_patterns = ["data/alpha/.*"]
[jobs.environment]
FOO = "bar"
"#;
let config = DatabuildConfig::from_toml(toml).unwrap();
assert_eq!(config.graph_label, "test_graph");
assert_eq!(config.idle_timeout_seconds, DEFAULT_IDLE_TIMEOUT_SECONDS);
assert_eq!(config.jobs.len(), 1);
assert_eq!(config.jobs[0].label, "//test:job_alpha");
}
}