shape_runtime/
extensions_config.rs1use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct ExtensionsConfig {
18 #[serde(default)]
20 pub extensions: Vec<ExtensionEntry>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ExtensionEntry {
26 pub name: String,
28 pub path: PathBuf,
30 #[serde(default)]
32 pub config: HashMap<String, toml::Value>,
33}
34
35impl ExtensionEntry {
36 pub fn config_as_json(&self) -> serde_json::Value {
38 toml_to_json(&toml::Value::Table(
39 self.config
40 .iter()
41 .map(|(k, v)| (k.clone(), v.clone()))
42 .collect(),
43 ))
44 }
45}
46
47pub fn load_extensions_config() -> Result<ExtensionsConfig> {
51 let config_path = get_config_path()?;
52 if !config_path.exists() {
53 return Ok(ExtensionsConfig::default());
54 }
55 load_extensions_config_from(&config_path)
56}
57
58pub fn load_extensions_config_from(path: &Path) -> Result<ExtensionsConfig> {
60 let content = std::fs::read_to_string(path)
61 .with_context(|| format!("Failed to read extension config from {:?}", path))?;
62 let config: ExtensionsConfig = toml::from_str(&content)
63 .with_context(|| format!("Failed to parse extension config from {:?}", path))?;
64 Ok(config)
65}
66
67pub fn get_config_path() -> Result<PathBuf> {
69 if let Ok(config_dir) = std::env::var("SHAPE_CONFIG_DIR") {
70 return Ok(PathBuf::from(config_dir).join("extensions.toml"));
71 }
72
73 let config_dir = dirs::config_dir()
74 .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
75 Ok(config_dir.join("shape").join("extensions.toml"))
76}
77
78fn toml_to_json(value: &toml::Value) -> serde_json::Value {
79 match value {
80 toml::Value::String(s) => serde_json::Value::String(s.clone()),
81 toml::Value::Integer(i) => serde_json::Value::Number((*i).into()),
82 toml::Value::Float(f) => serde_json::Number::from_f64(*f)
83 .map(serde_json::Value::Number)
84 .unwrap_or(serde_json::Value::Null),
85 toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
86 toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()),
87 toml::Value::Array(arr) => serde_json::Value::Array(arr.iter().map(toml_to_json).collect()),
88 toml::Value::Table(table) => {
89 let map: serde_json::Map<String, serde_json::Value> = table
90 .iter()
91 .map(|(k, v)| (k.clone(), toml_to_json(v)))
92 .collect();
93 serde_json::Value::Object(map)
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_parse_empty_config() {
104 let config: ExtensionsConfig = toml::from_str("").unwrap();
105 assert!(config.extensions.is_empty());
106 }
107
108 #[test]
109 fn test_parse_single_module() {
110 let toml_str = r#"
111[[extensions]]
112name = "files"
113path = "./libshape_plugin_files.so"
114
115[extensions.config]
116base_dir = "./data"
117"#;
118
119 let config: ExtensionsConfig = toml::from_str(toml_str).unwrap();
120 assert_eq!(config.extensions.len(), 1);
121 assert_eq!(config.extensions[0].name, "files");
122 assert_eq!(
123 config.extensions[0].path,
124 PathBuf::from("./libshape_plugin_files.so")
125 );
126
127 let json_config = config.extensions[0].config_as_json();
128 assert_eq!(json_config["base_dir"], "./data");
129 }
130
131 #[test]
132 fn test_parse_multiple_modules() {
133 let toml_str = r#"
134[[extensions]]
135name = "market-data"
136path = "./libshape_plugin_market_data.so"
137
138[extensions.config]
139duckdb_path = "/path/to/market.duckdb"
140default_timeframe = "1d"
141read_only = true
142
143[[extensions]]
144name = "files"
145path = "./libshape_plugin_files.so"
146
147[extensions.config]
148base_dir = "./data"
149"#;
150
151 let config: ExtensionsConfig = toml::from_str(toml_str).unwrap();
152 assert_eq!(config.extensions.len(), 2);
153 assert_eq!(config.extensions[0].name, "market-data");
154 let json0 = config.extensions[0].config_as_json();
155 assert_eq!(json0["duckdb_path"], "/path/to/market.duckdb");
156 assert_eq!(json0["default_timeframe"], "1d");
157 assert_eq!(json0["read_only"], true);
158 assert_eq!(config.extensions[1].name, "files");
159 }
160}