use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleOption {
pub name: String,
pub option_type: OptionType,
#[serde(default)]
pub default: Option<serde_json::Value>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub read_only: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum OptionType {
Bool,
Int,
Str,
Float,
Path,
Package,
ListOf {
item: Box<OptionType>,
},
AttrsOf {
value: Box<OptionType>,
},
Enum {
choices: Vec<String>,
},
Submodule {
options: Vec<ModuleOption>,
},
Any,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleImport {
pub path: String,
#[serde(default)]
pub args: BTreeMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "op")]
pub enum MkExpr {
Set { value: serde_json::Value },
If {
condition: serde_json::Value,
value: serde_json::Value,
},
Force { value: serde_json::Value },
Default { value: serde_json::Value },
Merge { values: Vec<serde_json::Value> },
Order {
placement: Placement,
value: serde_json::Value,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Placement {
Before,
After,
}
#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defmodule")]
pub struct Module {
pub name: String,
#[serde(default)]
pub imports: Vec<ModuleImport>,
#[serde(default)]
pub options: Vec<ModuleOption>,
#[serde(default)]
pub config: Vec<ConfigAssignment>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigAssignment {
pub path: String,
pub expr: MkExpr,
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::{domain::TataraDomain, read};
#[test]
fn minimal_module_compiles() {
let forms = read(
r#"(defmodule
:name "stub"
:description "minimal module for tests")"#,
)
.unwrap();
let m = Module::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(m.name, "stub");
assert!(m.imports.is_empty());
assert!(m.options.is_empty());
assert!(m.config.is_empty());
}
#[test]
fn module_with_options_and_config() {
let forms = read(
r#"(defmodule
:name "observability"
:options ((:name "enable" :option-type (:kind Bool) :default true)
(:name "retention-days" :option-type (:kind Int) :default 30))
:config ((:path "services.prometheus.enable"
:expr (:op Set :value true))))"#,
)
.unwrap();
let m = Module::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(m.options.len(), 2);
assert!(matches!(m.options[0].option_type, OptionType::Bool));
assert_eq!(m.config.len(), 1);
assert_eq!(m.config[0].path, "services.prometheus.enable");
}
}