dynamic-graphql-derive 0.10.2

Dynamic GraphQL schema macro
Documentation
use dynamic_graphql::App;

use self::node::NodeQuery;
use self::query::Query;
use crate::schema_utils::normalize_schema;

mod node {
    use std::collections::HashMap;
    use std::ops::Deref;
    use std::ops::DerefMut;

    use dynamic_graphql::Context;
    use dynamic_graphql::ExpandObject;
    use dynamic_graphql::ExpandObjectFields;
    use dynamic_graphql::Instance;
    use dynamic_graphql::Interface;
    use dynamic_graphql::experimental::GetSchemaData;

    use super::query::Query;

    pub type GetNode = fn(&str) -> Option<Instance<'static, dyn Node>>;
    #[derive(Default)]
    pub struct NodeData(HashMap<String, GetNode>);
    impl Deref for NodeData {
        type Target = HashMap<String, GetNode>;
        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }
    impl DerefMut for NodeData {
        fn deref_mut(&mut self) -> &mut Self::Target {
            &mut self.0
        }
    }

    fn format_id<T: Node>(node: &T) -> String {
        format!("{}:{}", T::get_name(), node.get_id())
    }

    fn parse_id(id: &str) -> Option<(String, String)> {
        let mut split = id.split(':');
        let name = split.next()?;
        let id = split.next()?;
        Some((name.to_string(), id.to_string()))
    }

    #[Interface]
    pub trait Node {
        fn id(&self) -> String
        where
            Self: Sized,
        {
            format_id(self)
        }
        #[graphql(skip)]
        fn get_id(&self) -> String;
        #[graphql(skip)]
        fn get_name() -> &'static str
        where
            Self: Sized;
    }

    #[derive(ExpandObject)]
    pub struct NodeQuery<'a>(&'a Query);

    #[ExpandObjectFields]
    impl NodeQuery<'_> {
        fn node(ctx: &Context<'_>, id: String) -> Option<Instance<'static, dyn Node>> {
            let (name, id) = parse_id(&id)?;
            let node_data = ctx.get_schema_data().get::<NodeData>()?;
            let get_node = node_data.0.get(&name)?;
            get_node(&id)
        }
    }
}

mod foo {
    use dynamic_graphql::Instance;
    use dynamic_graphql::SimpleObject;
    use dynamic_graphql::internal::Object;
    use dynamic_graphql::internal::Register;
    use dynamic_graphql::internal::Registry;

    use super::node::Node;
    use super::node::NodeData;

    #[derive(SimpleObject)]
    #[graphql(implements(Node))]
    #[graphql(register(RegisterFooNode))]
    pub struct FooNode {
        #[graphql(skip)]
        id: String,
        name: String,
    }

    impl Node for FooNode {
        fn get_id(&self) -> String {
            self.id.to_string()
        }
        fn get_name() -> &'static str {
            "FooNode"
        }
    }

    struct RegisterFooNode;
    impl Register for RegisterFooNode {
        fn register(mut registry: Registry) -> Registry {
            let node_data: &mut NodeData = registry.data.get_mut_or_default();
            node_data.insert(
                <FooNode as Object>::get_object_type_name().to_string(),
                |id| {
                    Some(Instance::new_owned(FooNode {
                        id: id.to_string(),
                        name: "foo".to_string(),
                    }))
                },
            );
            registry
        }
    }
}

mod query {
    use dynamic_graphql::SimpleObject;

    #[derive(SimpleObject)]
    #[graphql(root)]
    pub struct Query;
}

#[derive(App)]
struct App(Query, NodeQuery<'static>, foo::FooNode);

#[tokio::test]
async fn test() {
    let schema = App::create_schema().finish().unwrap();
    let sdl = schema.sdl();
    insta::assert_snapshot!(normalize_schema(&sdl), @r#"
    type FooNode implements Node {
      name: String!
      id: String!
    }

    interface Node {
      id: String!
    }

    type Query {
      node(id: String!): Node
    }

    "Directs the executor to include this field or fragment only when the `if` argument is true."
    directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

    "Directs the executor to skip this field or fragment when the `if` argument is true."
    directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

    schema {
      query: Query
    }
    "#);

    let query = r#"
        query {
            node(id: "FooNode:1") {
                id
                ... on FooNode {
                    name
                }
            }
        }
    "#;
    let res = schema.execute(query).await;
    assert_eq!(
        res.data.into_json().unwrap(),
        serde_json::json!({
            "node": {
                "id": "FooNode:1",
                "name": "foo",
            }
        })
    );
}