Skip to main content

apollo_smith/
operation.rs

1use crate::description::Description;
2use crate::directive::Directive;
3use crate::directive::DirectiveLocation;
4use crate::name::Name;
5use crate::selection_set::Selection;
6use crate::selection_set::SelectionSet;
7use crate::variable::VariableDef;
8use crate::DocumentBuilder;
9use apollo_compiler::ast;
10use apollo_compiler::Node;
11use arbitrary::Arbitrary;
12use arbitrary::Result as ArbitraryResult;
13use indexmap::IndexMap;
14
15/// The __operationDef type represents an operation definition
16///
17/// *OperationDefinition*:
18///     Description? OperationType Name? VariablesDefinition? Directives? SelectionSet
19///
20/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#sec-Language.Operations).
21#[derive(Debug, Clone)]
22pub struct OperationDef {
23    pub(crate) description: Option<Description>,
24    pub(crate) operation_type: OperationType,
25    pub(crate) name: Option<Name>,
26    pub(crate) variable_definitions: Vec<VariableDef>,
27    pub(crate) directives: IndexMap<Name, Directive>,
28    pub(crate) selection_set: SelectionSet,
29}
30
31impl From<OperationDef> for ast::Definition {
32    fn from(x: OperationDef) -> Self {
33        ast::OperationDefinition {
34            description: x.description.map(Into::into),
35            operation_type: x.operation_type.into(),
36            name: x.name.map(Into::into),
37            directives: Directive::to_ast(x.directives),
38            variables: x
39                .variable_definitions
40                .into_iter()
41                .map(|x| Node::new(x.into()))
42                .collect(),
43            selection_set: x.selection_set.into(),
44        }
45        .into()
46    }
47}
48
49impl From<OperationDef> for String {
50    fn from(op_def: OperationDef) -> Self {
51        ast::Definition::from(op_def).to_string()
52    }
53}
54
55impl TryFrom<apollo_parser::cst::OperationDefinition> for OperationDef {
56    type Error = crate::FromError;
57
58    fn try_from(
59        operation_def: apollo_parser::cst::OperationDefinition,
60    ) -> Result<Self, Self::Error> {
61        Ok(Self {
62            description: operation_def.description().map(Description::from),
63            name: operation_def.name().map(Name::from),
64            directives: operation_def
65                .directives()
66                .map(Directive::convert_directives)
67                .transpose()?
68                .unwrap_or_default(),
69            operation_type: operation_def
70                .operation_type()
71                .map(OperationType::from)
72                .unwrap_or(OperationType::Query),
73            variable_definitions: Vec::new(),
74            selection_set: operation_def.selection_set().unwrap().try_into()?,
75        })
76    }
77}
78
79/// The __operationType type represents the kind of operation
80///
81/// *OperationType*:
82///     query | mutation | subscription
83///
84/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#OperationType).
85#[derive(Debug, Arbitrary, Clone, Copy, PartialEq, Eq)]
86pub enum OperationType {
87    Query,
88    Mutation,
89    Subscription,
90}
91
92impl From<OperationType> for ast::OperationType {
93    fn from(op_type: OperationType) -> Self {
94        match op_type {
95            OperationType::Query => Self::Query,
96            OperationType::Mutation => Self::Mutation,
97            OperationType::Subscription => Self::Subscription,
98        }
99    }
100}
101
102impl From<apollo_parser::cst::OperationType> for OperationType {
103    fn from(op_type: apollo_parser::cst::OperationType) -> Self {
104        if op_type.query_token().is_some() {
105            Self::Query
106        } else if op_type.mutation_token().is_some() {
107            Self::Mutation
108        } else if op_type.subscription_token().is_some() {
109            Self::Subscription
110        } else {
111            Self::Query
112        }
113    }
114}
115
116impl DocumentBuilder<'_> {
117    /// Create an arbitrary `OperationDef` taking the last `SchemaDef`.
118    pub fn operation_definition(&mut self) -> ArbitraryResult<Option<OperationDef>> {
119        self.operation_definition_in_document(false)
120    }
121
122    /// Like [`operation_definition`], but forces the operation to be
123    /// named when `require_named` is true. A document with more than
124    /// one operation may not contain an anonymous operation.
125    ///
126    /// Operation Name Uniqueness across the document is upheld by
127    /// `type_name`, which never returns a name that already exists on
128    /// the builder.
129    ///
130    /// See <https://spec.graphql.org/September2025/#sec-Lone-Anonymous-Operation>
131    /// and <https://spec.graphql.org/September2025/#sec-Operation-Name-Uniqueness>.
132    ///
133    /// [`operation_definition`]: Self::operation_definition
134    pub(crate) fn operation_definition_in_document(
135        &mut self,
136        require_named: bool,
137    ) -> ArbitraryResult<Option<OperationDef>> {
138        let schema = match self.schema_def.clone() {
139            Some(schema_def) => schema_def,
140            None => return Ok(None),
141        };
142
143        let want_name = require_named || self.u.arbitrary().unwrap_or(false);
144        let name = if want_name {
145            Some(self.type_name()?)
146        } else {
147            None
148        };
149
150        let available_operations = {
151            let mut ops = vec![];
152            if let Some(query) = &schema.query {
153                ops.push((OperationType::Query, query));
154            }
155            if let Some(mutation) = &schema.mutation {
156                ops.push((OperationType::Mutation, mutation));
157            }
158            if let Some(subscription) = &schema.subscription {
159                ops.push((OperationType::Subscription, subscription));
160            }
161
162            ops
163        };
164
165        let (operation_type, chosen_ty) = self.u.choose(&available_operations)?;
166        let directive_location = match operation_type {
167            OperationType::Query => DirectiveLocation::Query,
168            OperationType::Mutation => DirectiveLocation::Mutation,
169            OperationType::Subscription => DirectiveLocation::Subscription,
170        };
171        let directives = self.directives(directive_location)?;
172
173        // Stack
174        self.stack_ty(chosen_ty);
175
176        let selection_set = if matches!(operation_type, OperationType::Subscription) {
177            // Subscription operations must have exactly one root field
178            // per <https://spec.graphql.org/September2025/#sec-Single-Root-Field>.
179            // We generate exactly one field to satisfy that condition. The 0-index
180            // is sometimes used to create an alias for the field.
181            SelectionSet {
182                selections: vec![Selection::Field(self.field(0)?)],
183            }
184        } else {
185            self.selection_set()?
186        };
187
188        self.stack.pop();
189        assert!(
190            self.stack.is_empty(),
191            "the stack must be empty at the end of an operation definition"
192        );
193
194        // TODO
195        let variable_definitions = vec![];
196
197        Ok(Some(OperationDef {
198            description: None,
199            operation_type: *operation_type,
200            name,
201            variable_definitions,
202            directives,
203            selection_set,
204        }))
205    }
206}