1pub mod config;
4pub mod decorators;
5pub mod federation;
6pub mod resolver;
7pub mod schema_builder;
8pub mod schema_docs;
9
10pub use async_graphql;
11pub use async_graphql::{
12 Context, EmptyMutation, EmptySubscription, Enum, Error, ID, InputObject, MergedObject,
13 MergedSubscription, Object, Result, Schema, SimpleObject, Subscription, Union,
14};
15
16pub use config::*;
17pub use decorators::*;
18pub use resolver::*;
19pub use schema_builder::*;
20pub use schema_docs::*;
21
22use armature_core::Error as ArmatureError;
23use armature_log::info;
24use std::sync::Arc;
25
26pub struct GraphQLSchema<Query, Mutation, Subscription> {
28 schema: Arc<Schema<Query, Mutation, Subscription>>,
29}
30
31impl<Query, Mutation, Subscription> GraphQLSchema<Query, Mutation, Subscription>
32where
33 Query: async_graphql::ObjectType + 'static,
34 Mutation: async_graphql::ObjectType + 'static,
35 Subscription: async_graphql::SubscriptionType + 'static,
36{
37 pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
38 info!("Initializing GraphQL schema");
39 Self {
40 schema: Arc::new(schema),
41 }
42 }
43
44 pub fn schema(&self) -> Arc<Schema<Query, Mutation, Subscription>> {
45 self.schema.clone()
46 }
47}
48
49impl<Query, Mutation, Subscription> Clone for GraphQLSchema<Query, Mutation, Subscription> {
50 fn clone(&self) -> Self {
51 Self {
52 schema: self.schema.clone(),
53 }
54 }
55}
56
57pub struct GraphQLRequest {
62 pub query: String,
63 pub variables: Option<serde_json::Value>,
64 pub operation_name: Option<String>,
65}
66
67impl GraphQLRequest {
68 pub fn new(query: String) -> Self {
69 Self {
70 query,
71 variables: None,
72 operation_name: None,
73 }
74 }
75
76 pub fn with_variables(mut self, variables: serde_json::Value) -> Self {
77 self.variables = Some(variables);
78 self
79 }
80
81 pub fn with_operation(mut self, operation_name: String) -> Self {
82 self.operation_name = Some(operation_name);
83 self
84 }
85}
86
87pub struct GraphQLResponse {
89 pub data: Option<serde_json::Value>,
90 pub errors: Vec<String>,
91}
92
93impl GraphQLResponse {
94 pub fn success(data: serde_json::Value) -> Self {
95 Self {
96 data: Some(data),
97 errors: Vec::new(),
98 }
99 }
100
101 pub fn error(message: String) -> Self {
102 Self {
103 data: None,
104 errors: vec![message],
105 }
106 }
107
108 pub fn to_json(&self) -> Result<String, ArmatureError> {
109 serde_json::to_string(self).map_err(|e| ArmatureError::Serialization(e.to_string()))
110 }
111}
112
113impl serde::Serialize for GraphQLResponse {
114 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115 where
116 S: serde::Serializer,
117 {
118 use serde::ser::SerializeMap;
119 let mut map = serializer.serialize_map(None)?;
120
121 if let Some(ref data) = self.data {
122 map.serialize_entry("data", data)?;
123 } else {
124 map.serialize_entry("data", &None::<()>)?;
125 }
126
127 if !self.errors.is_empty() {
128 map.serialize_entry("errors", &self.errors)?;
129 }
130
131 map.end()
132 }
133}
134
135pub struct SchemaBuilder<Query, Mutation, Subscription> {
137 query: Option<Query>,
138 mutation: Option<Mutation>,
139 subscription: Option<Subscription>,
140}
141
142impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription>
143where
144 Query: async_graphql::ObjectType + 'static,
145 Mutation: async_graphql::ObjectType + 'static,
146 Subscription: async_graphql::SubscriptionType + 'static,
147{
148 pub fn new() -> Self {
149 Self {
150 query: None,
151 mutation: None,
152 subscription: None,
153 }
154 }
155
156 pub fn query(mut self, query: Query) -> Self {
157 self.query = Some(query);
158 self
159 }
160
161 pub fn mutation(mut self, mutation: Mutation) -> Self {
162 self.mutation = Some(mutation);
163 self
164 }
165
166 pub fn subscription(mut self, subscription: Subscription) -> Self {
167 self.subscription = Some(subscription);
168 self
169 }
170
171 pub fn build(self) -> Result<Schema<Query, Mutation, Subscription>, ArmatureError> {
172 let query = self
173 .query
174 .ok_or_else(|| ArmatureError::Internal("Query root is required".to_string()))?;
175 let mutation = self
176 .mutation
177 .ok_or_else(|| ArmatureError::Internal("Mutation root is required".to_string()))?;
178 let subscription = self
179 .subscription
180 .ok_or_else(|| ArmatureError::Internal("Subscription root is required".to_string()))?;
181
182 Ok(Schema::build(query, mutation, subscription).finish())
183 }
184}
185
186impl<Query, Mutation, Subscription> Default for SchemaBuilder<Query, Mutation, Subscription>
187where
188 Query: async_graphql::ObjectType + 'static,
189 Mutation: async_graphql::ObjectType + 'static,
190 Subscription: async_graphql::SubscriptionType + 'static,
191{
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197pub fn graphql_playground_html(endpoint: &str) -> String {
208 format!(
209 r#"
210<!DOCTYPE html>
211<html>
212<head>
213 <meta charset="utf-8" />
214 <meta name="viewport" content="width=device-width, initial-scale=1" />
215 <title>GraphQL Playground</title>
216 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
217 <script src="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
218</head>
219<body>
220 <div id="root"></div>
221 <script>
222 window.addEventListener('load', function (event) {{
223 GraphQLPlayground.init(document.getElementById('root'), {{
224 endpoint: '{}'
225 }})
226 }})
227 </script>
228</body>
229</html>
230"#,
231 endpoint
232 )
233}
234
235pub fn graphiql_html(endpoint: &str) -> String {
237 format!(
238 r#"
239<!DOCTYPE html>
240<html>
241<head>
242 <title>GraphiQL</title>
243 <style>
244 body {{
245 height: 100vh;
246 margin: 0;
247 width: 100%;
248 overflow: hidden;
249 }}
250 #graphiql {{
251 height: 100vh;
252 }}
253 </style>
254 <script
255 crossorigin
256 src="https://unpkg.com/react@18/umd/react.production.min.js"
257 ></script>
258 <script
259 crossorigin
260 src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
261 ></script>
262 <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
263</head>
264<body>
265 <div id="graphiql">Loading...</div>
266 <script
267 src="https://unpkg.com/graphiql/graphiql.min.js"
268 type="application/javascript"
269 ></script>
270 <script>
271 const fetcher = GraphiQL.createFetcher({{
272 url: '{}',
273 }});
274
275 ReactDOM.render(
276 React.createElement(GraphiQL, {{ fetcher: fetcher }}),
277 document.getElementById('graphiql'),
278 );
279 </script>
280</body>
281</html>
282"#,
283 endpoint
284 )
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_graphql_request_builder() {
293 let req = GraphQLRequest::new("query { hello }".to_string())
294 .with_variables(serde_json::json!({"name": "World"}))
295 .with_operation("HelloQuery".to_string());
296
297 assert_eq!(req.query, "query { hello }");
298 assert!(req.variables.is_some());
299 assert_eq!(req.operation_name, Some("HelloQuery".to_string()));
300 }
301
302 #[test]
303 fn test_graphql_response_serialization() {
304 let response = GraphQLResponse::success(serde_json::json!({
305 "hello": "world"
306 }));
307
308 let json = response.to_json().unwrap();
309 assert!(json.contains("hello"));
310 assert!(json.contains("world"));
311 }
312}