1#![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#[derive(Debug, Clone)]
26pub struct GraphqlSubgraph {
27 executable_schema: async_graphql::dynamic::Schema,
28 schema: String,
29 name: String,
30}
31
32impl GraphqlSubgraph {
33 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 pub async fn start(self) -> MockGraphQlServer {
47 MockGraphQlServer::new(self.name, Arc::new((self.executable_schema, self.schema))).await
48 }
49
50 pub fn schema(&self) -> &str {
52 &self.schema
53 }
54
55 pub fn name(&self) -> &str {
57 &self.name
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct VirtualSubgraph {
64 schema: String,
65 name: String,
66}
67
68impl VirtualSubgraph {
69 pub fn new(name: &str, schema: &str) -> Self {
71 VirtualSubgraph {
72 name: name.to_string(),
73 schema: schema.to_string(),
74 }
75 }
76
77 pub fn schema(&self) -> &str {
79 &self.schema
80 }
81
82 pub fn name(&self) -> &str {
84 &self.name
85 }
86}
87
88#[derive(Debug, Clone)]
90pub enum Subgraph {
91 Graphql(GraphqlSubgraph),
93 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}