use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use walkdir::WalkDir;
pub(crate) const MAX_SCHEMA_FILES: usize = 1_000;
pub struct MultiFileLoader;
pub struct LoadResult {
pub merged: Value,
}
impl MultiFileLoader {
pub fn load_from_directory(dir_path: &str) -> Result<Value> {
let result = Self::load_from_directory_with_tracking(dir_path)?;
Ok(result.merged)
}
pub fn load_from_directory_with_tracking(dir_path: &str) -> Result<LoadResult> {
let dir = Path::new(dir_path);
if !dir.is_dir() {
bail!("Schema directory not found: {dir_path}");
}
let mut types = Vec::new();
let mut queries = Vec::new();
let mut mutations = Vec::new();
let mut name_to_file = HashMap::new();
let mut json_files = Vec::new();
for entry in WalkDir::new(dir_path)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
{
json_files.push(entry.path().to_path_buf());
if json_files.len() > MAX_SCHEMA_FILES {
bail!(
"Schema directory {dir_path:?} contains more than {MAX_SCHEMA_FILES} JSON \
files. Point --schema-dir at a directory containing only schema files."
);
}
}
json_files.sort();
for file_path in json_files {
let content = fs::read_to_string(&file_path)
.context(format!("Failed to read {}", file_path.display()))?;
let value: Value = serde_json::from_str(&content)
.context(format!("Failed to parse JSON from {}", file_path.display()))?;
let file_path_str = file_path.to_string_lossy().to_string();
if let Some(Value::Array(type_items)) = value.get("types") {
for item in type_items {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
let type_key = format!("type:{name}");
if let Some(existing) = name_to_file.get(&type_key) {
bail!(
"Duplicate type '{name}' found in:\n - {existing}\n - {file_path_str}"
);
}
name_to_file.insert(type_key, file_path_str.clone());
}
types.push(item.clone());
}
}
if let Some(Value::Array(query_items)) = value.get("queries") {
for item in query_items {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
let query_key = format!("query:{name}");
if let Some(existing) = name_to_file.get(&query_key) {
bail!(
"Duplicate query '{name}' found in:\n - {existing}\n - {file_path_str}"
);
}
name_to_file.insert(query_key, file_path_str.clone());
}
queries.push(item.clone());
}
}
if let Some(Value::Array(mutation_items)) = value.get("mutations") {
for item in mutation_items {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
let mutation_key = format!("mutation:{name}");
if let Some(existing) = name_to_file.get(&mutation_key) {
bail!(
"Duplicate mutation '{name}' found in:\n - {existing}\n - {file_path_str}"
);
}
name_to_file.insert(mutation_key, file_path_str.clone());
}
mutations.push(item.clone());
}
}
}
let merged = json!({
"types": types,
"queries": queries,
"mutations": mutations,
});
Ok(LoadResult { merged })
}
pub fn load_from_paths(paths: &[PathBuf]) -> Result<Value> {
let mut types = Vec::new();
let mut queries = Vec::new();
let mut mutations = Vec::new();
for path in paths {
if !path.exists() {
bail!("File not found: {}", path.display());
}
let content =
fs::read_to_string(path).context(format!("Failed to read {}", path.display()))?;
let value: Value = serde_json::from_str(&content)
.context(format!("Failed to parse JSON from {}", path.display()))?;
if let Some(Value::Array(type_items)) = value.get("types") {
types.extend(type_items.clone());
}
if let Some(Value::Array(query_items)) = value.get("queries") {
queries.extend(query_items.clone());
}
if let Some(Value::Array(mutation_items)) = value.get("mutations") {
mutations.extend(mutation_items.clone());
}
}
Ok(json!({
"types": types,
"queries": queries,
"mutations": mutations,
}))
}
}