Skip to main content

armature_graphql/
lib.rs

1// GraphQL support for Armature framework
2
3pub 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
26/// GraphQL schema wrapper
27pub 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
57// Provider is automatically implemented via blanket impl
58// when Query, Mutation, Subscription all satisfy Send + Sync + 'static
59
60/// GraphQL request handling
61pub 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
87/// GraphQL response
88pub 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
135/// Helper to build GraphQL schemas with DI integration
136pub 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
197/// GraphQL playground HTML
198///
199/// # Example
200///
201/// ```
202/// use armature_graphql::graphql_playground_html;
203///
204/// let html = graphql_playground_html("/graphql");
205/// assert!(html.contains("GraphQL Playground"));
206/// ```
207pub 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
235/// GraphiQL HTML (lighter alternative)
236pub 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}