Files
dbx/crates/dbx-core/src/config/expression.rs
T

308 lines
11 KiB
Rust

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Expression {
EnvVar(String),
Ref(String),
Eval(String),
Literal(String),
}
pub fn parse_expression(input: &str) -> Expression {
let trimmed = input.trim();
if trimmed.starts_with("${env:") && trimmed.ends_with('}') {
let inner = &trimmed[6..trimmed.len() - 1];
Expression::EnvVar(inner.to_string())
} else if trimmed.starts_with("${ref:") && trimmed.ends_with('}') {
let inner = &trimmed[6..trimmed.len() - 1];
Expression::Ref(inner.to_string())
} else if trimmed.starts_with("${eval:") && trimmed.ends_with('}') {
let inner = &trimmed[7..trimmed.len() - 1];
Expression::Eval(inner.to_string())
} else {
Expression::Literal(input.to_string())
}
}
pub fn resolve_env_var(name: &str) -> Result<String, String> {
std::env::var(name).map_err(|_| format!("Environment variable '${name}' not set"))
}
pub fn resolve_ref(path: &str, merged: &HashMap<String, serde_json::Value>) -> Result<serde_json::Value, String> {
let parts: Vec<&str> = path.split('.').collect();
let key = parts[0];
let value = merged.get(key).ok_or_else(|| format!("Config reference '${path}' not found in merged config"))?;
if parts.len() == 1 {
return Ok(value.clone());
}
let mut current = value;
for part in &parts[1..] {
match current {
serde_json::Value::Object(map) => {
current =
map.get(*part).ok_or_else(|| format!("Config reference '${path}': key '{part}' not found"))?;
}
_ => {
return Err(format!("Config reference '${path}': intermediate value is not an object"));
}
}
}
Ok(current.clone())
}
/// Parses JSON literals only (numbers, booleans, null, quoted strings).
/// Does NOT support arithmetic, operators, or function calls.
fn eval_simple(expr: &str) -> Result<serde_json::Value, String> {
let expr = expr.trim();
if let Ok(v) = expr.parse::<i64>() {
return Ok(serde_json::Value::Number(v.into()));
}
if let Ok(v) = expr.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(v) {
return Ok(serde_json::Value::Number(n));
}
}
if expr == "true" {
return Ok(serde_json::Value::Bool(true));
}
if expr == "false" {
return Ok(serde_json::Value::Bool(false));
}
if expr == "null" {
return Ok(serde_json::Value::Null);
}
if expr.starts_with('"') && expr.ends_with('"') && expr.len() >= 2 {
return Ok(serde_json::Value::String(expr[1..expr.len() - 1].to_string()));
}
Err(format!("Cannot evaluate expression: '{expr}'"))
}
pub fn resolve_expression(
expression: &Expression,
merged: &HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value, String> {
match expression {
Expression::EnvVar(name) => {
let val = resolve_env_var(name)?;
Ok(serde_json::Value::String(val))
}
Expression::Ref(path) => resolve_ref(path, merged),
Expression::Eval(expr) => eval_simple(expr),
Expression::Literal(val) => Ok(serde_json::Value::String(val.clone())),
}
}
pub fn resolve_all_expressions_in_value(
value: &serde_json::Value,
merged: &HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value, String> {
match value {
serde_json::Value::String(s) => {
let expr = parse_expression(s);
resolve_expression(&expr, merged)
}
serde_json::Value::Object(map) => {
let mut resolved = serde_json::Map::new();
for (k, v) in map {
resolved.insert(k.clone(), resolve_all_expressions_in_value(v, merged)?);
}
Ok(serde_json::Value::Object(resolved))
}
serde_json::Value::Array(arr) => {
let mut resolved = Vec::new();
for v in arr {
resolved.push(resolve_all_expressions_in_value(v, merged)?);
}
Ok(serde_json::Value::Array(resolved))
}
other => Ok(other.clone()),
}
}
pub fn apply_expression_resolution(
config: &mut HashMap<String, serde_json::Value>,
merged: &HashMap<String, serde_json::Value>,
) -> Result<(), String> {
let mut resolved = HashMap::new();
for (k, v) in config.iter() {
resolved.insert(k.clone(), resolve_all_expressions_in_value(v, merged)?);
}
config.extend(resolved);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_env_var_expression() {
assert_eq!(parse_expression("${env:HOME}"), Expression::EnvVar("HOME".to_string()));
}
#[test]
fn test_parse_ref_expression() {
assert_eq!(parse_expression("${ref:database.host}"), Expression::Ref("database.host".to_string()));
}
#[test]
fn test_parse_eval_expression() {
assert_eq!(parse_expression("${eval:42}"), Expression::Eval("42".to_string()));
}
#[test]
fn test_parse_literal() {
assert_eq!(parse_expression("hello"), Expression::Literal("hello".to_string()));
assert_eq!(parse_expression("${not-an-expr}"), Expression::Literal("${not-an-expr}".to_string()));
}
#[test]
fn test_resolve_env_var() {
std::env::set_var("DBX_TEST_VAR", "test_value");
let result = resolve_env_var("DBX_TEST_VAR");
assert_eq!(result.unwrap(), "test_value");
std::env::remove_var("DBX_TEST_VAR");
}
#[test]
fn test_resolve_env_var_missing() {
let result = resolve_env_var("DBX_NONEXISTENT_VAR_12345");
assert!(result.is_err());
}
#[test]
fn test_resolve_ref_simple() {
let mut merged = HashMap::new();
merged.insert("host".to_string(), serde_json::Value::String("localhost".to_string()));
let result = resolve_ref("host", &merged).unwrap();
assert_eq!(result, serde_json::Value::String("localhost".to_string()));
}
#[test]
fn test_resolve_ref_nested() {
let mut merged = HashMap::new();
let mut db = serde_json::Map::new();
db.insert("host".to_string(), serde_json::Value::String("pg.example.com".to_string()));
db.insert("port".to_string(), serde_json::Value::Number(serde_json::Number::from(5432)));
merged.insert("database".to_string(), serde_json::Value::Object(db));
let host = resolve_ref("database.host", &merged).unwrap();
assert_eq!(host, serde_json::Value::String("pg.example.com".to_string()));
let port = resolve_ref("database.port", &merged).unwrap();
assert_eq!(port, serde_json::Value::Number(serde_json::Number::from(5432)));
}
#[test]
fn test_resolve_ref_missing() {
let merged = HashMap::new();
let result = resolve_ref("nonexistent", &merged);
assert!(result.is_err());
}
#[test]
fn test_eval_simple_integer() {
let result = eval_simple("42").unwrap();
assert_eq!(result, serde_json::Value::Number(serde_json::Number::from(42)));
}
#[test]
fn test_eval_simple_bool() {
assert_eq!(eval_simple("true").unwrap(), serde_json::Value::Bool(true));
assert_eq!(eval_simple("false").unwrap(), serde_json::Value::Bool(false));
}
#[test]
fn test_eval_simple_null() {
assert_eq!(eval_simple("null").unwrap(), serde_json::Value::Null);
}
#[test]
fn test_eval_simple_string() {
let result = eval_simple(r#""hello world""#).unwrap();
assert_eq!(result, serde_json::Value::String("hello world".to_string()));
}
#[test]
fn test_eval_simple_invalid() {
assert!(eval_simple("some_random_text").is_err());
}
#[test]
fn test_resolve_expression_env_var() {
std::env::set_var("DBX_TEST_PORT", "8080");
let expr = Expression::EnvVar("DBX_TEST_PORT".to_string());
let merged = HashMap::new();
let result = resolve_expression(&expr, &merged).unwrap();
assert_eq!(result, serde_json::Value::String("8080".to_string()));
std::env::remove_var("DBX_TEST_PORT");
}
#[test]
fn test_resolve_expression_ref() {
let mut merged = HashMap::new();
merged.insert("host".to_string(), serde_json::Value::String("db.local".to_string()));
let expr = Expression::Ref("host".to_string());
let result = resolve_expression(&expr, &merged).unwrap();
assert_eq!(result, serde_json::Value::String("db.local".to_string()));
}
#[test]
fn test_resolve_expression_eval() {
let expr = Expression::Eval("true".to_string());
let merged = HashMap::new();
let result = resolve_expression(&expr, &merged).unwrap();
assert_eq!(result, serde_json::Value::Bool(true));
}
#[test]
fn test_resolve_expression_literal() {
let expr = Expression::Literal("hello".to_string());
let merged = HashMap::new();
let result = resolve_expression(&expr, &merged).unwrap();
assert_eq!(result, serde_json::Value::String("hello".to_string()));
}
#[test]
fn test_resolve_all_in_nested_object() {
let mut merged = HashMap::new();
merged.insert("default_host".to_string(), serde_json::Value::String("pg.example.com".to_string()));
let mut obj = serde_json::Map::new();
obj.insert("host".to_string(), serde_json::Value::String("${ref:default_host}".to_string()));
obj.insert("port".to_string(), serde_json::Value::Number(serde_json::Number::from(5432)));
obj.insert("debug".to_string(), serde_json::Value::String("${eval:true}".to_string()));
let input = serde_json::Value::Object(obj);
let result = resolve_all_expressions_in_value(&input, &merged).unwrap();
let obj = result.as_object().unwrap();
assert_eq!(obj.get("host").unwrap(), &serde_json::Value::String("pg.example.com".to_string()));
assert_eq!(obj.get("port").unwrap(), &serde_json::Value::Number(serde_json::Number::from(5432)));
assert_eq!(obj.get("debug").unwrap(), &serde_json::Value::Bool(true));
}
#[test]
fn test_apply_expression_resolution() {
let mut merged = HashMap::new();
merged.insert("base_url".to_string(), serde_json::Value::String("https://api.example.com".to_string()));
let mut config = HashMap::new();
config.insert("url".to_string(), serde_json::Value::String("${ref:base_url}".to_string()));
config.insert("timeout".to_string(), serde_json::Value::String("${eval:30}".to_string()));
apply_expression_resolution(&mut config, &merged).unwrap();
assert_eq!(config.get("url").unwrap(), &serde_json::Value::String("https://api.example.com".to_string()));
assert_eq!(config.get("timeout").unwrap(), &serde_json::Value::Number(serde_json::Number::from(30)));
}
}