use std::sync::Arc;
use async_graphql::{Executor, ObjectType, SchemaBuilder, SubscriptionType};
use futures::stream::BoxStream;
use crate::config::GraphQLConfig;
use crate::versioning::{ApiVersion, DeprecationInfo};
#[cfg(feature = "graphql-cedar")]
use crate::middleware::cedar::CedarAuthz;
use super::service::ActonGraphQL;
pub(crate) type ErasedExecutor = Arc<
dyn Fn(async_graphql::BatchRequest) -> futures::future::BoxFuture<'static, async_graphql::BatchResponse>
+ Send
+ Sync,
>;
pub(crate) type StreamFactory = Arc<
dyn Fn(async_graphql::Request) -> BoxStream<'static, async_graphql::Response> + Send + Sync,
>;
#[derive(Clone)]
pub(crate) struct ErasedSchema {
batch: ErasedExecutor,
stream: StreamFactory,
}
impl Executor for ErasedSchema {
fn execute(
&self,
request: async_graphql::Request,
) -> impl std::future::Future<Output = async_graphql::Response> + Send {
let fut = (self.batch)(async_graphql::BatchRequest::Single(request));
async move {
match fut.await {
async_graphql::BatchResponse::Single(s) => s,
async_graphql::BatchResponse::Batch(mut b) => b.pop().unwrap_or_default(),
}
}
}
fn execute_batch(
&self,
batch_request: async_graphql::BatchRequest,
) -> impl std::future::Future<Output = async_graphql::BatchResponse> + Send {
(self.batch)(batch_request)
}
fn execute_stream(
&self,
request: async_graphql::Request,
_session_data: Option<Arc<async_graphql::Data>>,
) -> BoxStream<'static, async_graphql::Response> {
(self.stream)(request)
}
}
impl ErasedSchema {
fn from_executor<E>(executor: E) -> Self
where
E: Executor + Clone + Send + Sync + 'static,
{
let batch_exec = executor.clone();
let stream_exec = executor;
Self {
batch: Arc::new(move |req| {
let exec = batch_exec.clone();
Box::pin(async move { exec.execute_batch(req).await })
}),
stream: Arc::new(move |req| {
let stream = stream_exec.execute_stream(req, None);
Box::pin(stream)
}),
}
}
}
pub(crate) struct VersionedSchemaEntry {
pub(crate) version: ApiVersion,
pub(crate) deprecation: Option<DeprecationInfo>,
pub(crate) executor: ErasedSchema,
}
pub struct VersionedGraphQL {
pub(crate) base_path: Option<String>,
pub(crate) entries: Vec<VersionedSchemaEntry>,
}
impl VersionedGraphQL {
pub fn version_count(&self) -> usize {
self.entries.len()
}
pub fn has_version(&self, version: ApiVersion) -> bool {
self.entries.iter().any(|e| e.version == version)
}
pub fn base_path(&self) -> Option<&str> {
self.base_path.as_deref()
}
pub fn versions(&self) -> impl Iterator<Item = ApiVersion> + '_ {
self.entries.iter().map(|e| e.version)
}
#[allow(dead_code)]
pub(crate) fn build_service_for(
executor: ErasedSchema,
#[cfg(feature = "graphql-cedar")] cedar: Option<CedarAuthz>,
) -> ActonGraphQL<ErasedSchema> {
ActonGraphQL::new(
executor,
#[cfg(feature = "graphql-cedar")]
cedar,
)
}
}
pub struct VersionedGraphQLBuilder {
base_path: Option<String>,
entries: Vec<VersionedSchemaEntry>,
}
impl Default for VersionedGraphQLBuilder {
fn default() -> Self {
Self::new()
}
}
impl VersionedGraphQLBuilder {
pub fn new() -> Self {
Self {
base_path: None,
entries: Vec::new(),
}
}
pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
let path = path.into();
let normalized = if !path.starts_with('/') {
format!("/{}", path.trim_end_matches('/'))
} else {
path.trim_end_matches('/').to_string()
};
self.base_path = Some(normalized);
self
}
pub fn add_version<E>(mut self, version: ApiVersion, executor: E) -> Self
where
E: Executor + Clone + Send + Sync + 'static,
{
let entry = VersionedSchemaEntry {
version,
deprecation: None,
executor: ErasedSchema::from_executor(executor),
};
self.entries.push(entry);
self
}
pub fn add_version_deprecated<E>(
mut self,
version: ApiVersion,
executor: E,
deprecation: DeprecationInfo,
) -> Self
where
E: Executor + Clone + Send + Sync + 'static,
{
let entry = VersionedSchemaEntry {
version,
deprecation: Some(deprecation),
executor: ErasedSchema::from_executor(executor),
};
self.entries.push(entry);
self
}
pub fn build(self) -> VersionedGraphQL {
VersionedGraphQL {
base_path: self.base_path,
entries: self.entries,
}
}
}
pub type GraphQLBuilder = VersionedGraphQLBuilder;
pub fn apply_config_to_builder<Q, M, S>(
mut builder: SchemaBuilder<Q, M, S>,
config: &GraphQLConfig,
) -> SchemaBuilder<Q, M, S>
where
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static,
{
if let Some(depth) = config.max_query_depth {
builder = builder.limit_depth(depth);
}
if let Some(complexity) = config.max_query_complexity {
builder = builder.limit_complexity(complexity);
}
if !config.introspection_enabled {
builder = builder.disable_introspection();
}
builder
}