fraiseql_cli/config/
mod.rs1pub mod security;
7pub mod toml_schema;
8
9use std::path::Path;
10
11use anyhow::{Context, Result};
12pub use security::SecurityConfig;
13use serde::{Deserialize, Serialize};
14pub use toml_schema::TomlSchema;
15use tracing::info;
16
17#[derive(Debug, Clone, Default, Deserialize, Serialize)]
19#[serde(default)]
20pub struct FraiseQLConfig {
21 #[serde(rename = "project")]
23 pub project: ProjectConfig,
24
25 #[serde(rename = "fraiseql")]
27 pub fraiseql: FraiseQLSettings,
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(default)]
33pub struct ProjectConfig {
34 pub name: String,
36 pub version: String,
38 pub description: Option<String>,
40}
41
42impl Default for ProjectConfig {
43 fn default() -> Self {
44 Self {
45 name: "my-fraiseql-app".to_string(),
46 version: "1.0.0".to_string(),
47 description: None,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(default)]
55pub struct FraiseQLSettings {
56 pub schema_file: String,
58 pub output_file: String,
60 #[serde(rename = "security")]
62 pub security: SecurityConfig,
63}
64
65impl Default for FraiseQLSettings {
66 fn default() -> Self {
67 Self {
68 schema_file: "schema.json".to_string(),
69 output_file: "schema.compiled.json".to_string(),
70 security: SecurityConfig::default(),
71 }
72 }
73}
74
75impl FraiseQLConfig {
76 pub fn from_file(path: &str) -> Result<Self> {
78 info!("Loading configuration from {path}");
79
80 let path = Path::new(path);
81 if !path.exists() {
82 anyhow::bail!("Configuration file not found: {}", path.display());
83 }
84
85 let toml_content = std::fs::read_to_string(path).context("Failed to read fraiseql.toml")?;
86
87 let config: FraiseQLConfig = toml::from_str(&toml_content)
88 .map_err(|e| anyhow::anyhow!("Failed to parse fraiseql.toml: {e}"))?;
89
90 Ok(config)
91 }
92
93 pub fn validate(&self) -> Result<()> {
95 info!("Validating configuration");
96 self.fraiseql.security.validate()?;
97 Ok(())
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_default_config() {
107 let config = FraiseQLConfig::default();
108 assert_eq!(config.project.name, "my-fraiseql-app");
109 assert_eq!(config.fraiseql.schema_file, "schema.json");
110 }
111
112 #[test]
113 fn test_default_security_config() {
114 let config = FraiseQLConfig::default();
115 assert!(config.fraiseql.security.audit_logging.enabled);
116 assert!(config.fraiseql.security.rate_limiting.enabled);
117 }
118
119 #[test]
120 fn test_validation() {
121 let config = FraiseQLConfig::default();
122 assert!(config.validate().is_ok());
123 }
124
125 #[test]
126 fn test_role_definitions_default() {
127 let config = FraiseQLConfig::default();
128 assert!(config.fraiseql.security.role_definitions.is_empty());
129 assert!(config.fraiseql.security.default_role.is_none());
130 }
131
132 #[test]
133 fn test_parse_role_definitions_from_toml() {
134 let toml_str = r#"
135[project]
136name = "test-app"
137
138[fraiseql]
139schema_file = "schema.json"
140
141[[fraiseql.security.role_definitions]]
142name = "viewer"
143description = "Read-only access"
144scopes = ["read:*"]
145
146[[fraiseql.security.role_definitions]]
147name = "admin"
148description = "Full access"
149scopes = ["admin:*"]
150
151[fraiseql.security]
152default_role = "viewer"
153"#;
154
155 let config: FraiseQLConfig = toml::from_str(toml_str).expect("Failed to parse TOML");
156
157 assert_eq!(config.fraiseql.security.role_definitions.len(), 2);
158 assert_eq!(config.fraiseql.security.role_definitions[0].name, "viewer");
159 assert_eq!(config.fraiseql.security.role_definitions[0].scopes[0], "read:*");
160 assert_eq!(config.fraiseql.security.role_definitions[1].name, "admin");
161 assert_eq!(config.fraiseql.security.default_role, Some("viewer".to_string()));
162 }
163
164 #[test]
165 fn test_security_config_role_lookup() {
166 let toml_str = r#"
167[[fraiseql.security.role_definitions]]
168name = "viewer"
169scopes = ["read:User.*", "read:Post.*"]
170
171[[fraiseql.security.role_definitions]]
172name = "editor"
173scopes = ["read:*", "write:*"]
174"#;
175
176 let config: FraiseQLConfig = toml::from_str(toml_str).expect("Failed to parse TOML");
177
178 let viewer = config.fraiseql.security.find_role("viewer");
180 assert!(viewer.is_some());
181 assert_eq!(viewer.unwrap().name, "viewer");
182
183 let scopes = config.fraiseql.security.get_role_scopes("viewer");
185 assert_eq!(scopes.len(), 2);
186 assert!(scopes.contains(&"read:User.*".to_string()));
187
188 let scopes = config.fraiseql.security.get_role_scopes("non-existent");
190 assert!(scopes.is_empty());
191 }
192
193 #[test]
194 fn test_security_config_validation_empty_role_name() {
195 let toml_str = r#"
196[[fraiseql.security.role_definitions]]
197name = ""
198scopes = ["read:*"]
199"#;
200
201 let config: FraiseQLConfig = toml::from_str(toml_str).expect("Failed to parse TOML");
202 assert!(config.validate().is_err(), "Should fail with empty role name");
203 }
204
205 #[test]
206 fn test_security_config_validation_empty_scopes() {
207 let toml_str = r#"
208[[fraiseql.security.role_definitions]]
209name = "viewer"
210scopes = []
211"#;
212
213 let config: FraiseQLConfig = toml::from_str(toml_str).expect("Failed to parse TOML");
214 assert!(config.validate().is_err(), "Should fail with empty scopes");
215 }
216}