use std::{collections::HashSet, fmt};
use super::CompiledSchema;
use crate::{
runtime::suggest_similar,
schema::{FieldDefinition, FieldType},
utils::casing::to_camel_case,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FederationKeyProblem {
UnknownType {
entity: String,
suggestions: Vec<String>,
},
DuplicateEntity {
entity: String,
},
EmbeddedType {
entity: String,
},
EmptyKey {
entity: String,
},
MalformedFieldSet {
entity: String,
field_set: String,
reason: String,
},
MissingField {
entity: String,
path: String,
owner: String,
hint: Option<String>,
},
SelectionOnLeaf {
entity: String,
path: String,
},
ObjectWithoutSelection {
entity: String,
path: String,
},
InvalidKeyFieldType {
entity: String,
path: String,
kind: &'static str,
},
UnknownNestedType {
entity: String,
path: String,
type_name: String,
},
}
impl fmt::Display for FederationKeyProblem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownType {
entity,
suggestions,
} => {
write!(
f,
"federation entity '{entity}' names no object type or interface this \
schema declares"
)?;
if !suggestions.is_empty() {
write!(f, " (did you mean: {}?)", suggestions.join(", "))?;
}
Ok(())
},
Self::DuplicateEntity { entity } => write!(
f,
"federation entity '{entity}' is declared more than once; one entity carries \
one `@key`, so every declaration after the first would be dropped from the SDL"
),
Self::EmbeddedType { entity } => write!(
f,
"federation entity '{entity}' is an embedded value type: it has no identity of \
its own and no `sql_source`, so `_entities` could never resolve it. Drop it \
from `federation.entities` (it is served as part of its parent)"
),
Self::EmptyKey { entity } => {
write!(f, "federation entity '{entity}' has an `@key` that selects no field")
},
Self::MalformedFieldSet {
entity,
field_set,
reason,
} => write!(
f,
"federation entity '{entity}' has `@key(fields: \"{field_set}\")`, which is not \
a field set: {reason}"
),
Self::MissingField {
entity,
path,
owner,
hint,
} => {
write!(
f,
"federation entity '{entity}' is keyed on '{path}', but type '{owner}' has \
no field '{}'; the router would refuse the supergraph with \
KEY_INVALID_FIELDS",
path.rsplit('.').next().unwrap_or(path)
)?;
if let Some(hint) = hint {
write!(f, " (did you mean '{hint}'? keys name the published field)")?;
}
Ok(())
},
Self::SelectionOnLeaf { entity, path } => write!(
f,
"federation entity '{entity}' selects sub-fields of '{path}', which has none"
),
Self::ObjectWithoutSelection { entity, path } => write!(
f,
"federation entity '{entity}' is keyed on '{path}', an object; a key must \
select the object's fields, e.g. '{path} {{ id }}'"
),
Self::InvalidKeyFieldType { entity, path, kind } => write!(
f,
"federation entity '{entity}' is keyed on '{path}', which is {kind}; a key \
cannot contain a list, interface, union or input-object field \
(KEY_FIELDS_SELECT_INVALID_TYPE)"
),
Self::UnknownNestedType {
entity,
path,
type_name,
} => write!(
f,
"federation entity '{entity}' selects into '{path}', whose type '{type_name}' \
this schema does not declare"
),
}
}
}
#[derive(Debug)]
struct Selection {
name: String,
children: Vec<Selection>,
}
fn parse_field_set(input: &str) -> Result<Vec<Selection>, String> {
let mut stack: Vec<Vec<Selection>> = vec![Vec::new()];
let mut chars = input.char_indices().peekable();
while let Some(&(start, c)) = chars.peek() {
if c.is_whitespace() || c == ',' {
chars.next();
} else if c == '_' || c.is_ascii_alphabetic() {
let mut end = start;
while let Some(&(i, c)) = chars.peek() {
if c == '_' || c.is_ascii_alphanumeric() {
end = i + c.len_utf8();
chars.next();
} else {
break;
}
}
let level = stack.last_mut().ok_or("unbalanced braces")?;
level.push(Selection {
name: input[start..end].to_string(),
children: Vec::new(),
});
} else if c == '{' {
chars.next();
let has_parent = stack
.last()
.is_some_and(|level| level.last().is_some_and(|s| s.children.is_empty()));
if !has_parent {
return Err("'{' must follow the field it selects into".to_string());
}
stack.push(Vec::new());
} else if c == '}' {
chars.next();
if stack.len() < 2 {
return Err("unbalanced braces".to_string());
}
let children = stack.pop().unwrap_or_default();
if children.is_empty() {
return Err("an empty selection '{}'".to_string());
}
let parent =
stack.last_mut().and_then(|level| level.last_mut()).ok_or("unbalanced braces")?;
parent.children = children;
} else {
return Err(format!(
"unexpected '{c}' (aliases, arguments, directives and fragments have no \
meaning in a key)"
));
}
}
if stack.len() != 1 {
return Err("unbalanced braces".to_string());
}
Ok(stack.pop().unwrap_or_default())
}
fn lookup<'a>(
fields: &'a [FieldDefinition],
name: &str,
) -> Result<&'a FieldDefinition, Option<String>> {
if let Some(field) = fields.iter().find(|f| f.name == name) {
return Ok(field);
}
let wanted = to_camel_case(name);
Err(fields
.iter()
.find(|f| to_camel_case(f.name.as_str()) == wanted)
.map(|f| f.name.to_string()))
}
impl CompiledSchema {
#[must_use]
pub fn federation_key_problems(&self) -> Vec<FederationKeyProblem> {
let Some(federation) = &self.federation else {
return Vec::new();
};
let mut problems = Vec::new();
let mut seen: HashSet<&str> = HashSet::new();
for entity in &federation.entities {
let name = entity.name.as_str();
if !seen.insert(name) {
problems.push(FederationKeyProblem::DuplicateEntity {
entity: name.to_string(),
});
continue;
}
let fields = if let Some(type_def) = self.find_type(name) {
if type_def.embedded {
problems.push(FederationKeyProblem::EmbeddedType {
entity: name.to_string(),
});
continue;
}
&type_def.fields
} else if let Some(interface) = self.find_interface(name) {
&interface.fields
} else {
let known: Vec<&str> = self
.types
.iter()
.map(|t| t.name.as_str())
.chain(self.interfaces.iter().map(|i| i.name.as_str()))
.collect();
problems.push(FederationKeyProblem::UnknownType {
entity: name.to_string(),
suggestions: suggest_similar(name, &known)
.into_iter()
.map(ToString::to_string)
.collect(),
});
continue;
};
let field_set = entity.key_fields.join(" ");
match parse_field_set(&field_set) {
Err(reason) => problems.push(FederationKeyProblem::MalformedFieldSet {
entity: name.to_string(),
field_set,
reason,
}),
Ok(selections) if selections.is_empty() => {
problems.push(FederationKeyProblem::EmptyKey {
entity: name.to_string(),
});
},
Ok(selections) => {
self.check_selections(name, name, "", fields, &selections, &mut problems);
},
}
}
problems
}
#[must_use]
pub fn federation_key_violations(&self) -> Vec<String> {
self.federation_key_problems().iter().map(ToString::to_string).collect()
}
fn check_selections(
&self,
entity: &str,
owner: &str,
prefix: &str,
fields: &[FieldDefinition],
selections: &[Selection],
problems: &mut Vec<FederationKeyProblem>,
) {
for selection in selections {
let path = if prefix.is_empty() {
selection.name.clone()
} else {
format!("{prefix}.{}", selection.name)
};
let field = match lookup(fields, &selection.name) {
Ok(field) => field,
Err(hint) => {
problems.push(FederationKeyProblem::MissingField {
entity: entity.to_string(),
path,
owner: owner.to_string(),
hint,
});
continue;
},
};
let invalid = |kind| FederationKeyProblem::InvalidKeyFieldType {
entity: entity.to_string(),
path: path.clone(),
kind,
};
match &field.field_type {
FieldType::List(_) => problems.push(invalid("a list")),
FieldType::Interface(_) => problems.push(invalid("an interface")),
FieldType::Union(_) => problems.push(invalid("a union")),
FieldType::Input(_) => problems.push(invalid("an input object")),
FieldType::Object(type_name) => {
if selection.children.is_empty() {
problems.push(FederationKeyProblem::ObjectWithoutSelection {
entity: entity.to_string(),
path,
});
} else if let Some(nested) = self.find_type(type_name) {
self.check_selections(
entity,
type_name,
&path,
&nested.fields,
&selection.children,
problems,
);
} else {
problems.push(FederationKeyProblem::UnknownNestedType {
entity: entity.to_string(),
path,
type_name: type_name.clone(),
});
}
},
FieldType::String
| FieldType::Int
| FieldType::Float
| FieldType::Boolean
| FieldType::Id
| FieldType::DateTime
| FieldType::Date
| FieldType::Time
| FieldType::Json
| FieldType::Uuid
| FieldType::Decimal
| FieldType::Vector
| FieldType::BitVector
| FieldType::HalfVector
| FieldType::SparseVector
| FieldType::Scalar(_)
| FieldType::Enum(_) => {
if !selection.children.is_empty() {
problems.push(FederationKeyProblem::SelectionOnLeaf {
entity: entity.to_string(),
path,
});
}
},
}
}
}
}