use anyhow::{Result, bail};
use serde_json::Value;
pub const AUTHORABLE_ARRAY_SECTIONS: &[&str] = &[
"types",
"enums",
"input_types",
"interfaces",
"unions",
"queries",
"mutations",
"subscriptions",
"fragments",
"directives",
"fact_tables",
"aggregate_queries",
"observers",
"sources",
"functions",
"custom_scalars",
];
pub const AUTHORABLE_SINGLETON_SECTIONS: &[&str] = &[
"version",
"naming_convention",
"session_variables",
"federation",
"federation_config",
"inject_defaults",
"security",
"auth",
"observers_config",
"subscriptions_config",
"validation_config",
"debug_config",
"mcp_config",
"rest_config",
"grpc_config",
"hierarchies_config",
"changelog_config",
"query_defaults",
];
pub fn absorb_sections(target: &mut Value, source: &Value, source_label: &str) -> Result<()> {
let Some(source_map) = source.as_object() else {
bail!("{source_label}: expected a JSON object at the top level");
};
let Some(target_map) = target.as_object_mut() else {
bail!("internal error: the seam accumulator is not a JSON object");
};
for (key, value) in source_map {
if AUTHORABLE_ARRAY_SECTIONS.contains(&key.as_str()) {
let Some(items) = value.as_array() else {
bail!(
"{source_label}: section `{key}` must be an array of definitions, found {}",
json_type_name(value)
);
};
let slot = target_map.entry(key.clone()).or_insert_with(|| Value::Array(Vec::new()));
let Some(accumulated) = slot.as_array_mut() else {
bail!(
"{source_label}: section `{key}` was already accumulated as a non-array, \
which means an earlier source declared it with a conflicting shape"
);
};
accumulated.extend(items.iter().cloned());
} else if let Some(existing) = target_map.get(key) {
if existing != value {
bail!(
"{source_label}: section `{key}` is already defined with a different value by \
an earlier schema file. A single-valued section must be declared in exactly \
one place — move it to one file, or reconcile the two declarations."
);
}
} else {
target_map.insert(key.clone(), value.clone());
}
}
Ok(())
}
fn json_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
pub fn section_noun(section: &str) -> &str {
match section {
"types" => "type",
"enums" => "enum",
"input_types" => "input type",
"interfaces" => "interface",
"unions" => "union",
"queries" => "query",
"mutations" => "mutation",
"subscriptions" => "subscription",
"fragments" => "fragment",
"directives" => "directive",
"fact_tables" => "fact table",
"aggregate_queries" => "aggregate query",
"observers" => "observer",
"sources" => "source",
"custom_scalars" => "custom scalar",
other => other,
}
}
pub fn empty_accumulator() -> Value {
let mut map = serde_json::Map::new();
for key in AUTHORABLE_ARRAY_SECTIONS {
map.insert((*key).to_string(), Value::Array(Vec::new()));
}
Value::Object(map)
}
#[cfg(test)]
mod tests;