1use reqwest::header::{HeaderName, HeaderValue};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Serialize)]
6struct IntrospectionQuery {
7 query: String,
8}
9
10#[derive(Debug, Deserialize)]
11struct IntrospectionResponse {
12 data: Option<IntrospectionData>,
13 errors: Option<Vec<GraphQLError>>,
14}
15
16#[derive(Debug, Deserialize)]
17struct GraphQLError {
18 message: String,
19}
20
21#[derive(Debug, Deserialize)]
22struct IntrospectionData {
23 #[serde(rename = "__schema")]
24 schema: Schema,
25}
26
27#[derive(Debug, Deserialize)]
28#[allow(dead_code)]
29pub struct Schema {
30 pub query_type: Option<TypeRef>,
31 pub mutation_type: Option<TypeRef>,
32 pub subscription_type: Option<TypeRef>,
33 pub types: Vec<Type>,
34 pub directives: Vec<Directive>,
35}
36
37#[derive(Debug, Deserialize)]
38#[allow(dead_code)]
39pub struct Type {
40 pub name: Option<String>,
41 pub kind: TypeKind,
42 pub description: Option<String>,
43 pub fields: Option<Vec<Field>>,
44 pub interfaces: Option<Vec<TypeRef>>,
45 pub possible_types: Option<Vec<TypeRef>>,
46 pub enum_values: Option<Vec<EnumValue>>,
47 pub input_fields: Option<Vec<InputValue>>,
48 pub of_type: Option<Box<TypeRef>>,
49}
50
51#[derive(Debug, Deserialize)]
52#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
53pub enum TypeKind {
54 Scalar,
55 Object,
56 Interface,
57 Union,
58 Enum,
59 InputObject,
60 List,
61 NonNull,
62}
63
64#[derive(Debug, Deserialize)]
65pub struct TypeRef {
66 pub name: Option<String>,
67 pub kind: Option<TypeKind>,
68 pub of_type: Option<Box<TypeRef>>,
69}
70
71#[derive(Debug, Deserialize)]
72#[allow(dead_code)]
73pub struct Field {
74 pub name: String,
75 pub description: Option<String>,
76 pub args: Vec<InputValue>,
77 pub type_: TypeRef,
78 #[serde(rename = "isDeprecated")]
79 pub is_deprecated: bool,
80 pub deprecation_reason: Option<String>,
81}
82
83#[derive(Debug, Deserialize)]
84#[allow(dead_code)]
85pub struct InputValue {
86 pub name: String,
87 pub description: Option<String>,
88 pub type_: TypeRef,
89 pub default_value: Option<String>,
90}
91
92#[derive(Debug, Deserialize)]
93#[allow(dead_code)]
94pub struct EnumValue {
95 pub name: String,
96 pub description: Option<String>,
97 #[serde(rename = "isDeprecated")]
98 pub is_deprecated: bool,
99 pub deprecation_reason: Option<String>,
100}
101
102#[derive(Debug, Deserialize)]
103#[allow(dead_code)]
104pub struct Directive {
105 pub name: String,
106 pub description: Option<String>,
107 pub locations: Vec<DirectiveLocation>,
108 pub args: Vec<InputValue>,
109}
110
111#[derive(Debug, Deserialize)]
112#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
113pub enum DirectiveLocation {
114 Query,
115 Mutation,
116 Subscription,
117 Field,
118 FragmentDefinition,
119 FragmentSpread,
120 InlineFragment,
121 VariableDefinition,
122 Schema,
123 Scalar,
124 Object,
125 FieldDefinition,
126 ArgumentDefinition,
127 Interface,
128 Union,
129 Enum,
130 EnumValue,
131 InputObject,
132 InputFieldDefinition,
133}
134
135pub struct Introspector {
136 client: reqwest::Client,
137}
138
139#[allow(dead_code)]
140impl Default for Introspector {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146#[allow(dead_code)]
147impl Introspector {
148 pub fn new() -> Self {
149 Self {
150 client: reqwest::Client::new(),
151 }
152 }
153
154 pub async fn introspect_schema(
155 &self,
156 url: &str,
157 headers: &HashMap<String, String>,
158 ) -> anyhow::Result<Schema> {
159 let introspection_query = r#"
160 query IntrospectionQuery {
161 __schema {
162 queryType { name }
163 mutationType { name }
164 subscriptionType { name }
165 types {
166 ...FullType
167 }
168 directives {
169 name
170 description
171 locations
172 args {
173 ...InputValue
174 }
175 }
176 }
177 }
178
179 fragment FullType on __Type {
180 kind
181 name
182 description
183 fields(includeDeprecated: true) {
184 name
185 description
186 args {
187 ...InputValue
188 }
189 type {
190 ...TypeRef
191 }
192 isDeprecated
193 deprecationReason
194 }
195 inputFields {
196 ...InputValue
197 }
198 interfaces {
199 ...TypeRef
200 }
201 enumValues(includeDeprecated: true) {
202 name
203 description
204 isDeprecated
205 deprecationReason
206 }
207 possibleTypes {
208 ...TypeRef
209 }
210 }
211
212 fragment InputValue on __InputValue {
213 name
214 description
215 type {
216 ...TypeRef
217 }
218 defaultValue
219 }
220
221 fragment TypeRef on __Type {
222 kind
223 name
224 ofType {
225 kind
226 name
227 ofType {
228 kind
229 name
230 ofType {
231 kind
232 name
233 ofType {
234 kind
235 name
236 ofType {
237 kind
238 name
239 ofType {
240 kind
241 name
242 ofType {
243 kind
244 name
245 }
246 }
247 }
248 }
249 }
250 }
251 }
252 }
253 "#;
254
255 let query = IntrospectionQuery {
256 query: introspection_query.to_string(),
257 };
258
259 let mut request = self.client.post(url).json(&query);
260
261 for (key, value) in headers {
263 let header_name = HeaderName::from_bytes(key.as_bytes())?;
264 let header_value = HeaderValue::from_str(value)?;
265 request = request.header(header_name, header_value);
266 }
267
268 let response = request.send().await?;
269 let status = response.status();
270
271 if !status.is_success() {
272 let status_code = status.as_u16();
273 let error_msg = match status_code {
274 400 => "Bad Request - The GraphQL query may be malformed",
275 401 => "Unauthorized - Authentication required. Check your headers",
276 403 => "Forbidden - Access denied. Verify your credentials and permissions",
277 404 => "Not Found - GraphQL endpoint not found at the specified URL",
278 500 => "Internal Server Error - The GraphQL server encountered an error",
279 _ => "HTTP request failed",
280 };
281
282 return Err(anyhow::anyhow!(
283 "GraphQL introspection failed with HTTP {}: {}\nURL: {}\n\nTroubleshooting:\n- Verify the URL is correct and accessible\n- Check authentication headers if required\n- Ensure the server supports GraphQL introspection",
284 status_code,
285 error_msg,
286 url
287 ));
288 }
289
290 let introspection_response: IntrospectionResponse = response.json().await?;
291
292 if let Some(errors) = introspection_response.errors {
293 let error_messages: Vec<String> = errors.into_iter().map(|e| e.message).collect();
294 let error_count = error_messages.len();
295
296 let mut error_text = format!(
297 "GraphQL introspection failed with {} error{}:\n",
298 error_count,
299 if error_count == 1 { "" } else { "s" }
300 );
301
302 for (i, message) in error_messages.iter().enumerate() {
303 error_text.push_str(&format!("{}. {}\n", i + 1, message));
304 }
305
306 error_text.push_str("\nCommon causes:\n");
307 error_text.push_str("- Introspection is disabled on the GraphQL server\n");
308 error_text.push_str("- Authentication or authorization issues\n");
309 error_text.push_str("- Server-side GraphQL schema errors\n");
310 error_text.push_str("- Network connectivity problems\n");
311
312 return Err(anyhow::anyhow!(error_text));
313 }
314
315 let schema = introspection_response
316 .data
317 .ok_or_else(|| {
318 anyhow::anyhow!(
319 "No data returned from GraphQL introspection\n\nThis typically indicates:\n- The GraphQL endpoint returned an empty response\n- The server may not support the introspection query\n- Network issues prevented a complete response\n\nTry:\n- Checking if the endpoint supports GraphQL introspection\n- Verifying network connectivity\n- Testing with a simple GraphQL query first"
320 )
321 })?
322 .schema;
323
324 Ok(schema)
325 }
326
327 fn object_type_to_sdl(&self, type_def: &Type) -> String {
328 let mut sdl = String::new();
329
330 if let Some(description) = &type_def.description {
331 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
332 }
333
334 let name = type_def.name.as_ref().unwrap();
335 sdl.push_str(&format!("type {} ", name));
336
337 if let Some(interfaces) = &type_def.interfaces {
339 if !interfaces.is_empty() {
340 let interface_names: Vec<String> =
341 interfaces.iter().filter_map(|i| i.name.clone()).collect();
342 sdl.push_str(&format!("implements {} ", interface_names.join(" & ")));
343 }
344 }
345
346 sdl.push_str("{\n");
347
348 if let Some(fields) = &type_def.fields {
349 for field in fields {
350 if let Some(description) = &field.description {
351 sdl.push_str(&format!(" \"\"\"\n {}\n \"\"\"\n", description));
352 }
353 sdl.push_str(&format!(
354 " {}: {}\n",
355 field.name,
356 self.type_ref_to_sdl(&field.type_)
357 ));
358 }
359 }
360
361 sdl.push_str("}\n\n");
362 sdl
363 }
364
365 fn interface_type_to_sdl(&self, type_def: &Type) -> String {
366 let mut sdl = String::new();
367
368 if let Some(description) = &type_def.description {
369 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
370 }
371
372 let name = type_def.name.as_ref().unwrap();
373 sdl.push_str(&format!("interface {} {{\n", name));
374
375 if let Some(fields) = &type_def.fields {
376 for field in fields {
377 if let Some(description) = &field.description {
378 sdl.push_str(&format!(" \"\"\"\n {}\n \"\"\"\n", description));
379 }
380 sdl.push_str(&format!(
381 " {}: {}\n",
382 field.name,
383 self.type_ref_to_sdl(&field.type_)
384 ));
385 }
386 }
387
388 sdl.push_str("}\n\n");
389 sdl
390 }
391
392 fn enum_type_to_sdl(&self, type_def: &Type) -> String {
393 let mut sdl = String::new();
394
395 if let Some(description) = &type_def.description {
396 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
397 }
398
399 let name = type_def.name.as_ref().unwrap();
400 sdl.push_str(&format!("enum {} {{\n", name));
401
402 if let Some(values) = &type_def.enum_values {
403 for value in values {
404 if let Some(description) = &value.description {
405 sdl.push_str(&format!(" \"\"\"\n {}\n \"\"\"\n", description));
406 }
407 sdl.push_str(&format!(" {}\n", value.name));
408 }
409 }
410
411 sdl.push_str("}\n\n");
412 sdl
413 }
414
415 fn input_object_type_to_sdl(&self, type_def: &Type) -> String {
416 let mut sdl = String::new();
417
418 if let Some(description) = &type_def.description {
419 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
420 }
421
422 let name = type_def.name.as_ref().unwrap();
423 sdl.push_str(&format!("input {} {{\n", name));
424
425 if let Some(fields) = &type_def.input_fields {
426 for field in fields {
427 if let Some(description) = &field.description {
428 sdl.push_str(&format!(" \"\"\"\n {}\n \"\"\"\n", description));
429 }
430 let type_str = self.type_ref_to_sdl(&field.type_);
431 let default_value = field
432 .default_value
433 .as_ref()
434 .map(|v| format!(" = {}", v))
435 .unwrap_or_default();
436 sdl.push_str(&format!(
437 " {}: {}{}\n",
438 field.name, type_str, default_value
439 ));
440 }
441 }
442
443 sdl.push_str("}\n\n");
444 sdl
445 }
446
447 fn scalar_type_to_sdl(&self, type_def: &Type) -> String {
448 let mut sdl = String::new();
449
450 if let Some(description) = &type_def.description {
451 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
452 }
453
454 let name = type_def.name.as_ref().unwrap();
455 sdl.push_str(&format!("scalar {}\n\n", name));
456 sdl
457 }
458
459 fn union_type_to_sdl(&self, type_def: &Type) -> String {
460 let mut sdl = String::new();
461
462 if let Some(description) = &type_def.description {
463 sdl.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", description));
464 }
465
466 let name = type_def.name.as_ref().unwrap();
467 sdl.push_str(&format!("union {} = ", name));
468
469 if let Some(possible_types) = &type_def.possible_types {
470 let type_names: Vec<String> = possible_types
471 .iter()
472 .filter_map(|t| t.name.clone())
473 .collect();
474 sdl.push_str(&type_names.join(" | "));
475 }
476
477 sdl.push_str("\n\n");
478 sdl
479 }
480
481 #[allow(clippy::only_used_in_recursion)]
482 fn type_ref_to_sdl(&self, type_ref: &TypeRef) -> String {
483 let mut result = String::new();
484
485 match type_ref.kind {
487 Some(TypeKind::NonNull) => {
488 if let Some(of_type) = &type_ref.of_type {
489 result.push_str(&self.type_ref_to_sdl(of_type));
490 result.push('!');
491 }
492 }
493 Some(TypeKind::List) => {
494 if let Some(of_type) = &type_ref.of_type {
495 result.push('[');
496 result.push_str(&self.type_ref_to_sdl(of_type));
497 result.push(']');
498 }
499 }
500 _ => {
501 if let Some(name) = &type_ref.name {
502 result.push_str(name);
503 }
504 }
505 }
506
507 result
508 }
509
510 pub fn schema_to_sdl(&self, schema: &Schema) -> String {
512 let mut sdl = String::new();
513
514 sdl.push_str("schema {\n");
516 if let Some(query) = &schema.query_type {
517 if let Some(name) = &query.name {
518 sdl.push_str(&format!(" query: {}\n", name));
519 }
520 }
521 if let Some(mutation) = &schema.mutation_type {
522 if let Some(name) = &mutation.name {
523 sdl.push_str(&format!(" mutation: {}\n", name));
524 }
525 }
526 if let Some(subscription) = &schema.subscription_type {
527 if let Some(name) = &subscription.name {
528 sdl.push_str(&format!(" subscription: {}\n", name));
529 }
530 }
531 sdl.push_str("}\n\n");
532
533 for type_def in &schema.types {
535 if let Some(name) = &type_def.name {
536 if name.starts_with("__") {
538 continue;
539 }
540
541 match type_def.kind {
542 TypeKind::Object => {
543 sdl.push_str(&self.object_type_to_sdl(type_def));
544 }
545 TypeKind::Interface => {
546 sdl.push_str(&self.interface_type_to_sdl(type_def));
547 }
548 TypeKind::Enum => {
549 sdl.push_str(&self.enum_type_to_sdl(type_def));
550 }
551 TypeKind::InputObject => {
552 sdl.push_str(&self.input_object_type_to_sdl(type_def));
553 }
554 TypeKind::Scalar => {
555 sdl.push_str(&self.scalar_type_to_sdl(type_def));
556 }
557 TypeKind::Union => {
558 sdl.push_str(&self.union_type_to_sdl(type_def));
559 }
560 _ => {} }
562 }
563 }
564
565 sdl
566 }
567}