prax_schema/ast/
graphql.rs

1//! GraphQL-specific AST types and configuration.
2//!
3//! This module provides types for configuring GraphQL behavior directly in the schema,
4//! allowing fine-grained control over how models and fields are exposed in GraphQL APIs.
5//!
6//! # Schema Syntax
7//!
8//! ```prax
9//! model User {
10//!     /// @graphql.skip - Hide from GraphQL
11//!     internal_id   String
12//!
13//!     /// @graphql.name("userId") - Custom GraphQL name
14//!     id            Int    @id @auto
15//!
16//!     /// @graphql.complexity(5) - Query complexity
17//!     posts         Post[]
18//!
19//!     /// @graphql.resolver("customEmailResolver")
20//!     email         String
21//! }
22//!
23//! /// @graphql.interface - Generate as GraphQL interface
24//! model Node {
25//!     id            String @id
26//! }
27//!
28//! /// @graphql.union(SearchResult) - Part of union type
29//! model Post {
30//!     id            Int    @id
31//! }
32//! ```
33
34use serde::{Deserialize, Serialize};
35use smol_str::SmolStr;
36
37use super::Span;
38
39/// GraphQL-specific configuration for a model or type.
40#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
41pub struct GraphQLConfig {
42    /// Custom GraphQL type name (if different from model name).
43    pub name: Option<String>,
44    /// Description override for GraphQL.
45    pub description: Option<String>,
46    /// Whether to skip this type entirely in GraphQL.
47    pub skip: bool,
48    /// Generate as a GraphQL interface.
49    pub is_interface: bool,
50    /// Union types this model belongs to.
51    pub union_types: Vec<String>,
52    /// Implements interfaces.
53    pub implements: Vec<String>,
54    /// Directives to apply.
55    pub directives: Vec<GraphQLDirective>,
56    /// Query complexity for this type.
57    pub complexity: Option<u32>,
58    /// Guard/authorization expression.
59    pub guard: Option<String>,
60    /// Whether to generate input types.
61    pub generate_input: bool,
62    /// Whether to generate filter types.
63    pub generate_filter: bool,
64    /// Whether to generate ordering types.
65    pub generate_order: bool,
66    /// Custom resolver module.
67    pub resolver: Option<String>,
68}
69
70impl GraphQLConfig {
71    /// Create a new GraphQL config with defaults.
72    pub fn new() -> Self {
73        Self {
74            generate_input: true,
75            generate_filter: true,
76            generate_order: true,
77            ..Default::default()
78        }
79    }
80
81    /// Set the custom GraphQL name.
82    pub fn with_name(mut self, name: impl Into<String>) -> Self {
83        self.name = Some(name.into());
84        self
85    }
86
87    /// Mark as skipped in GraphQL.
88    pub fn skip(mut self) -> Self {
89        self.skip = true;
90        self
91    }
92
93    /// Mark as GraphQL interface.
94    pub fn as_interface(mut self) -> Self {
95        self.is_interface = true;
96        self
97    }
98
99    /// Add to a union type.
100    pub fn in_union(mut self, union_name: impl Into<String>) -> Self {
101        self.union_types.push(union_name.into());
102        self
103    }
104
105    /// Implement an interface.
106    pub fn implements(mut self, interface: impl Into<String>) -> Self {
107        self.implements.push(interface.into());
108        self
109    }
110
111    /// Set complexity.
112    pub fn with_complexity(mut self, complexity: u32) -> Self {
113        self.complexity = Some(complexity);
114        self
115    }
116}
117
118/// GraphQL-specific configuration for a field.
119#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
120pub struct GraphQLFieldConfig {
121    /// Custom GraphQL field name.
122    pub name: Option<String>,
123    /// Description override.
124    pub description: Option<String>,
125    /// Whether to skip this field in GraphQL.
126    pub skip: bool,
127    /// Deprecation reason.
128    pub deprecation: Option<String>,
129    /// Query complexity for this field.
130    pub complexity: Option<ComplexityConfig>,
131    /// Guard/authorization expression.
132    pub guard: Option<String>,
133    /// Custom resolver function.
134    pub resolver: Option<String>,
135    /// Directives to apply.
136    pub directives: Vec<GraphQLDirective>,
137    /// Whether this field is derived/computed.
138    pub derived: bool,
139    /// Shareable across subgraphs (federation).
140    pub shareable: bool,
141    /// External field (federation).
142    pub external: bool,
143    /// Provides fields (federation).
144    pub provides: Option<String>,
145    /// Requires fields (federation).
146    pub requires: Option<String>,
147    /// Field is used as entity key (federation).
148    pub key: bool,
149}
150
151impl GraphQLFieldConfig {
152    /// Create new field config with defaults.
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    /// Set custom name.
158    pub fn with_name(mut self, name: impl Into<String>) -> Self {
159        self.name = Some(name.into());
160        self
161    }
162
163    /// Mark as skipped.
164    pub fn skip(mut self) -> Self {
165        self.skip = true;
166        self
167    }
168
169    /// Set deprecation reason.
170    pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
171        self.deprecation = Some(reason.into());
172        self
173    }
174
175    /// Set as derived field.
176    pub fn derived(mut self) -> Self {
177        self.derived = true;
178        self
179    }
180}
181
182/// Query complexity configuration.
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub enum ComplexityConfig {
185    /// Fixed complexity value.
186    Fixed(u32),
187    /// Complexity based on arguments (multiplier).
188    Multiplier {
189        /// Base complexity.
190        base: u32,
191        /// Argument to multiply by.
192        argument: String,
193    },
194    /// Custom complexity function.
195    Custom(String),
196}
197
198impl Default for ComplexityConfig {
199    fn default() -> Self {
200        Self::Fixed(1)
201    }
202}
203
204/// A GraphQL directive with arguments.
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct GraphQLDirective {
207    /// Directive name (without @).
208    pub name: SmolStr,
209    /// Directive arguments.
210    pub arguments: Vec<GraphQLArgument>,
211    /// Source location.
212    pub span: Span,
213}
214
215impl GraphQLDirective {
216    /// Create a new directive.
217    pub fn new(name: impl Into<SmolStr>, span: Span) -> Self {
218        Self {
219            name: name.into(),
220            arguments: Vec::new(),
221            span,
222        }
223    }
224
225    /// Add an argument.
226    pub fn with_arg(mut self, name: impl Into<SmolStr>, value: GraphQLValue) -> Self {
227        self.arguments.push(GraphQLArgument {
228            name: name.into(),
229            value,
230        });
231        self
232    }
233
234    /// Format as SDL directive.
235    pub fn to_sdl(&self) -> String {
236        if self.arguments.is_empty() {
237            format!("@{}", self.name)
238        } else {
239            let args: Vec<String> = self
240                .arguments
241                .iter()
242                .map(|a| format!("{}: {}", a.name, a.value.to_sdl()))
243                .collect();
244            format!("@{}({})", self.name, args.join(", "))
245        }
246    }
247}
248
249/// A GraphQL directive argument.
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub struct GraphQLArgument {
252    /// Argument name.
253    pub name: SmolStr,
254    /// Argument value.
255    pub value: GraphQLValue,
256}
257
258/// A GraphQL value for directive arguments.
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
260pub enum GraphQLValue {
261    /// String value.
262    String(String),
263    /// Integer value.
264    Int(i64),
265    /// Float value.
266    Float(f64),
267    /// Boolean value.
268    Boolean(bool),
269    /// Enum value (unquoted).
270    Enum(String),
271    /// List of values.
272    List(Vec<GraphQLValue>),
273    /// Object value.
274    Object(Vec<(String, GraphQLValue)>),
275    /// Null value.
276    Null,
277}
278
279impl GraphQLValue {
280    /// Format as SDL value.
281    pub fn to_sdl(&self) -> String {
282        match self {
283            Self::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
284            Self::Int(i) => i.to_string(),
285            Self::Float(f) => f.to_string(),
286            Self::Boolean(b) => b.to_string(),
287            Self::Enum(e) => e.clone(),
288            Self::List(items) => {
289                let vals: Vec<String> = items.iter().map(|v| v.to_sdl()).collect();
290                format!("[{}]", vals.join(", "))
291            }
292            Self::Object(fields) => {
293                let field_strs: Vec<String> = fields
294                    .iter()
295                    .map(|(k, v)| format!("{}: {}", k, v.to_sdl()))
296                    .collect();
297                format!("{{{}}}", field_strs.join(", "))
298            }
299            Self::Null => "null".to_string(),
300        }
301    }
302}
303
304/// GraphQL subscription configuration.
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306pub struct SubscriptionConfig {
307    /// Enable subscriptions for this model.
308    pub enabled: bool,
309    /// Subscribe to create events.
310    pub on_create: bool,
311    /// Subscribe to update events.
312    pub on_update: bool,
313    /// Subscribe to delete events.
314    pub on_delete: bool,
315    /// Custom subscription filter.
316    pub filter: Option<String>,
317}
318
319impl SubscriptionConfig {
320    /// Enable all subscription events.
321    pub fn all() -> Self {
322        Self {
323            enabled: true,
324            on_create: true,
325            on_update: true,
326            on_delete: true,
327            filter: None,
328        }
329    }
330
331    /// Enable only specific events.
332    pub fn only_create() -> Self {
333        Self {
334            enabled: true,
335            on_create: true,
336            ..Default::default()
337        }
338    }
339}
340
341/// Federation 2.0 configuration for a model.
342#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
343pub struct FederationConfig {
344    /// This type is an entity (has @key).
345    pub is_entity: bool,
346    /// Key fields for entity resolution.
347    pub keys: Vec<FederationKey>,
348    /// This type is shareable.
349    pub shareable: bool,
350    /// This type is external (defined in another subgraph).
351    pub external: bool,
352    /// This type extends another type.
353    pub extends: bool,
354    /// Interface object (federation 2.3+).
355    pub interface_object: bool,
356}
357
358/// Federation entity key configuration.
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub struct FederationKey {
361    /// Fields that make up the key.
362    pub fields: String,
363    /// Whether this key is resolvable.
364    pub resolvable: bool,
365}
366
367impl FederationKey {
368    /// Create a new federation key.
369    pub fn new(fields: impl Into<String>) -> Self {
370        Self {
371            fields: fields.into(),
372            resolvable: true,
373        }
374    }
375
376    /// Mark as non-resolvable.
377    pub fn non_resolvable(mut self) -> Self {
378        self.resolvable = false;
379        self
380    }
381
382    /// Format as SDL directive.
383    pub fn to_sdl(&self) -> String {
384        if self.resolvable {
385            format!("@key(fields: \"{}\")", self.fields)
386        } else {
387            format!("@key(fields: \"{}\", resolvable: false)", self.fields)
388        }
389    }
390}
391
392/// Parse GraphQL configuration from doc tags.
393pub fn parse_graphql_config_from_tags(tags: &[super::validation::DocTag]) -> GraphQLConfig {
394    let mut config = GraphQLConfig::new();
395
396    for tag in tags {
397        match tag.name.as_str() {
398            "graphql.skip" | "graphql_skip" => config.skip = true,
399            "graphql.name" | "graphql_name" => config.name = tag.value.clone(),
400            "graphql.interface" | "graphql_interface" => config.is_interface = true,
401            "graphql.union" | "graphql_union" => {
402                if let Some(v) = &tag.value {
403                    config.union_types.push(v.clone());
404                }
405            }
406            "graphql.implements" | "graphql_implements" => {
407                if let Some(v) = &tag.value {
408                    config.implements.push(v.clone());
409                }
410            }
411            "graphql.complexity" | "graphql_complexity" => {
412                if let Some(v) = &tag.value {
413                    config.complexity = v.parse().ok();
414                }
415            }
416            "graphql.guard" | "graphql_guard" => config.guard = tag.value.clone(),
417            "graphql.resolver" | "graphql_resolver" => config.resolver = tag.value.clone(),
418            "graphql.no_input" | "graphql_no_input" => config.generate_input = false,
419            "graphql.no_filter" | "graphql_no_filter" => config.generate_filter = false,
420            "graphql.no_order" | "graphql_no_order" => config.generate_order = false,
421            _ => {}
422        }
423    }
424
425    config
426}
427
428/// Parse field GraphQL configuration from doc tags.
429pub fn parse_graphql_field_config_from_tags(
430    tags: &[super::validation::DocTag],
431) -> GraphQLFieldConfig {
432    let mut config = GraphQLFieldConfig::new();
433
434    for tag in tags {
435        match tag.name.as_str() {
436            "graphql.skip" | "graphql_skip" => config.skip = true,
437            "graphql.name" | "graphql_name" => config.name = tag.value.clone(),
438            "graphql.deprecation" | "graphql_deprecation" | "deprecated" => {
439                config.deprecation = tag.value.clone().or(Some(String::new()));
440            }
441            "graphql.guard" | "graphql_guard" => config.guard = tag.value.clone(),
442            "graphql.resolver" | "graphql_resolver" => config.resolver = tag.value.clone(),
443            "graphql.derived" | "graphql_derived" => config.derived = true,
444            "graphql.shareable" | "graphql_shareable" => config.shareable = true,
445            "graphql.external" | "graphql_external" => config.external = true,
446            "graphql.provides" | "graphql_provides" => config.provides = tag.value.clone(),
447            "graphql.requires" | "graphql_requires" => config.requires = tag.value.clone(),
448            "graphql.key" | "graphql_key" => config.key = true,
449            _ => {}
450        }
451    }
452
453    config
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_graphql_config_builder() {
462        let config = GraphQLConfig::new()
463            .with_name("CustomUser")
464            .as_interface()
465            .in_union("SearchResult")
466            .implements("Node")
467            .with_complexity(10);
468
469        assert_eq!(config.name, Some("CustomUser".to_string()));
470        assert!(config.is_interface);
471        assert_eq!(config.union_types, vec!["SearchResult"]);
472        assert_eq!(config.implements, vec!["Node"]);
473        assert_eq!(config.complexity, Some(10));
474    }
475
476    #[test]
477    fn test_graphql_field_config() {
478        let config = GraphQLFieldConfig::new()
479            .with_name("userId")
480            .deprecated("Use newId instead")
481            .derived();
482
483        assert_eq!(config.name, Some("userId".to_string()));
484        assert_eq!(config.deprecation, Some("Use newId instead".to_string()));
485        assert!(config.derived);
486    }
487
488    #[test]
489    fn test_graphql_directive_sdl() {
490        let directive = GraphQLDirective::new("deprecated", Span::new(0, 0))
491            .with_arg("reason", GraphQLValue::String("Use newField".to_string()));
492
493        assert_eq!(directive.to_sdl(), "@deprecated(reason: \"Use newField\")");
494
495        let simple_directive = GraphQLDirective::new("shareable", Span::new(0, 0));
496        assert_eq!(simple_directive.to_sdl(), "@shareable");
497    }
498
499    #[test]
500    fn test_graphql_value_sdl() {
501        assert_eq!(
502            GraphQLValue::String("hello".to_string()).to_sdl(),
503            "\"hello\""
504        );
505        assert_eq!(GraphQLValue::Int(42).to_sdl(), "42");
506        assert_eq!(GraphQLValue::Float(3.14).to_sdl(), "3.14");
507        assert_eq!(GraphQLValue::Boolean(true).to_sdl(), "true");
508        assert_eq!(GraphQLValue::Enum("ADMIN".to_string()).to_sdl(), "ADMIN");
509        assert_eq!(GraphQLValue::Null.to_sdl(), "null");
510
511        let list = GraphQLValue::List(vec![
512            GraphQLValue::Int(1),
513            GraphQLValue::Int(2),
514            GraphQLValue::Int(3),
515        ]);
516        assert_eq!(list.to_sdl(), "[1, 2, 3]");
517
518        let obj = GraphQLValue::Object(vec![
519            ("name".to_string(), GraphQLValue::String("test".to_string())),
520            ("count".to_string(), GraphQLValue::Int(5)),
521        ]);
522        assert_eq!(obj.to_sdl(), "{name: \"test\", count: 5}");
523    }
524
525    #[test]
526    fn test_federation_key_sdl() {
527        let key = FederationKey::new("id");
528        assert_eq!(key.to_sdl(), "@key(fields: \"id\")");
529
530        let composite_key = FederationKey::new("userId organizationId");
531        assert_eq!(
532            composite_key.to_sdl(),
533            "@key(fields: \"userId organizationId\")"
534        );
535
536        let non_resolvable = FederationKey::new("id").non_resolvable();
537        assert_eq!(
538            non_resolvable.to_sdl(),
539            "@key(fields: \"id\", resolvable: false)"
540        );
541    }
542
543    #[test]
544    fn test_subscription_config() {
545        let all = SubscriptionConfig::all();
546        assert!(all.enabled);
547        assert!(all.on_create);
548        assert!(all.on_update);
549        assert!(all.on_delete);
550
551        let only_create = SubscriptionConfig::only_create();
552        assert!(only_create.enabled);
553        assert!(only_create.on_create);
554        assert!(!only_create.on_update);
555        assert!(!only_create.on_delete);
556    }
557
558    #[test]
559    fn test_parse_graphql_config_from_tags() {
560        use super::super::validation::DocTag;
561
562        let span = Span::new(0, 0);
563        let tags = vec![
564            DocTag::new("graphql.name", Some("CustomModel".to_string()), span),
565            DocTag::new("graphql.interface", None, span),
566            DocTag::new("graphql.complexity", Some("5".to_string()), span),
567        ];
568
569        let config = parse_graphql_config_from_tags(&tags);
570
571        assert_eq!(config.name, Some("CustomModel".to_string()));
572        assert!(config.is_interface);
573        assert_eq!(config.complexity, Some(5));
574    }
575
576    #[test]
577    fn test_parse_graphql_field_config_from_tags() {
578        use super::super::validation::DocTag;
579
580        let span = Span::new(0, 0);
581        let tags = vec![
582            DocTag::new("graphql.name", Some("userId".to_string()), span),
583            DocTag::new("deprecated", Some("Use newId".to_string()), span),
584            DocTag::new("graphql.shareable", None, span),
585        ];
586
587        let config = parse_graphql_field_config_from_tags(&tags);
588
589        assert_eq!(config.name, Some("userId".to_string()));
590        assert_eq!(config.deprecation, Some("Use newId".to_string()));
591        assert!(config.shareable);
592    }
593}