Skip to main content

mockforge_graphql/
schema.rs

1//! GraphQL schema parsing and generation
2
3use async_graphql::{Object, Schema, Subscription};
4use futures::stream::Stream;
5use std::time::Duration;
6
7/// Simple User type for GraphQL
8#[derive(async_graphql::SimpleObject, Clone)]
9pub struct User {
10    /// Unique identifier
11    pub id: String,
12    /// User's full name
13    pub name: String,
14    /// User's email address
15    pub email: String,
16}
17
18/// Simple Post type for GraphQL
19#[derive(async_graphql::SimpleObject)]
20pub struct Post {
21    /// Unique identifier
22    pub id: String,
23    /// Post title
24    pub title: String,
25    /// Post content
26    pub content: String,
27    /// Author of the post
28    pub author: User,
29}
30
31/// Root query type
32pub struct QueryRoot;
33
34#[Object]
35impl QueryRoot {
36    /// Get all users
37    async fn users(&self, limit: Option<i32>) -> Vec<User> {
38        let limit = limit.unwrap_or(10) as usize;
39        (0..limit.min(100))
40            .map(|i| User {
41                id: format!("user-{}", i),
42                name: format!("User {}", i),
43                email: format!("user{}@example.com", i),
44            })
45            .collect()
46    }
47
48    /// Get a user by ID
49    async fn user(&self, id: String) -> Option<User> {
50        Some(User {
51            id,
52            name: "Mock User".to_string(),
53            email: "mock@example.com".to_string(),
54        })
55    }
56
57    /// Get all posts
58    async fn posts(&self, limit: Option<i32>) -> Vec<Post> {
59        let limit = limit.unwrap_or(10) as usize;
60        (0..limit.min(50))
61            .map(|i| Post {
62                id: format!("post-{}", i),
63                title: format!("Post {}", i),
64                content: format!("This is the content of post {}", i),
65                author: User {
66                    id: format!("user-{}", i % 5),
67                    name: format!("Author {}", i % 5),
68                    email: format!("author{}@example.com", i % 5),
69                },
70            })
71            .collect()
72    }
73}
74
75/// Root mutation type. A single `createUser` mutation lets clients
76/// exercise the mutation dispatch path against the default schema
77/// without registering a custom SDL. The mutation is stateless — the
78/// "created" user is fabricated from the inputs plus a deterministic
79/// id derived from (name, email), so callers can assert on the shape
80/// of the return value without mocking storage.
81pub struct MutationRoot;
82
83#[Object]
84impl MutationRoot {
85    /// Create a new user. Returns a freshly-shaped `User` with the
86    /// supplied `name` / `email` and an id derived from them so the
87    /// output is deterministic for tests (`user-{12 hex chars}`).
88    async fn create_user(&self, name: String, email: String) -> User {
89        use std::collections::hash_map::DefaultHasher;
90        use std::hash::{Hash, Hasher};
91        let mut hasher = DefaultHasher::new();
92        name.hash(&mut hasher);
93        email.hash(&mut hasher);
94        // Mask to 48 bits so the hex suffix is always exactly 12 chars
95        // (tests can then pin the exact id length without depending on
96        // how the hasher happens to fill the high bits).
97        let id = format!("user-{:012x}", hasher.finish() & 0xffff_ffff_ffff);
98        User { id, name, email }
99    }
100}
101
102/// Root subscription type. A single `tick` subscription lets clients
103/// exercise the subscription dispatch path (WebSocket via
104/// `graphql-transport-ws` / `graphql-ws`, SSE, etc.) against the
105/// default schema without registering a custom SDL.
106///
107/// `tick` emits a monotonically increasing `i32` every 100ms, starting
108/// from 1, up to the requested `count`. Clients without a `count`
109/// argument get 5 ticks by default. The interval is short enough that
110/// multi-event tests complete in well under a second.
111pub struct SubscriptionRoot;
112
113#[Subscription]
114impl SubscriptionRoot {
115    /// Emit `count` ticks (default 5) at 100ms intervals.
116    async fn tick(&self, count: Option<i32>) -> impl Stream<Item = i32> {
117        let n = count.unwrap_or(5).max(1) as usize;
118        async_stream::stream! {
119            for i in 1..=n {
120                tokio::time::sleep(Duration::from_millis(100)).await;
121                yield i as i32;
122            }
123        }
124    }
125}
126
127/// GraphQL schema manager
128pub struct GraphQLSchema {
129    schema: Schema<QueryRoot, MutationRoot, SubscriptionRoot>,
130}
131
132impl GraphQLSchema {
133    /// Create a new basic schema
134    pub fn new() -> Self {
135        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot).finish();
136        Self { schema }
137    }
138
139    /// Get the underlying schema
140    pub fn schema(&self) -> &Schema<QueryRoot, MutationRoot, SubscriptionRoot> {
141        &self.schema
142    }
143
144    /// Generate a basic schema with common types
145    pub fn generate_basic_schema() -> Self {
146        Self::new()
147    }
148}
149
150impl Default for GraphQLSchema {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_user_creation() {
162        let user = User {
163            id: "user-1".to_string(),
164            name: "John Doe".to_string(),
165            email: "john@example.com".to_string(),
166        };
167
168        assert_eq!(user.id, "user-1");
169        assert_eq!(user.name, "John Doe");
170        assert_eq!(user.email, "john@example.com");
171    }
172
173    #[test]
174    fn test_post_creation() {
175        let user = User {
176            id: "user-1".to_string(),
177            name: "Author".to_string(),
178            email: "author@example.com".to_string(),
179        };
180
181        let post = Post {
182            id: "post-1".to_string(),
183            title: "Test Post".to_string(),
184            content: "This is a test post".to_string(),
185            author: user.clone(),
186        };
187
188        assert_eq!(post.id, "post-1");
189        assert_eq!(post.title, "Test Post");
190        assert_eq!(post.author.name, "Author");
191    }
192
193    #[test]
194    fn test_query_root_creation() {
195        let _query = QueryRoot;
196        // Should create successfully
197    }
198
199    #[test]
200    fn test_graphql_schema_new() {
201        let schema = GraphQLSchema::new();
202        assert!(!schema.schema().sdl().is_empty());
203    }
204
205    #[test]
206    fn test_graphql_schema_default() {
207        let schema = GraphQLSchema::default();
208        assert!(!schema.schema().sdl().is_empty());
209    }
210
211    #[test]
212    fn test_graphql_schema_generate_basic() {
213        let schema = GraphQLSchema::generate_basic_schema();
214        let sdl = schema.schema().sdl();
215        assert!(sdl.contains("Query"));
216    }
217}