Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extract document building from OperationBuilder #1004

Merged
merged 2 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 13 additions & 44 deletions cynic/src/operation/builder.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::{borrow::Cow, collections::HashSet, marker::PhantomData, rc::Rc, sync::mpsc};
use std::{borrow::Cow, collections::HashSet, marker::PhantomData};

use crate::{
queries::{SelectionBuilder, SelectionSet},
queries::{build_executable_document, OperationType},
schema::{MutationRoot, QueryRoot, SubscriptionRoot},
QueryFragment, QueryVariables,
};

use super::{variables::VariableDefinitions, Operation};
use super::Operation;

/// Low level builder for [Operation].
///
Expand All @@ -15,7 +15,7 @@ use super::{variables::VariableDefinitions, Operation};
/// this builder.
pub struct OperationBuilder<QueryFragment, Variables = ()> {
variables: Option<Variables>,
operation_kind: OperationKind,
operation_kind: OperationType,
operation_name: Option<Cow<'static, str>>,
features: HashSet<String>,
phantom: PhantomData<fn() -> QueryFragment>,
Expand All @@ -26,7 +26,7 @@ where
Fragment: QueryFragment,
Variables: QueryVariables,
{
fn new(operation_kind: OperationKind) -> Self {
fn new(operation_kind: OperationType) -> Self {
OperationBuilder {
variables: None,
operation_kind,
Expand All @@ -41,23 +41,23 @@ where
where
Fragment::SchemaType: QueryRoot,
{
Self::new(OperationKind::Query)
Self::new(OperationType::Query)
}

/// Creates an `OperationBuilder` for a mutation operation
pub fn mutation() -> Self
where
Fragment::SchemaType: MutationRoot,
{
Self::new(OperationKind::Mutation)
Self::new(OperationType::Mutation)
}

/// Creates an `OperationBuilder` for a subscription operation
pub fn subscription() -> Self
where
Fragment::SchemaType: SubscriptionRoot,
{
Self::new(OperationKind::Subscription)
Self::new(OperationType::Subscription)
}

/// Adds variables for the operation
Expand Down Expand Up @@ -99,37 +99,12 @@ where

/// Tries to builds an [Operation]
pub fn build(self) -> Result<super::Operation<Fragment, Variables>, OperationBuildError> {
use std::fmt::Write;

let features_enabled = Rc::new(self.features);
let mut selection_set = SelectionSet::default();
let (variable_tx, variable_rx) = mpsc::channel();
let builder = SelectionBuilder::<_, Fragment::VariablesFields>::new(
&mut selection_set,
&variable_tx,
&features_enabled,
);

Fragment::query(builder);

let vars = VariableDefinitions::new::<Variables>(variable_rx.try_iter().collect());

let name_str = self.operation_name.as_deref().unwrap_or("");

let declaration_str = match self.operation_kind {
OperationKind::Query => "query",
OperationKind::Mutation => "mutation",
OperationKind::Subscription => "subscription",
};

let mut query = String::new();
writeln!(
&mut query,
"{declaration_str} {name_str}{vars}{selection_set}"
)?;

Ok(Operation {
query,
query: build_executable_document::<Fragment, Variables>(
self.operation_kind,
self.operation_name.as_deref(),
self.features.clone(),
),
variables: self.variables.ok_or(OperationBuildError::VariablesNotSet)?,
operation_name: self.operation_name,
phantom: PhantomData,
Expand All @@ -147,9 +122,3 @@ pub enum OperationBuildError {
/// Error when a write! call that builds the query string failed
CouldntBuildQueryString(#[from] std::fmt::Error),
}

enum OperationKind {
Query,
Mutation,
Subscription,
}
1 change: 0 additions & 1 deletion cynic/src/operation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::{
};

mod builder;
mod variables;

pub use builder::{OperationBuildError, OperationBuilder};

Expand Down
65 changes: 65 additions & 0 deletions cynic/src/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ mod indent;
mod input_literal_ser;
mod recurse;
mod type_eq;
mod variables;

use variables::VariableDefinitions;

pub use self::{
ast::{Argument, InputLiteral, SelectionSet},
Expand All @@ -16,3 +19,65 @@ pub use self::{
recurse::Recursable,
type_eq::IsFieldType,
};

use std::{collections::HashSet, rc::Rc, sync::mpsc};

/// Builds an executable document for the given Fragment
///
/// Users should prefer to use `crate::QueryBuilder`, `crate::MutationBuilder` &
/// `crate::SubscriptionBuilder` over this function, but may prefer to use this
/// function when they need to construct an executable document without first constructing an
/// `Operation` (e.g. when they do not have easy access to a variable struct)
///
/// Note that this function does not enforce as much safety as the regular
/// builders and relies on the user providing the right `r#type` and correct variables
/// when submitting the query.
pub fn build_executable_document<Fragment, Variables>(
r#type: OperationType,
operation_name: Option<&str>,
features_enabled: HashSet<String>,
) -> String
where
Fragment: crate::QueryFragment,
Variables: crate::QueryVariables,
{
let features_enabled = Rc::new(features_enabled);
let mut selection_set = SelectionSet::default();
let (variable_tx, variable_rx) = mpsc::channel();
let builder = SelectionBuilder::<_, Fragment::VariablesFields>::new(
&mut selection_set,
&variable_tx,
&features_enabled,
);

Fragment::query(builder);

let vars = VariableDefinitions::new::<Variables>(variable_rx.try_iter().collect());

let name_str = operation_name.unwrap_or("");

let declaration_str = r#type.as_str();

format!("{declaration_str} {name_str}{vars}{selection_set}")
}

/// The kind of operation to build an executable document for
pub enum OperationType {
/// A query operation
Query,
/// A mutation operation
Mutation,
/// A subscription operation
Subscription,
}

impl OperationType {
/// The operation type as it would appear in a GraphQl document
pub fn as_str(&self) -> &'static str {
match self {
OperationType::Query => "query",
OperationType::Mutation => "mutation",
OperationType::Subscription => "subscription",
}
}
}
96 changes: 96 additions & 0 deletions cynic/src/queries/variables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::{variables::VariableType, QueryVariables};

pub struct VariableDefinitions<'a> {
vars: Vec<&'a (&'static str, VariableType)>,
}

impl<'a> VariableDefinitions<'a> {
pub fn new<T: QueryVariables>(used_variables: Vec<&str>) -> Self {
let vars = T::VARIABLES
.iter()
.filter(|(name, _)| used_variables.contains(name))
.collect();

VariableDefinitions { vars }
}
}

impl std::fmt::Display for VariableDefinitions<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.vars.is_empty() {
return Ok(());
}

write!(f, "(")?;
let mut first = true;
for (name, ty) in &self.vars {
if !first {
write!(f, ", ")?;
}
first = false;

let ty = GraphqlVariableType::new(*ty);
write!(f, "${name}: {ty}")?;
}
write!(f, ")")
}
}

enum GraphqlVariableType {
List(Box<GraphqlVariableType>),
NotNull(Box<GraphqlVariableType>),
Named(&'static str),
}

impl GraphqlVariableType {
fn new(ty: VariableType) -> Self {
fn recurse(ty: VariableType, required: bool) -> GraphqlVariableType {
match (ty, required) {
(VariableType::Nullable(inner), _) => recurse(*inner, false),
(any, true) => GraphqlVariableType::NotNull(Box::new(recurse(any, false))),
(VariableType::List(inner), _) => {
GraphqlVariableType::List(Box::new(recurse(*inner, true)))
}
(VariableType::Named(name), false) => GraphqlVariableType::Named(name),
}
}

recurse(ty, true)
}
}

impl std::fmt::Display for GraphqlVariableType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphqlVariableType::List(inner) => write!(f, "[{inner}]"),
GraphqlVariableType::NotNull(inner) => write!(f, "{inner}!"),
GraphqlVariableType::Named(name) => write!(f, "{name}"),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_variable_printing() {
insta::assert_display_snapshot!(VariableDefinitions {
vars: vec![
&("foo", VariableType::List(&VariableType::Named("Foo"))),
&("bar", VariableType::Named("Bar")),
&("nullable_bar", VariableType::Nullable(&VariableType::Named("Bar"))),
&(
"nullable_list_foo",
VariableType::Nullable(&(VariableType::List(&VariableType::Named("Foo"))))
),
&(
"nullable_list_nullable_foo",
VariableType::Nullable(&VariableType::List(&VariableType::Nullable(
&VariableType::Named("Foo")
)))
)
]
}, @"($foo: [Foo!]!, $bar: Bar!, $nullable_bar: Bar, $nullable_list_foo: [Foo!], $nullable_list_nullable_foo: [Foo])")
}
}
3 changes: 3 additions & 0 deletions cynic/src/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub trait QueryVariables {
/// A struct that determines which variables are available when using this
/// struct.
type Fields: QueryVariablesFields;

/// An associated constant that contains the variable names & their types.
///
/// This is used to construct the query string we send to a server.
Expand All @@ -33,8 +34,10 @@ pub trait QueryVariablesFields {}

impl QueryVariables for () {
type Fields = ();

const VARIABLES: &'static [(&'static str, VariableType)] = &[];
}

impl QueryVariablesFields for () {}

#[doc(hidden)]
Expand Down
Loading