Skip to main content

fraiseql_cli/config/
mod.rs

1//! Configuration loading and management
2//!
3//! This module handles loading configuration from fraiseql.toml files,
4//! including security settings, project metadata, and compilation options.
5
6pub 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/// Project configuration from fraiseql.toml
18#[derive(Debug, Clone, Default, Deserialize, Serialize)]
19#[serde(default)]
20pub struct FraiseQLConfig {
21    /// Project metadata (name, version, description)
22    #[serde(rename = "project")]
23    pub project: ProjectConfig,
24
25    /// FraiseQL-specific settings
26    #[serde(rename = "fraiseql")]
27    pub fraiseql: FraiseQLSettings,
28}
29
30/// Project metadata
31#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(default)]
33pub struct ProjectConfig {
34    /// Project name
35    pub name:        String,
36    /// Project version
37    pub version:     String,
38    /// Optional project description
39    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/// FraiseQL-specific settings
53#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(default)]
55pub struct FraiseQLSettings {
56    /// Path to the GraphQL schema file
57    pub schema_file: String,
58    /// Path to the output compiled schema file
59    pub output_file: String,
60    /// Security configuration
61    #[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    /// Load configuration from fraiseql.toml file
77    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    /// Validate configuration
94    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        // Test find_role
179        let viewer = config.fraiseql.security.find_role("viewer");
180        assert!(viewer.is_some());
181        assert_eq!(viewer.unwrap().name, "viewer");
182
183        // Test get_role_scopes
184        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        // Test non-existent role
189        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}