fraiseql_cli/config/
mod.rs1pub mod runtime;
7pub mod security;
8pub mod toml_schema;
9
10use std::path::Path;
11
12use anyhow::{Context, Result};
13pub use runtime::{DatabaseRuntimeConfig, ServerRuntimeConfig};
14pub use security::SecurityConfig;
15use serde::{Deserialize, Serialize};
16pub use toml_schema::TomlSchema;
17use tracing::info;
18
19#[derive(Debug, Clone, Default, Deserialize, Serialize)]
21#[serde(default, deny_unknown_fields)]
22pub struct TomlProjectConfig {
23 #[serde(rename = "project")]
25 pub project: ProjectConfig,
26
27 #[serde(rename = "fraiseql")]
29 pub fraiseql: FraiseQLSettings,
30
31 #[serde(default)]
33 pub server: ServerRuntimeConfig,
34
35 #[serde(default)]
37 pub database: DatabaseRuntimeConfig,
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
42#[serde(default, deny_unknown_fields)]
43pub struct ProjectConfig {
44 pub name: String,
46 pub version: String,
48 pub description: Option<String>,
50 pub database_target: Option<String>,
52}
53
54impl Default for ProjectConfig {
55 fn default() -> Self {
56 Self {
57 name: "my-fraiseql-app".to_string(),
58 version: "1.0.0".to_string(),
59 description: None,
60 database_target: None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Deserialize, Serialize)]
67#[serde(default, deny_unknown_fields)]
68pub struct FraiseQLSettings {
69 pub schema_file: String,
71 pub output_file: String,
73 #[serde(rename = "security")]
75 pub security: SecurityConfig,
76 #[serde(default)]
78 pub tenancy: security::TenancyTomlConfig,
79}
80
81impl Default for FraiseQLSettings {
82 fn default() -> Self {
83 Self {
84 schema_file: "schema.json".to_string(),
85 output_file: "schema.compiled.json".to_string(),
86 security: SecurityConfig::default(),
87 tenancy: security::TenancyTomlConfig::default(),
88 }
89 }
90}
91
92impl TomlProjectConfig {
93 pub fn from_file(path: &str) -> Result<Self> {
102 info!("Loading configuration from {path}");
103
104 let path = Path::new(path);
105 if !path.exists() {
106 anyhow::bail!("Configuration file not found: {}", path.display());
107 }
108
109 let raw = std::fs::read_to_string(path).context("Failed to read fraiseql.toml")?;
110 let toml_content = expand_env_vars(&raw)?;
111
112 let config: TomlProjectConfig = toml::from_str(&toml_content)
113 .map_err(|e| anyhow::anyhow!("Failed to parse fraiseql.toml: {e}"))?;
114
115 Ok(config)
116 }
117
118 pub fn validate(&self) -> Result<()> {
125 info!("Validating configuration");
126 self.fraiseql.security.validate()?;
127 self.fraiseql.tenancy.validate()?;
128 self.server.validate()?;
129 self.database.validate()?;
130 Ok(())
131 }
132}
133
134#[allow(clippy::expect_used)] pub(crate) fn expand_env_vars(content: &str) -> Result<String> {
139 use std::sync::LazyLock;
140
141 static ENV_VAR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
142 regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").expect("env var regex is valid")
143 });
144
145 let mut result = String::with_capacity(content.len());
146 let mut last_end = 0;
147
148 for cap in ENV_VAR_REGEX.captures_iter(content) {
149 let m = cap.get(0).expect("INVARIANT: Regex captures group 0 is always present");
152 result.push_str(&content[last_end..m.start()]);
153 let var_name = &cap[1];
154 match std::env::var(var_name) {
155 Ok(val) => {
156 validate_env_var_value(var_name, &val)?;
157 result.push_str(&val);
158 },
159 Err(_) => {
160 result.push_str(&format!("${{{}}}", var_name));
161 },
162 }
163 last_end = m.end();
164 }
165 result.push_str(&content[last_end..]);
166 Ok(result)
167}
168
169fn validate_env_var_value(var_name: &str, value: &str) -> Result<()> {
170 if value.contains('\n') {
171 anyhow::bail!("Environment variable {} contains newline character", var_name);
172 }
173 if value.contains('\r') {
174 anyhow::bail!("Environment variable {} contains carriage return character", var_name);
175 }
176 if value.contains('\0') {
177 anyhow::bail!("Environment variable {} contains null character", var_name);
178 }
179 if value.contains('"')
181 || value.contains('\'')
182 || value.contains('\\')
183 || value.contains(']')
184 || value.contains('[')
185 || value.contains('{')
186 || value.contains('}')
187 {
188 anyhow::bail!(
189 "Environment variable {} contains TOML metacharacter that could break TOML parsing",
190 var_name
191 );
192 }
193 Ok(())
194}
195
196#[cfg(test)]
197mod tests;