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