use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FederationVersion {
V1,
V2,
}
#[derive(Debug, Clone)]
pub struct FederationDirective {
pub name: String,
pub description: String,
pub locations: Vec<DirectiveLocation>,
pub arguments: Vec<DirectiveArgument>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DirectiveLocation {
Object,
FieldDefinition,
Interface,
Scalar,
Enum,
Union,
InputObject,
}
#[derive(Debug, Clone)]
pub struct DirectiveArgument {
pub name: String,
pub arg_type: String,
pub description: Option<String>,
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityKey {
pub fields: Vec<String>,
pub resolvable: bool,
}
impl EntityKey {
pub fn new(fields: Vec<String>) -> Self {
Self {
fields,
resolvable: true,
}
}
pub fn with_resolvable(mut self, resolvable: bool) -> Self {
self.resolvable = resolvable;
self
}
pub fn to_directive_string(&self) -> String {
let fields_str = self.fields.join(" ");
if self.resolvable {
format!("@key(fields: \"{}\")", fields_str)
} else {
format!("@key(fields: \"{}\", resolvable: false)", fields_str)
}
}
}
#[derive(Debug, Clone)]
pub struct ExternalField {
pub field_name: String,
pub type_name: String,
}
#[derive(Debug, Clone)]
pub struct RequiresField {
pub field_name: String,
pub required_fields: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ProvidesField {
pub field_name: String,
pub provided_fields: Vec<String>,
}
#[derive(Debug)]
pub struct FederationSchemaBuilder {
_schema_content: String,
version: FederationVersion,
entities: HashMap<String, Vec<EntityKey>>,
external_fields: HashMap<String, Vec<ExternalField>>,
requires_fields: HashMap<String, Vec<RequiresField>>,
provides_fields: HashMap<String, Vec<ProvidesField>>,
shareable_types: HashSet<String>,
override_fields: HashMap<String, String>, }
impl FederationSchemaBuilder {
pub fn new(version: FederationVersion) -> Self {
Self {
_schema_content: String::new(),
version,
entities: HashMap::new(),
external_fields: HashMap::new(),
requires_fields: HashMap::new(),
provides_fields: HashMap::new(),
shareable_types: HashSet::new(),
override_fields: HashMap::new(),
}
}
pub fn add_entity(mut self, type_name: impl Into<String>, key: EntityKey) -> Self {
let type_name = type_name.into();
self.entities
.entry(type_name)
.or_insert_with(Vec::new)
.push(key);
self
}
pub fn add_external_field(
mut self,
type_name: impl Into<String>,
field_name: impl Into<String>,
) -> Self {
let external = ExternalField {
field_name: field_name.into(),
type_name: type_name.into(),
};
self.external_fields
.entry(external.type_name.clone())
.or_insert_with(Vec::new)
.push(external);
self
}
pub fn add_requires(
mut self,
type_name: impl Into<String>,
field_name: impl Into<String>,
required_fields: Vec<String>,
) -> Self {
let requires = RequiresField {
field_name: field_name.into(),
required_fields,
};
self.requires_fields
.entry(type_name.into())
.or_insert_with(Vec::new)
.push(requires);
self
}
pub fn add_provides(
mut self,
type_name: impl Into<String>,
field_name: impl Into<String>,
provided_fields: Vec<String>,
) -> Self {
let provides = ProvidesField {
field_name: field_name.into(),
provided_fields,
};
self.provides_fields
.entry(type_name.into())
.or_insert_with(Vec::new)
.push(provides);
self
}
pub fn add_shareable(mut self, type_name: impl Into<String>) -> Self {
if self.version == FederationVersion::V2 {
self.shareable_types.insert(type_name.into());
}
self
}
pub fn add_override(
mut self,
field_name: impl Into<String>,
from_subgraph: impl Into<String>,
) -> Self {
if self.version == FederationVersion::V2 {
self.override_fields
.insert(field_name.into(), from_subgraph.into());
}
self
}
pub fn build(self) -> Result<FederationSchema> {
let mut sdl = String::new();
self.write_federation_schema_extension(&mut sdl)?;
for (type_name, keys) in &self.entities {
self.write_entity_type(&mut sdl, type_name, keys)?;
}
self.write_federation_queries(&mut sdl)?;
Ok(FederationSchema {
sdl,
version: self.version,
entities: self.entities.keys().cloned().collect(),
})
}
fn write_federation_schema_extension(&self, sdl: &mut String) -> Result<()> {
match self.version {
FederationVersion::V1 => {
writeln!(sdl, "extend schema @link(url: \"https://specs.apollo.dev/federation/v1.0\")")?;
}
FederationVersion::V2 => {
writeln!(sdl, "extend schema")?;
writeln!(
sdl,
" @link(url: \"https://specs.apollo.dev/federation/v2.0\","
)?;
writeln!(sdl, " import: [\"@key\", \"@external\", \"@requires\", \"@provides\", \"@shareable\", \"@override\"])")?;
}
}
writeln!(sdl)?;
Ok(())
}
fn write_entity_type(
&self,
sdl: &mut String,
type_name: &str,
keys: &[EntityKey],
) -> Result<()> {
for key in keys {
writeln!(sdl, "type {} {}", type_name, key.to_directive_string())?;
}
if self.shareable_types.contains(type_name) {
writeln!(sdl, " @shareable")?;
}
writeln!(sdl, " {{")?;
writeln!(sdl, " id: ID!")?;
writeln!(sdl, "}}")?;
writeln!(sdl)?;
Ok(())
}
fn write_federation_queries(&self, sdl: &mut String) -> Result<()> {
writeln!(sdl, "# Federation queries")?;
writeln!(sdl, "extend type Query {{")?;
writeln!(sdl, " _entities(representations: [_Any!]!): [_Entity]!")?;
writeln!(sdl, " _service: _Service!")?;
writeln!(sdl, "}}")?;
writeln!(sdl)?;
writeln!(sdl, "scalar _Any")?;
writeln!(sdl, "scalar _FieldSet")?;
writeln!(sdl)?;
writeln!(sdl, "union _Entity =")?;
let entity_names: Vec<_> = self.entities.keys().collect();
for (i, entity) in entity_names.iter().enumerate() {
if i > 0 {
write!(sdl, " | ")?;
}
write!(sdl, "{}", entity)?;
}
writeln!(sdl)?;
writeln!(sdl)?;
writeln!(sdl, "type _Service {{")?;
writeln!(sdl, " sdl: String!")?;
writeln!(sdl, "}}")?;
writeln!(sdl)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FederationSchema {
pub sdl: String,
pub version: FederationVersion,
pub entities: Vec<String>,
}
impl FederationSchema {
pub fn to_sdl(&self) -> &str {
&self.sdl
}
pub fn is_entity(&self, type_name: &str) -> bool {
self.entities.iter().any(|e| e == type_name)
}
}
pub trait EntityResolver: Send + Sync {
fn resolve_entity(
&self,
type_name: &str,
representation: HashMap<String, serde_json::Value>,
) -> Result<Option<serde_json::Value>>;
}
#[derive(Debug, Clone)]
pub struct FederationServiceConfig {
pub name: String,
pub version: String,
pub enable_entity_cache: bool,
pub cache_ttl: u64,
}
impl Default for FederationServiceConfig {
fn default() -> Self {
Self {
name: "oxirs-federation-service".to_string(),
version: "1.0.0".to_string(),
enable_entity_cache: true,
cache_ttl: 300, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_key_directive() {
let key = EntityKey::new(vec!["id".to_string()]);
assert_eq!(key.to_directive_string(), "@key(fields: \"id\")");
let key = EntityKey::new(vec!["id".to_string(), "email".to_string()]);
assert_eq!(key.to_directive_string(), "@key(fields: \"id email\")");
}
#[test]
fn test_entity_key_non_resolvable() {
let key = EntityKey::new(vec!["id".to_string()]).with_resolvable(false);
assert_eq!(
key.to_directive_string(),
"@key(fields: \"id\", resolvable: false)"
);
}
#[test]
fn test_federation_schema_builder() {
let schema = FederationSchemaBuilder::new(FederationVersion::V2)
.add_entity("User", EntityKey::new(vec!["id".to_string()]))
.add_entity("Product", EntityKey::new(vec!["sku".to_string()]))
.build()
.expect("should succeed");
assert_eq!(schema.version, FederationVersion::V2);
assert_eq!(schema.entities.len(), 2);
assert!(schema.is_entity("User"));
assert!(schema.is_entity("Product"));
assert!(!schema.is_entity("Order"));
}
#[test]
fn test_federation_schema_sdl_generation() {
let schema = FederationSchemaBuilder::new(FederationVersion::V2)
.add_entity("User", EntityKey::new(vec!["id".to_string()]))
.build()
.expect("should succeed");
let sdl = schema.to_sdl();
assert!(sdl.contains("@link"));
assert!(sdl.contains("federation/v2.0"));
assert!(sdl.contains("_entities"));
assert!(sdl.contains("_service"));
assert!(sdl.contains("type User"));
}
}