use std::{
collections::{BTreeSet, HashMap, HashSet},
sync::Arc,
};
use parking_lot::RwLock;
use serde_json::Value;
use super::schema_builder::IntrospectionResponses;
use crate::{
schema::CompiledSchema,
security::{
SecurityContext,
role_gate::{caller_holds, visible_schema},
},
};
pub struct CallerIntrospection {
full: Arc<IntrospectionResponses>,
gating_roles: BTreeSet<String>,
full_reachable: HashSet<String>,
build: fn(&CompiledSchema) -> IntrospectionResponses,
views: RwLock<HashMap<String, Arc<IntrospectionResponses>>>,
}
impl CallerIntrospection {
pub fn new(
schema: &CompiledSchema,
build: fn(&CompiledSchema) -> IntrospectionResponses,
) -> Self {
let full = build(schema);
let full_reachable = reachable_types(schema_of(&full));
let gating_roles = schema
.queries
.iter()
.filter_map(|q| q.requires_role.clone())
.chain(schema.mutations.iter().filter_map(|m| m.requires_role.clone()))
.chain(schema.types.iter().filter_map(|t| t.requires_role.clone()))
.collect();
Self {
full: Arc::new(full),
gating_roles,
full_reachable,
build,
views: RwLock::new(HashMap::new()),
}
}
pub fn for_caller(
&self,
schema: &CompiledSchema,
caller: Option<&SecurityContext>,
) -> (String, Arc<IntrospectionResponses>) {
let held: Vec<&str> = self
.gating_roles
.iter()
.map(String::as_str)
.filter(|role| caller_holds(Some(role), caller))
.collect();
if held.len() == self.gating_roles.len() {
return (String::new(), Arc::clone(&self.full));
}
let key = view_key(&held);
if let Some(view) = self.views.read().get(&key) {
return (key, Arc::clone(view));
}
let view = Arc::new(self.build_view(schema, caller));
let view = Arc::clone(self.views.write().entry(key.clone()).or_insert(view));
(key, view)
}
pub fn visible_schema(
&self,
schema: &CompiledSchema,
caller: Option<&SecurityContext>,
) -> CompiledSchema {
let mut visible = visible_schema(schema, caller);
let (_, view) = self.for_caller(schema, caller);
let still_listed = listed_types(schema_of(&view));
let dropped: HashSet<&str> = listed_types(schema_of(&self.full))
.into_iter()
.filter(|name| !still_listed.contains(name))
.collect();
if dropped.is_empty() {
return visible;
}
let kept = |name: &str| !dropped.contains(name);
visible.types.retain(|t| kept(t.name.as_str()));
visible.input_types.retain(|t| kept(t.name.as_str()));
visible.enums.retain(|t| kept(t.name.as_str()));
visible.interfaces.retain(|t| kept(t.name.as_str()));
visible.unions.retain(|t| kept(t.name.as_str()));
visible
}
fn build_view(
&self,
schema: &CompiledSchema,
caller: Option<&SecurityContext>,
) -> IntrospectionResponses {
let mut view = (self.build)(&visible_schema(schema, caller));
let still_reached = reachable_types(schema_of(&view));
let dropped: HashSet<&str> = self
.full_reachable
.iter()
.map(String::as_str)
.filter(|name| !still_reached.contains(*name))
.collect();
if dropped.is_empty() {
return view;
}
view.type_responses.retain(|name, _| !dropped.contains(name.as_str()));
let mut schema_response = view.schema_response.as_ref().clone();
if let Some(types) = schema_response
.pointer_mut("/data/__schema/types")
.and_then(Value::as_array_mut)
{
types.retain(|t| t["name"].as_str().is_none_or(|name| !dropped.contains(name)));
}
view.schema_response = Arc::new(schema_response);
view
}
}
fn view_key(held: &[&str]) -> String {
format!("\u{1}{}", held.join("\u{1f}"))
}
fn listed_types(schema: &Value) -> HashSet<&str> {
schema["types"]
.as_array()
.into_iter()
.flatten()
.filter_map(|t| t["name"].as_str())
.collect()
}
pub(super) fn schema_of(responses: &IntrospectionResponses) -> &Value {
&responses.schema_response["data"]["__schema"]
}
pub(super) fn reachable_types(schema: &Value) -> HashSet<String> {
let by_name: HashMap<&str, &Value> = schema["types"]
.as_array()
.into_iter()
.flatten()
.filter_map(|t| t["name"].as_str().map(|name| (name, t)))
.collect();
let mut pending: Vec<&str> = ["queryType", "mutationType", "subscriptionType"]
.iter()
.filter_map(|root| schema[root]["name"].as_str())
.collect();
for directive in schema["directives"].as_array().into_iter().flatten() {
pending.extend(input_types(&directive["args"]));
}
let mut reached = HashSet::new();
while let Some(name) = pending.pop() {
if !reached.insert(name.to_string()) {
continue;
}
let Some(t) = by_name.get(name) else { continue };
for field in t["fields"].as_array().into_iter().flatten() {
pending.extend(named_type(&field["type"]));
pending.extend(input_types(&field["args"]));
}
pending.extend(input_types(&t["inputFields"]));
for list in ["interfaces", "possibleTypes"] {
pending.extend(
t[list].as_array().into_iter().flatten().filter_map(|r| r["name"].as_str()),
);
}
}
reached
}
fn input_types(values: &Value) -> impl Iterator<Item = &str> {
values.as_array().into_iter().flatten().filter_map(|v| named_type(&v["type"]))
}
fn named_type(type_ref: &Value) -> Option<&str> {
let mut current = type_ref;
loop {
if let Some(name) = current["name"].as_str() {
return Some(name);
}
current = current.get("ofType")?;
}
}