#![allow(clippy::pedantic)]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ComponentConfig {
pub name: String,
pub kind: String,
#[serde(default)]
pub depends_on: Vec<String>,
#[serde(default)]
pub required: bool,
#[serde(default = "serde_json::Value::default")]
pub config: serde_json::Value,
#[serde(default)]
pub tags: Vec<String>,
}
impl ComponentConfig {
#[must_use]
pub fn new(name: impl Into<String>, kind: impl Into<String>) -> Self {
Self {
name: name.into(),
kind: kind.into(),
depends_on: Vec::new(),
required: false,
config: serde_json::Value::Null,
tags: Vec::new(),
}
}
#[must_use]
pub fn with_config(mut self, config: serde_json::Value) -> Self {
self.config = config;
self
}
#[must_use]
pub fn with_dependency(mut self, dep: impl Into<String>) -> Self {
self.depends_on.push(dep.into());
self
}
#[must_use]
pub fn required(mut self) -> Self {
self.required = true;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ComponentFile {
#[serde(default)]
pub component: Vec<ComponentConfig>,
}
impl ComponentFile {
pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
toml::from_str(text)
}
pub fn from_json(text: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(text)
}
#[allow(dead_code, unused_variables)]
pub fn from_yaml(text: &str) -> Result<Self, String> {
Err("YAML support is not compiled in; use TOML or JSON".to_string())
}
#[must_use]
pub fn len(&self) -> usize {
self.component.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.component.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn component_config_builder_chains() {
let cfg = ComponentConfig::new("openai-primary", "provider.openai")
.with_config(serde_json::json!({ "api_key": "sk-test" }))
.with_dependency("redis-sessions")
.required();
assert_eq!(cfg.name, "openai-primary");
assert_eq!(cfg.kind, "provider.openai");
assert_eq!(cfg.depends_on, vec!["redis-sessions".to_string()]);
assert!(cfg.required);
assert_eq!(cfg.config["api_key"], "sk-test");
}
#[test]
fn component_file_parses_toml() {
let toml_str = r#"
[[component]]
name = "openai-primary"
kind = "provider.openai"
config = { api_key = "sk-test" }
[[component]]
name = "redis-sessions"
kind = "store.session"
config = { url = "redis://localhost" }
depends_on = []
"#;
let file = match ComponentFile::from_toml(toml_str) {
Ok(f) => f,
Err(e) => panic!("parse toml: {e}"),
};
assert_eq!(file.component.len(), 2);
assert_eq!(file.component[0].name, "openai-primary");
assert_eq!(file.component[1].depends_on.len(), 0);
}
#[test]
fn component_file_is_empty_by_default() {
let file = ComponentFile::default();
assert!(file.is_empty());
assert_eq!(file.len(), 0);
}
#[test]
fn component_file_parses_json() {
let json_str = r#"{
"component": [
{"name": "x", "kind": "k", "config": {"a": 1}}
]
}"#;
let file = match ComponentFile::from_json(json_str) {
Ok(f) => f,
Err(e) => panic!("parse json: {e}"),
};
assert_eq!(file.component.len(), 1);
assert_eq!(file.component[0].config["a"], 1);
}
}