graphql_codegen_rust/
introspection.rs

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        // Add custom headers
262        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        // Add interfaces
338        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        // Handle NonNull and List wrappers
486        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    /// Convert introspection schema to SDL string
511    pub fn schema_to_sdl(&self, schema: &Schema) -> String {
512        let mut sdl = String::new();
513
514        // Add schema definition
515        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        // Add types
534        for type_def in &schema.types {
535            if let Some(name) = &type_def.name {
536                // Skip introspection types
537                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                    _ => {} // Skip List, NonNull as they're handled in type refs
561                }
562            }
563        }
564
565        sdl
566    }
567}