fraiseql_cli/schema/
multi_file_loader.rs1use std::{
10 collections::HashMap,
11 fs,
12 path::{Path, PathBuf},
13};
14
15use anyhow::{Context, Result, bail};
16use serde_json::{Value, json};
17use walkdir::WalkDir;
18
19pub(crate) const MAX_SCHEMA_FILES: usize = 1_000;
24
25pub struct MultiFileLoader;
27
28pub struct LoadResult {
30 pub merged: Value,
32}
33
34impl MultiFileLoader {
35 pub fn load_from_directory(dir_path: &str) -> Result<Value> {
59 let result = Self::load_from_directory_with_tracking(dir_path)?;
60 Ok(result.merged)
61 }
62
63 pub fn load_from_directory_with_tracking(dir_path: &str) -> Result<LoadResult> {
71 let dir = Path::new(dir_path);
72 if !dir.is_dir() {
73 bail!("Schema directory not found: {dir_path}");
74 }
75
76 let mut types = Vec::new();
77 let mut queries = Vec::new();
78 let mut mutations = Vec::new();
79 let mut name_to_file = HashMap::new();
80
81 let mut json_files = Vec::new();
83 for entry in WalkDir::new(dir_path)
84 .into_iter()
85 .filter_map(Result::ok)
86 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
87 {
88 json_files.push(entry.path().to_path_buf());
89 if json_files.len() > MAX_SCHEMA_FILES {
90 bail!(
91 "Schema directory {dir_path:?} contains more than {MAX_SCHEMA_FILES} JSON \
92 files. Point --schema-dir at a directory containing only schema files."
93 );
94 }
95 }
96
97 json_files.sort();
98
99 for file_path in json_files {
101 let content = fs::read_to_string(&file_path)
102 .context(format!("Failed to read {}", file_path.display()))?;
103 let value: Value = serde_json::from_str(&content)
104 .context(format!("Failed to parse JSON from {}", file_path.display()))?;
105
106 let file_path_str = file_path.to_string_lossy().to_string();
108
109 if let Some(Value::Array(type_items)) = value.get("types") {
111 for item in type_items {
112 if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
113 let type_key = format!("type:{name}");
114 if let Some(existing) = name_to_file.get(&type_key) {
115 bail!(
116 "Duplicate type '{name}' found in:\n - {existing}\n - {file_path_str}"
117 );
118 }
119 name_to_file.insert(type_key, file_path_str.clone());
120 }
121 types.push(item.clone());
122 }
123 }
124
125 if let Some(Value::Array(query_items)) = value.get("queries") {
127 for item in query_items {
128 if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
129 let query_key = format!("query:{name}");
130 if let Some(existing) = name_to_file.get(&query_key) {
131 bail!(
132 "Duplicate query '{name}' found in:\n - {existing}\n - {file_path_str}"
133 );
134 }
135 name_to_file.insert(query_key, file_path_str.clone());
136 }
137 queries.push(item.clone());
138 }
139 }
140
141 if let Some(Value::Array(mutation_items)) = value.get("mutations") {
143 for item in mutation_items {
144 if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
145 let mutation_key = format!("mutation:{name}");
146 if let Some(existing) = name_to_file.get(&mutation_key) {
147 bail!(
148 "Duplicate mutation '{name}' found in:\n - {existing}\n - {file_path_str}"
149 );
150 }
151 name_to_file.insert(mutation_key, file_path_str.clone());
152 }
153 mutations.push(item.clone());
154 }
155 }
156 }
157
158 let merged = json!({
159 "types": types,
160 "queries": queries,
161 "mutations": mutations,
162 });
163
164 Ok(LoadResult { merged })
165 }
166
167 pub fn load_from_paths(paths: &[PathBuf]) -> Result<Value> {
180 let mut types = Vec::new();
181 let mut queries = Vec::new();
182 let mut mutations = Vec::new();
183
184 for path in paths {
185 if !path.exists() {
186 bail!("File not found: {}", path.display());
187 }
188
189 let content =
190 fs::read_to_string(path).context(format!("Failed to read {}", path.display()))?;
191 let value: Value = serde_json::from_str(&content)
192 .context(format!("Failed to parse JSON from {}", path.display()))?;
193
194 if let Some(Value::Array(type_items)) = value.get("types") {
196 types.extend(type_items.clone());
197 }
198
199 if let Some(Value::Array(query_items)) = value.get("queries") {
201 queries.extend(query_items.clone());
202 }
203
204 if let Some(Value::Array(mutation_items)) = value.get("mutations") {
206 mutations.extend(mutation_items.clone());
207 }
208 }
209
210 Ok(json!({
211 "types": types,
212 "queries": queries,
213 "mutations": mutations,
214 }))
215 }
216}