use super::error::Error;
use config::Configuration;
use context::{GraphQLContext, RequestContext};
use database::{CrudOperation, DatabaseEndpoint, DatabasePool};
use events::{EventFacade, EventHandlerBag};
use juniper::http::GraphQLRequest;
use log::debug;
use resolvers::Resolvers;
use schema::{create_root_node, Info, NodeType, RootRef};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::option::Option;
use std::sync::Arc;
use validators::Validators;
pub mod config;
pub mod context;
pub mod database;
pub mod events;
pub mod loader;
pub mod objects;
pub mod resolvers;
pub mod schema;
pub mod validators;
pub mod value;
#[derive(Clone)]
pub struct EngineBuilder<RequestCtx = ()>
where
RequestCtx: RequestContext,
{
config: Configuration,
db_pool: <<RequestCtx as RequestContext>::DBEndpointType as DatabaseEndpoint>::PoolType,
event_handlers: EventHandlerBag<RequestCtx>,
resolvers: Resolvers<RequestCtx>,
validators: Validators,
version: Option<String>,
}
impl<RequestCtx> EngineBuilder<RequestCtx>
where
RequestCtx: RequestContext,
{
pub fn with_resolvers(mut self, resolvers: Resolvers<RequestCtx>) -> EngineBuilder<RequestCtx> {
self.resolvers = resolvers;
self
}
pub fn with_validators(mut self, validators: Validators) -> EngineBuilder<RequestCtx> {
self.validators = validators;
self
}
pub fn with_event_handlers(
mut self,
event_handlers: EventHandlerBag<RequestCtx>,
) -> EngineBuilder<RequestCtx> {
self.event_handlers = event_handlers;
self
}
pub fn with_version(mut self, version: String) -> EngineBuilder<RequestCtx> {
self.version = Some(version);
self
}
pub fn build(mut self) -> Result<Engine<RequestCtx>, Error> {
self.validate()?;
for event_handler in self.event_handlers.before_engine_build() {
event_handler(&mut self.config)?;
}
let root_node = create_root_node(&self.config)?;
let engine = Engine::<RequestCtx> {
config: self.config,
db_pool: self.db_pool,
resolvers: self.resolvers,
validators: self.validators,
event_handlers: self.event_handlers,
version: self.version,
root_node,
};
Ok(engine)
}
fn validate(&self) -> Result<(), Error> {
self.config.validate()?;
self.config
.endpoints()
.map(|e| {
if !self.resolvers.contains_key(e.name()) {
Err(Error::ResolverNotFound {
name: e.name().to_string(),
})
} else {
Ok(())
}
})
.collect::<Result<Vec<_>, Error>>()?;
self.config
.types()
.map(|t| {
t.props()
.filter_map(|p| p.resolver())
.map(|r| {
if !self.resolvers.contains_key(r) {
Err(Error::ResolverNotFound {
name: r.to_string(),
})
} else {
Ok(())
}
})
.collect::<Result<Vec<_>, Error>>()?;
t.props()
.filter_map(|p| p.validator())
.map(|v| {
if !self.validators.contains_key(v) {
Err(Error::ValidatorNotFound {
name: v.to_string(),
})
} else {
Ok(())
}
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(())
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(())
}
}
impl Debug for EngineBuilder {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_struct("EngineBuilder")
.field("config", &self.config)
.field("version", &self.version)
.finish()
}
}
#[derive(Clone)]
pub struct Engine<RequestCtx = ()>
where
RequestCtx: RequestContext,
{
config: Configuration,
db_pool: <<RequestCtx as RequestContext>::DBEndpointType as DatabaseEndpoint>::PoolType,
resolvers: Resolvers<RequestCtx>,
validators: Validators,
event_handlers: EventHandlerBag<RequestCtx>,
version: Option<String>,
root_node: RootRef<RequestCtx>,
}
impl<RequestCtx> Engine<RequestCtx>
where
RequestCtx: RequestContext,
{
#[allow(clippy::new_ret_no_self)]
pub fn new(
config: Configuration,
database_pool: <<RequestCtx as RequestContext>::DBEndpointType as DatabaseEndpoint>::PoolType,
) -> EngineBuilder<RequestCtx> {
EngineBuilder::<RequestCtx> {
config,
db_pool: database_pool,
resolvers: HashMap::new(),
validators: HashMap::new(),
event_handlers: EventHandlerBag::new(),
version: None,
}
}
#[tracing::instrument(name = "wg-execute", skip(self, query, input, metadata))]
pub async fn execute(
&self,
query: String,
input: Option<serde_json::Value>,
metadata: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
debug!("Engine::execute called -- input: {:#?}", input);
let mut rctx = RequestCtx::new();
let gql_schema: HashMap<String, NodeType> =
crate::engine::schema::generate_schema(&self.config)?;
let info = Info::new("".to_string(), Arc::new(gql_schema));
let before_request_handlers = self.event_handlers.before_request();
if !before_request_handlers.is_empty() {
let mut dbtx = self.db_pool.transaction().await?;
let gqlctx_tmp = GraphQLContext::<RequestCtx>::new(
self.db_pool.clone(),
self.resolvers.clone(),
self.validators.clone(),
self.event_handlers.clone(),
Some(rctx.clone()),
self.version.clone(),
metadata.clone(),
info.clone(),
);
for handler in before_request_handlers {
rctx = handler(
rctx,
EventFacade::new(CrudOperation::None, &gqlctx_tmp, &mut dbtx, &info),
metadata.clone(),
)
.await?;
}
std::mem::drop(dbtx);
}
let input_value: Option<juniper::InputValue> = match input {
Some(input) => Some(serde_json::from_value::<juniper::InputValue>(input)?),
None => None,
};
let gqlctx = GraphQLContext::<RequestCtx>::new(
self.db_pool.clone(),
self.resolvers.clone(),
self.validators.clone(),
self.event_handlers.clone(),
Some(rctx.clone()),
self.version.clone(),
metadata.clone(),
info.clone(),
);
let req = GraphQLRequest::new(query, None, input_value);
let res = req.execute(&self.root_node, &gqlctx).await;
let mut ret_value = serde_json::to_value(&res)?;
let after_request_handlers = self.event_handlers.after_request();
if !after_request_handlers.is_empty() {
let mut dbtx = self.db_pool.transaction().await?;
let gqlctx_tmp = GraphQLContext::<RequestCtx>::new(
self.db_pool.clone(),
self.resolvers.clone(),
self.validators.clone(),
self.event_handlers.clone(),
Some(rctx.clone()),
self.version.clone(),
metadata.clone(),
info.clone(),
);
for handler in self.event_handlers.after_request() {
ret_value = handler(
EventFacade::new(CrudOperation::None, &gqlctx_tmp, &mut dbtx, &info),
ret_value,
)
.await?;
}
std::mem::drop(dbtx);
}
debug!("Engine::execute -- ret_value: {:#?}", ret_value);
Ok(ret_value)
}
}
impl<RequestCtx> Display for Engine<RequestCtx>
where
RequestCtx: RequestContext,
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{:#?}", self)
}
}
impl<RequestCtx> Debug for Engine<RequestCtx>
where
RequestCtx: RequestContext,
{
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
f.debug_struct("Engine")
.field("config", &self.config)
.field("version", &self.version)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::EngineBuilder;
use crate::engine::database::no_database::NoDatabasePool;
use crate::engine::resolvers::{ResolverFacade, Resolvers};
use crate::engine::validators::Validators;
use crate::engine::value::Value;
use crate::{Configuration, Engine, Error};
use juniper::{BoxFuture, ExecutionResult};
use std::convert::TryInto;
use std::fs::File;
#[test]
fn engine_new() {
let _engine = Engine::<()>::new(
File::open("tests/fixtures/minimal.yml")
.expect("Couldn't read config")
.try_into()
.expect("Couldn't convert to config"),
NoDatabasePool {},
)
.build()
.unwrap();
}
#[test]
fn test_engine_validate_minimal() {
assert!(Engine::<()>::new(
File::open("tests/fixtures/config-validation/test_config_ok.yml")
.expect("Couldn't read config")
.try_into()
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.build()
.is_ok());
}
#[test]
fn test_engine_validate_custom_validators() {
let mut validators = Validators::new();
validators.insert("MyValidator".to_string(), Box::new(my_validator));
assert!(Engine::<()>::new(
File::open("tests/fixtures/minimal.yml")
.expect("Couldn't read config")
.try_into()
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_validators(validators)
.build()
.is_ok());
let mut validators = Validators::new();
validators.insert("MyValidator".to_string(), Box::new(my_validator));
assert!(Engine::<()>::new(
File::open("tests/fixtures/config-validation/test_config_with_custom_validator.yml")
.expect("Couldn't read config")
.try_into()
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_validators(validators)
.build()
.is_ok());
let validators = Validators::new();
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open(
"tests/fixtures/config-validation/test_config_with_custom_validator.yml"
)
.expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_validators(validators)
.build()
.is_err());
}
#[test]
fn test_engine_validate_custom_endpoint() {
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open("tests/fixtures/config-validation/test_config_ok.yml")
.expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.build()
.is_ok());
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open("tests/fixtures/config-validation/test_config_with_custom_resolver.yml")
.expect("Couldn't read config")
)
.expect("Couldn't convert config"),
NoDatabasePool {}
)
.build()
.is_err());
let mut resolvers = Resolvers::<()>::new();
resolvers.insert("MyResolver".to_string(), Box::new(my_resolver));
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open("tests/fixtures/config-validation/test_config_with_custom_resolver.yml")
.expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_resolvers(resolvers)
.build()
.is_ok());
}
#[test]
fn test_engine_validate_custom_prop() {
let mut resolvers = Resolvers::<()>::new();
resolvers.insert("MyResolver".to_string(), Box::new(my_resolver));
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open(
"tests/fixtures/config-validation/test_config_with_custom_prop_resolver.yml"
)
.expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_resolvers(resolvers)
.build()
.is_ok());
let mut resolvers = Resolvers::<()>::new();
resolvers.insert("MyResolver".to_string(), Box::new(my_resolver));
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open("tests/fixtures/minimal.yml").expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.with_resolvers(resolvers)
.build()
.is_ok());
assert!(Engine::<()>::new(
TryInto::<Configuration>::try_into(
File::open(
"tests/fixtures/config-validation/test_config_with_custom_prop_resolver.yml"
)
.expect("Couldn't read config")
)
.expect("Couldn't convert to config"),
NoDatabasePool {}
)
.build()
.is_err());
}
pub fn my_resolver(executor: ResolverFacade<()>) -> BoxFuture<ExecutionResult> {
Box::pin(async move { executor.resolve_scalar(1) })
}
#[allow(clippy::unnecessary_wraps)]
fn my_validator(_value: &Value) -> Result<(), Error> {
Ok(())
}
#[test]
fn test_engine_builder_send() {
fn assert_send<T: Send>() {}
assert_send::<EngineBuilder>();
}
#[test]
fn test_engine_builder_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<EngineBuilder>();
}
#[test]
fn test_engine_send() {
fn assert_send<T: Send>() {}
assert_send::<Engine>();
}
#[test]
fn test_engine_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Engine>();
}
}