grafbase_sdk_mock/
lib.rs

1//! Provides a dynamic GraphQL schema and subgraph implementation that can be built and executed at runtime.
2//!
3//! This crate allows creating GraphQL schemas dynamically from SDL (Schema Definition Language) strings
4//! and executing queries against them. It also provides functionality for running mock GraphQL servers
5//! using these dynamic schemas.
6
7#![deny(missing_docs)]
8
9mod builder;
10mod entity_resolver;
11mod resolver;
12mod server;
13
14use std::{
15    hash::{DefaultHasher, Hasher as _},
16    sync::Arc,
17};
18
19pub use async_graphql::dynamic::ResolverContext;
20pub use builder::GraphqlSubgraphBuilder;
21pub use entity_resolver::EntityResolverContext;
22pub use server::MockGraphQlServer;
23
24/// A dynamic subgraph implementation that can be started as a mock GraphQL server.
25#[derive(Debug, Clone)]
26pub struct GraphqlSubgraph {
27    executable_schema: async_graphql::dynamic::Schema,
28    schema: String,
29    name: String,
30}
31
32impl GraphqlSubgraph {
33    /// Creates a builder for constructing a new dynamic subgraph schema from SDL.
34    ///
35    /// # Arguments
36    ///
37    /// * `sdl` - GraphQL schema definition language string to build from
38    pub fn with_schema(sdl: impl AsRef<str>) -> GraphqlSubgraphBuilder {
39        let sdl = sdl.as_ref();
40        GraphqlSubgraphBuilder::new(sdl.to_string(), anonymous_name(sdl))
41    }
42
43    /// Starts this subgraph as a mock GraphQL server.
44    ///
45    /// Returns a handle to the running server that can be used to stop it.
46    pub async fn start(self) -> MockGraphQlServer {
47        MockGraphQlServer::new(self.name, Arc::new((self.executable_schema, self.schema))).await
48    }
49
50    /// Returns the GraphQL schema in SDL (Schema Definition Language)
51    pub fn schema(&self) -> &str {
52        &self.schema
53    }
54
55    /// Returns the name of this subgraph
56    pub fn name(&self) -> &str {
57        &self.name
58    }
59}
60
61/// A subgraph that only contains extension definitions. We do not spawn a GraphQL server for this subgraph.
62#[derive(Debug, Clone)]
63pub struct VirtualSubgraph {
64    schema: String,
65    name: String,
66}
67
68impl VirtualSubgraph {
69    /// Creates a new virtual subgraph with the given SDL and name.
70    pub fn new(name: &str, schema: &str) -> Self {
71        VirtualSubgraph {
72            name: name.to_string(),
73            schema: schema.to_string(),
74        }
75    }
76
77    /// Returns the GraphQL schema in SDL (Schema Definition Language)
78    pub fn schema(&self) -> &str {
79        &self.schema
80    }
81
82    /// Returns the name of this subgraph
83    pub fn name(&self) -> &str {
84        &self.name
85    }
86}
87
88/// A mock subgraph that can either be a full dynamic GraphQL service or just extension definitions.
89#[derive(Debug, Clone)]
90pub enum Subgraph {
91    /// A full dynamic subgraph that can be started as a GraphQL server
92    Graphql(GraphqlSubgraph),
93    /// A subgraph that only contains extension definitions and is not started as a server
94    Virtual(VirtualSubgraph),
95}
96
97impl From<GraphqlSubgraph> for Subgraph {
98    fn from(subgraph: GraphqlSubgraph) -> Self {
99        Subgraph::Graphql(subgraph)
100    }
101}
102
103impl From<GraphqlSubgraphBuilder> for Subgraph {
104    fn from(builder: GraphqlSubgraphBuilder) -> Self {
105        builder.build().into()
106    }
107}
108
109impl From<VirtualSubgraph> for Subgraph {
110    fn from(subgraph: VirtualSubgraph) -> Self {
111        Subgraph::Virtual(subgraph)
112    }
113}
114
115impl<T: Into<Subgraph>> From<(String, T)> for Subgraph {
116    fn from((name, subgraph): (String, T)) -> Self {
117        (name.as_str(), subgraph).into()
118    }
119}
120
121impl<T: Into<Subgraph>> From<(&str, T)> for Subgraph {
122    fn from((name, subgraph): (&str, T)) -> Self {
123        match subgraph.into() {
124            Subgraph::Graphql(mut graphql_subgraph) => {
125                graphql_subgraph.name = name.to_string();
126                Subgraph::Graphql(graphql_subgraph)
127            }
128            Subgraph::Virtual(mut virtual_subgraph) => {
129                virtual_subgraph.name = name.to_string();
130                Subgraph::Virtual(virtual_subgraph)
131            }
132        }
133    }
134}
135
136impl From<String> for Subgraph {
137    fn from(sdl: String) -> Self {
138        sdl.as_str().into()
139    }
140}
141
142impl From<&str> for Subgraph {
143    fn from(schema: &str) -> Self {
144        Subgraph::Virtual(VirtualSubgraph::new(&anonymous_name(schema), schema))
145    }
146}
147
148fn anonymous_name(schema: &str) -> String {
149    let mut hasher = DefaultHasher::default();
150    hasher.write(schema.as_bytes());
151    format!("anonymous{:X}", hasher.finish())
152}
153
154#[cfg(test)]
155mod tests {
156
157    use crate::*;
158
159    #[tokio::test]
160    async fn echo_header() {
161        let subgraph = GraphqlSubgraph::with_schema(r#"type Query { header(name: String!): String }"#)
162            .with_resolver("Query", "header", |ctx: ResolverContext<'_>| {
163                ctx.data_unchecked::<http::HeaderMap>()
164                    .get(ctx.args.get("name").unwrap().string().unwrap())
165                    .map(|value| value.to_str().unwrap().to_owned().into())
166            })
167            .build();
168
169        let server = subgraph.start().await;
170
171        let response = reqwest::Client::new()
172            .post(server.url().clone())
173            .body(serde_json::to_vec(&serde_json::json!({"query":r#"query { header(name: "hi") }"#})).unwrap())
174            .header("hi", "John")
175            .send()
176            .await
177            .unwrap()
178            .text()
179            .await
180            .unwrap();
181
182        let response: serde_json::Value = serde_json::from_str(&response).unwrap_or_else(|err| {
183            panic!(
184                "Failed to parse response as JSON: {}\nResponse body:\n{}",
185                err, response
186            )
187        });
188        insta::assert_json_snapshot!(response, @r#"
189        {
190          "data": {
191            "header": "John"
192          }
193        }
194        "#);
195    }
196}