fraiseql_cli/config/
mod.rs1pub mod runtime;
7pub mod security;
8pub mod toml_schema;
9
10use std::path::Path;
11
12use anyhow::{Context, Result};
13use fraiseql_core::schema::NamingConvention;
14pub use runtime::{DatabaseRuntimeConfig, ServerRuntimeConfig};
15pub use security::SecurityConfig;
16use serde::{Deserialize, Serialize};
17pub use toml_schema::TomlSchema;
18use tracing::info;
19
20#[derive(Debug, Clone, Default, Deserialize, Serialize)]
22#[serde(default, deny_unknown_fields)]
23pub struct TomlProjectConfig {
24 #[serde(rename = "project")]
26 pub project: ProjectConfig,
27
28 #[serde(rename = "fraiseql")]
30 pub fraiseql: FraiseQLSettings,
31
32 #[serde(default)]
34 pub server: ServerRuntimeConfig,
35
36 #[serde(default)]
38 pub database: DatabaseRuntimeConfig,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
43#[serde(default, deny_unknown_fields)]
44pub struct ProjectConfig {
45 pub name: String,
47 pub version: String,
49 pub description: Option<String>,
51 pub database_target: Option<String>,
53}
54
55impl Default for ProjectConfig {
56 fn default() -> Self {
57 Self {
58 name: "my-fraiseql-app".to_string(),
59 version: "1.0.0".to_string(),
60 description: None,
61 database_target: None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Deserialize, Serialize)]
68#[serde(default, deny_unknown_fields)]
69pub struct FraiseQLSettings {
70 pub schema_file: String,
72 pub output_file: String,
74 #[serde(rename = "security")]
76 pub security: SecurityConfig,
77 #[serde(default)]
79 pub tenancy: security::TenancyTomlConfig,
80 #[serde(default)]
82 pub mutations: MutationsConfig,
83 #[serde(default)]
85 pub naming: NamingTomlConfig,
86}
87
88impl Default for FraiseQLSettings {
89 fn default() -> Self {
90 Self {
91 schema_file: "schema.json".to_string(),
92 output_file: "schema.compiled.json".to_string(),
93 security: SecurityConfig::default(),
94 tenancy: security::TenancyTomlConfig::default(),
95 mutations: MutationsConfig::default(),
96 naming: NamingTomlConfig::default(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Deserialize, Serialize)]
103#[serde(default, deny_unknown_fields)]
104pub struct NamingTomlConfig {
105 pub convention: NamingConvention,
114 pub acronyms: Vec<String>,
119}
120
121impl Default for NamingTomlConfig {
122 fn default() -> Self {
123 Self {
124 convention: NamingConvention::CamelCase,
127 acronyms: Vec::new(),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Default, Deserialize, Serialize)]
134#[serde(default, deny_unknown_fields)]
135pub struct MutationsConfig {
136 pub auto_error_union: bool,
142}
143
144impl TomlProjectConfig {
145 pub fn from_file(path: &str) -> Result<Self> {
154 info!("Loading configuration from {path}");
155
156 let path = Path::new(path);
157 if !path.exists() {
158 anyhow::bail!("Configuration file not found: {}", path.display());
159 }
160
161 let raw = std::fs::read_to_string(path).context("Failed to read fraiseql.toml")?;
162 let toml_content = expand_env_vars(&raw)?;
163
164 let config: TomlProjectConfig = toml::from_str(&toml_content)
165 .map_err(|e| anyhow::anyhow!("Failed to parse fraiseql.toml: {e}"))?;
166
167 Ok(config)
168 }
169
170 pub fn validate(&self) -> Result<()> {
177 info!("Validating configuration");
178 self.fraiseql.security.validate()?;
179 self.fraiseql.tenancy.validate()?;
180 self.server.validate()?;
181 self.database.validate()?;
182 Ok(())
183 }
184}
185
186#[allow(clippy::expect_used)] pub(crate) fn expand_env_vars(content: &str) -> Result<String> {
191 use std::sync::LazyLock;
192
193 static ENV_VAR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
194 regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").expect("env var regex is valid")
195 });
196
197 let mut result = String::with_capacity(content.len());
198 let mut last_end = 0;
199
200 for cap in ENV_VAR_REGEX.captures_iter(content) {
201 let m = cap.get(0).expect("INVARIANT: Regex captures group 0 is always present");
204 result.push_str(&content[last_end..m.start()]);
205 let var_name = &cap[1];
206 match std::env::var(var_name) {
207 Ok(val) => {
208 validate_env_var_value(var_name, &val)?;
209 result.push_str(&val);
210 },
211 Err(_) => {
212 result.push_str(&format!("${{{}}}", var_name));
213 },
214 }
215 last_end = m.end();
216 }
217 result.push_str(&content[last_end..]);
218 Ok(result)
219}
220
221fn validate_env_var_value(var_name: &str, value: &str) -> Result<()> {
222 if value.contains('\n') {
223 anyhow::bail!("Environment variable {} contains newline character", var_name);
224 }
225 if value.contains('\r') {
226 anyhow::bail!("Environment variable {} contains carriage return character", var_name);
227 }
228 if value.contains('\0') {
229 anyhow::bail!("Environment variable {} contains null character", var_name);
230 }
231 if value.contains('"')
233 || value.contains('\'')
234 || value.contains('\\')
235 || value.contains(']')
236 || value.contains('[')
237 || value.contains('{')
238 || value.contains('}')
239 {
240 anyhow::bail!(
241 "Environment variable {} contains TOML metacharacter that could break TOML parsing",
242 var_name
243 );
244 }
245 Ok(())
246}
247
248#[cfg(test)]
249mod tests;