Skip to main content

fraiseql_server/schema/
loader.rs

1//! Schema loader for compiled GraphQL schemas.
2
3use std::path::{Path, PathBuf};
4
5use fraiseql_core::schema::CompiledSchema;
6use tracing::{debug, info};
7
8/// Error loading schema.
9#[derive(Debug, thiserror::Error)]
10pub enum SchemaLoadError {
11    /// Schema file not found.
12    #[error("Schema file not found: {0}")]
13    NotFound(PathBuf),
14
15    /// IO error reading file.
16    #[error("Failed to read schema file: {0}")]
17    IoError(#[from] std::io::Error),
18
19    /// JSON parsing error.
20    #[error("Failed to parse schema JSON: {0}")]
21    ParseError(#[from] serde_json::Error),
22
23    /// Schema validation error.
24    #[error("Invalid schema: {0}")]
25    ValidationError(String),
26}
27
28/// Loader for compiled GraphQL schemas from JSON files.
29///
30/// Loads and caches a compiled schema from a JSON file on disk.
31/// Used during server startup to prepare the schema for query execution.
32#[derive(Debug, Clone)]
33pub struct CompiledSchemaLoader {
34    /// Path to the compiled schema JSON file.
35    path: PathBuf,
36}
37
38impl CompiledSchemaLoader {
39    /// Create a new schema loader pointing to a schema file.
40    ///
41    /// # Arguments
42    ///
43    /// * `path` - Path to the compiled schema JSON file
44    ///
45    /// # Example
46    ///
47    /// ```rust,ignore
48    /// let loader = CompiledSchemaLoader::new("schema.compiled.json");
49    /// let schema = loader.load().await?;
50    /// ```
51    #[must_use]
52    pub fn new<P: AsRef<Path>>(path: P) -> Self {
53        Self {
54            path: path.as_ref().to_path_buf(),
55        }
56    }
57
58    /// Load schema from file.
59    ///
60    /// Reads the schema JSON file, parses it, and returns a `CompiledSchema`.
61    ///
62    /// # Errors
63    ///
64    /// Returns error if:
65    /// - File does not exist
66    /// - File cannot be read
67    /// - JSON is invalid
68    /// - Schema validation fails
69    ///
70    /// # Example
71    ///
72    /// ```rust,ignore
73    /// let loader = CompiledSchemaLoader::new("schema.compiled.json");
74    /// let schema = loader.load().await?;
75    /// ```
76    pub async fn load(&self) -> Result<CompiledSchema, SchemaLoadError> {
77        info!(path = %self.path.display(), "Loading compiled schema");
78
79        // Check if file exists
80        if !self.path.exists() {
81            return Err(SchemaLoadError::NotFound(self.path.clone()));
82        }
83
84        // Read file asynchronously
85        let contents =
86            tokio::fs::read_to_string(&self.path).await.map_err(SchemaLoadError::IoError)?;
87
88        debug!(
89            path = %self.path.display(),
90            size_bytes = contents.len(),
91            "Schema file read successfully"
92        );
93
94        // Parse JSON and validate it's valid JSON first
95        serde_json::from_str::<serde_json::Value>(&contents)?;
96
97        // Create CompiledSchema from JSON string
98        let schema = CompiledSchema::from_json(&contents)
99            .map_err(|e| SchemaLoadError::ValidationError(e.to_string()))?;
100
101        info!(path = %self.path.display(), "Schema loaded successfully");
102
103        Ok(schema)
104    }
105
106    /// Get the path to the schema file.
107    #[must_use]
108    pub fn path(&self) -> &Path {
109        &self.path
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use std::io::Write;
116
117    use tempfile::NamedTempFile;
118
119    use super::*;
120
121    #[tokio::test]
122    async fn test_loader_not_found() {
123        let loader = CompiledSchemaLoader::new("/nonexistent/path/schema.json");
124        let result = loader.load().await;
125        assert!(matches!(result, Err(SchemaLoadError::NotFound(_))));
126    }
127
128    #[tokio::test]
129    async fn test_loader_invalid_json() {
130        let mut file = NamedTempFile::new().unwrap();
131        writeln!(file, "{{invalid json").unwrap();
132        file.flush().unwrap();
133
134        let loader = CompiledSchemaLoader::new(file.path());
135        let result = loader.load().await;
136        assert!(matches!(result, Err(SchemaLoadError::ParseError(_))));
137    }
138}