Skip to main content

wesley_core/adapters/
apollo.rs

1//! Apollo Parser implementation of the LoweringPort.
2
3use crate::domain::error::WesleyError;
4use crate::domain::ir::*;
5use crate::domain::operation::{
6    OperationArgument, OperationDirectiveArgs, OperationType, SchemaOperation,
7};
8use crate::domain::optic::{
9    CodecField, CodecShape, DirectiveRecord, EvidenceKind, Footprint, IdentityRequirement,
10    LawClaimTemplate, OperationKind, OpticAdmissionRequirements, OpticArtifact, OpticOperation,
11    OpticRegistrationDescriptor, PermissionAction, PermissionRequirement, RootArgumentBinding,
12    SelectionArgumentBinding,
13};
14use crate::domain::schema_delta::{diff_schema_ir, SchemaDelta};
15use crate::ports::lowering::LoweringPort;
16use apollo_parser::{cst, Parser};
17use async_trait::async_trait;
18use indexmap::IndexMap;
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20
21/// Adapter that uses `apollo-parser` to lower SDL to IR.
22pub struct ApolloLoweringAdapter {
23    _max_retries: usize,
24}
25
26impl ApolloLoweringAdapter {
27    /// Creates a new adapter.
28    pub fn new(max_retries: usize) -> Self {
29        Self {
30            _max_retries: max_retries,
31        }
32    }
33}
34
35#[async_trait]
36impl LoweringPort for ApolloLoweringAdapter {
37    async fn lower_sdl(&self, sdl: &str) -> Result<WesleyIR, WesleyError> {
38        self.parse_and_lower(sdl)
39    }
40}
41
42/// Lowers GraphQL SDL into the Wesley L1 IR using the Apollo parser adapter.
43pub fn lower_schema_sdl(sdl: &str) -> Result<WesleyIR, WesleyError> {
44    ApolloLoweringAdapter::new(0).parse_and_lower(sdl)
45}
46
47/// Computes the structural L1 delta between two GraphQL SDL documents.
48pub fn diff_schema_sdl(old_sdl: &str, new_sdl: &str) -> Result<SchemaDelta, WesleyError> {
49    let adapter = ApolloLoweringAdapter::new(0);
50    let old_ir = adapter.parse_and_lower(old_sdl)?;
51    let new_ir = adapter.parse_and_lower(new_sdl)?;
52
53    Ok(diff_schema_ir(&old_ir, &new_ir))
54}
55
56/// Lists schema root operations from GraphQL SDL.
57pub fn list_schema_operations_sdl(schema_sdl: &str) -> Result<Vec<SchemaOperation>, WesleyError> {
58    let parser = Parser::new(schema_sdl);
59    let cst = parser.parse();
60
61    let errors = cst.errors().collect::<Vec<_>>();
62    if !errors.is_empty() {
63        let err = &errors[0];
64        return Err(WesleyError::ParseError {
65            message: err.message().to_string(),
66            line: None,
67            column: None,
68        });
69    }
70
71    let doc = cst.document();
72    let mut root_types = RootTypes::default();
73    for def in doc.definitions() {
74        match def {
75            cst::Definition::SchemaDefinition(schema) => {
76                update_root_types(schema.root_operation_type_definitions(), &mut root_types)?;
77            }
78            cst::Definition::SchemaExtension(schema) => {
79                update_root_types(schema.root_operation_type_definitions(), &mut root_types)?;
80            }
81            _ => {}
82        }
83    }
84
85    let mut operations = Vec::new();
86    for def in doc.definitions() {
87        match def {
88            cst::Definition::ObjectTypeDefinition(node) => {
89                collect_schema_operations_from_object(
90                    node.name(),
91                    node.fields_definition(),
92                    &root_types,
93                    &mut operations,
94                )?;
95            }
96            cst::Definition::ObjectTypeExtension(node) => {
97                collect_schema_operations_from_object(
98                    node.name(),
99                    node.fields_definition(),
100                    &root_types,
101                    &mut operations,
102                )?;
103            }
104            _ => {}
105        }
106    }
107
108    Ok(operations)
109}
110
111/// Represents the consolidated parts of a single GraphQL Type.
112struct TypeAggregate {
113    name: String,
114    kind: TypeKind,
115    definitions: Vec<TypeDefinitionNode>,
116    extensions: Vec<TypeExtensionNode>,
117}
118
119enum TypeDefinitionNode {
120    Scalar(cst::ScalarTypeDefinition),
121    Object(cst::ObjectTypeDefinition),
122    Interface(cst::InterfaceTypeDefinition),
123    Union(cst::UnionTypeDefinition),
124    Enum(cst::EnumTypeDefinition),
125    InputObject(cst::InputObjectTypeDefinition),
126}
127
128impl TypeDefinitionNode {
129    fn name(&self) -> Option<cst::Name> {
130        match self {
131            TypeDefinitionNode::Scalar(node) => node.name(),
132            TypeDefinitionNode::Object(node) => node.name(),
133            TypeDefinitionNode::Interface(node) => node.name(),
134            TypeDefinitionNode::Union(node) => node.name(),
135            TypeDefinitionNode::Enum(node) => node.name(),
136            TypeDefinitionNode::InputObject(node) => node.name(),
137        }
138    }
139
140    fn description(&self) -> Option<cst::Description> {
141        match self {
142            TypeDefinitionNode::Scalar(node) => node.description(),
143            TypeDefinitionNode::Object(node) => node.description(),
144            TypeDefinitionNode::Interface(node) => node.description(),
145            TypeDefinitionNode::Union(node) => node.description(),
146            TypeDefinitionNode::Enum(node) => node.description(),
147            TypeDefinitionNode::InputObject(node) => node.description(),
148        }
149    }
150}
151
152enum TypeExtensionNode {
153    Scalar(cst::ScalarTypeExtension),
154    Object(cst::ObjectTypeExtension),
155    Interface(cst::InterfaceTypeExtension),
156    Union(cst::UnionTypeExtension),
157    Enum(cst::EnumTypeExtension),
158    InputObject(cst::InputObjectTypeExtension),
159}
160
161impl TypeExtensionNode {
162    fn name(&self) -> Option<cst::Name> {
163        match self {
164            TypeExtensionNode::Scalar(node) => node.name(),
165            TypeExtensionNode::Object(node) => node.name(),
166            TypeExtensionNode::Interface(node) => node.name(),
167            TypeExtensionNode::Union(node) => node.name(),
168            TypeExtensionNode::Enum(node) => node.name(),
169            TypeExtensionNode::InputObject(node) => node.name(),
170        }
171    }
172}
173
174impl ApolloLoweringAdapter {
175    fn parse_and_lower(&self, sdl: &str) -> Result<WesleyIR, WesleyError> {
176        let parser = Parser::new(sdl);
177        let cst = parser.parse();
178
179        let errors = cst.errors().collect::<Vec<_>>();
180        if !errors.is_empty() {
181            let err = &errors[0];
182            return Err(WesleyError::ParseError {
183                message: err.message().to_string(),
184                line: None,
185                column: None,
186            });
187        }
188
189        let doc = cst.document();
190        let mut aggregates: BTreeMap<String, TypeAggregate> = BTreeMap::new();
191
192        for def in doc.definitions() {
193            match def {
194                cst::Definition::ScalarTypeDefinition(node) => self.aggregate_definition(
195                    TypeDefinitionNode::Scalar(node),
196                    TypeKind::Scalar,
197                    &mut aggregates,
198                )?,
199                cst::Definition::ObjectTypeDefinition(node) => self.aggregate_definition(
200                    TypeDefinitionNode::Object(node),
201                    TypeKind::Object,
202                    &mut aggregates,
203                )?,
204                cst::Definition::InterfaceTypeDefinition(node) => self.aggregate_definition(
205                    TypeDefinitionNode::Interface(node),
206                    TypeKind::Interface,
207                    &mut aggregates,
208                )?,
209                cst::Definition::UnionTypeDefinition(node) => self.aggregate_definition(
210                    TypeDefinitionNode::Union(node),
211                    TypeKind::Union,
212                    &mut aggregates,
213                )?,
214                cst::Definition::EnumTypeDefinition(node) => self.aggregate_definition(
215                    TypeDefinitionNode::Enum(node),
216                    TypeKind::Enum,
217                    &mut aggregates,
218                )?,
219                cst::Definition::InputObjectTypeDefinition(node) => self.aggregate_definition(
220                    TypeDefinitionNode::InputObject(node),
221                    TypeKind::InputObject,
222                    &mut aggregates,
223                )?,
224                cst::Definition::ScalarTypeExtension(node) => self.aggregate_extension(
225                    TypeExtensionNode::Scalar(node),
226                    TypeKind::Scalar,
227                    &mut aggregates,
228                )?,
229                cst::Definition::ObjectTypeExtension(node) => self.aggregate_extension(
230                    TypeExtensionNode::Object(node),
231                    TypeKind::Object,
232                    &mut aggregates,
233                )?,
234                cst::Definition::InterfaceTypeExtension(node) => self.aggregate_extension(
235                    TypeExtensionNode::Interface(node),
236                    TypeKind::Interface,
237                    &mut aggregates,
238                )?,
239                cst::Definition::UnionTypeExtension(node) => self.aggregate_extension(
240                    TypeExtensionNode::Union(node),
241                    TypeKind::Union,
242                    &mut aggregates,
243                )?,
244                cst::Definition::EnumTypeExtension(node) => self.aggregate_extension(
245                    TypeExtensionNode::Enum(node),
246                    TypeKind::Enum,
247                    &mut aggregates,
248                )?,
249                cst::Definition::InputObjectTypeExtension(node) => self.aggregate_extension(
250                    TypeExtensionNode::InputObject(node),
251                    TypeKind::InputObject,
252                    &mut aggregates,
253                )?,
254                _ => {}
255            }
256        }
257
258        let mut types = Vec::new();
259        for agg in aggregates.values() {
260            types.push(self.build_type_from_aggregate(agg)?);
261        }
262
263        Ok(WesleyIR {
264            version: "1.0.0".to_string(),
265            metadata: None,
266            types,
267        })
268    }
269
270    fn aggregate_definition(
271        &self,
272        node: TypeDefinitionNode,
273        kind: TypeKind,
274        aggregates: &mut BTreeMap<String, TypeAggregate>,
275    ) -> Result<(), WesleyError> {
276        let name = type_node_name(node.name(), "Type definition missing name")?;
277        let agg = aggregate_for(aggregates, name, kind)?;
278        agg.definitions.push(node);
279        Ok(())
280    }
281
282    fn aggregate_extension(
283        &self,
284        node: TypeExtensionNode,
285        kind: TypeKind,
286        aggregates: &mut BTreeMap<String, TypeAggregate>,
287    ) -> Result<(), WesleyError> {
288        let name = type_node_name(node.name(), "Type extension missing name")?;
289        let agg = aggregate_for(aggregates, name, kind)?;
290        agg.extensions.push(node);
291        Ok(())
292    }
293
294    fn build_type_from_aggregate(
295        &self,
296        agg: &TypeAggregate,
297    ) -> Result<TypeDefinition, WesleyError> {
298        let mut directives = IndexMap::new();
299        let mut implements = Vec::new();
300        let mut fields = Vec::new();
301        let mut enum_values = Vec::new();
302        let mut union_members = Vec::new();
303        let mut description = None;
304
305        for def in &agg.definitions {
306            if description.is_none() {
307                description = description_from(def.description());
308            }
309            self.merge_definition(
310                def,
311                &mut directives,
312                &mut implements,
313                &mut fields,
314                &mut enum_values,
315                &mut union_members,
316            )?;
317        }
318
319        for ext in &agg.extensions {
320            self.merge_extension(
321                ext,
322                &mut directives,
323                &mut implements,
324                &mut fields,
325                &mut enum_values,
326                &mut union_members,
327            )?;
328        }
329
330        Ok(TypeDefinition {
331            name: agg.name.clone(),
332            kind: agg.kind,
333            description,
334            directives,
335            implements,
336            fields,
337            enum_values,
338            union_members,
339        })
340    }
341
342    fn merge_definition(
343        &self,
344        def: &TypeDefinitionNode,
345        directives: &mut IndexMap<String, serde_json::Value>,
346        implements: &mut Vec<String>,
347        fields: &mut Vec<Field>,
348        enum_values: &mut Vec<String>,
349        union_members: &mut Vec<String>,
350    ) -> Result<(), WesleyError> {
351        match def {
352            TypeDefinitionNode::Scalar(node) => {
353                if let Some(dirs) = node.directives() {
354                    self.extract_directives(dirs, directives)?;
355                }
356            }
357            TypeDefinitionNode::Object(node) => {
358                if let Some(interfaces) = node.implements_interfaces() {
359                    collect_implements(interfaces, implements)?;
360                }
361                if let Some(dirs) = node.directives() {
362                    self.extract_directives(dirs, directives)?;
363                }
364                if let Some(fields_def) = node.fields_definition() {
365                    self.collect_fields(fields_def, fields)?;
366                }
367            }
368            TypeDefinitionNode::Interface(node) => {
369                if let Some(interfaces) = node.implements_interfaces() {
370                    collect_implements(interfaces, implements)?;
371                }
372                if let Some(dirs) = node.directives() {
373                    self.extract_directives(dirs, directives)?;
374                }
375                if let Some(fields_def) = node.fields_definition() {
376                    self.collect_fields(fields_def, fields)?;
377                }
378            }
379            TypeDefinitionNode::Union(node) => {
380                if let Some(dirs) = node.directives() {
381                    self.extract_directives(dirs, directives)?;
382                }
383                if let Some(member_types) = node.union_member_types() {
384                    collect_union_members(member_types, union_members)?;
385                }
386            }
387            TypeDefinitionNode::Enum(node) => {
388                if let Some(dirs) = node.directives() {
389                    self.extract_directives(dirs, directives)?;
390                }
391                if let Some(values_def) = node.enum_values_definition() {
392                    collect_enum_values(values_def, enum_values)?;
393                }
394            }
395            TypeDefinitionNode::InputObject(node) => {
396                if let Some(dirs) = node.directives() {
397                    self.extract_directives(dirs, directives)?;
398                }
399                if let Some(fields_def) = node.input_fields_definition() {
400                    self.collect_input_fields(fields_def, fields)?;
401                }
402            }
403        }
404
405        Ok(())
406    }
407
408    fn merge_extension(
409        &self,
410        ext: &TypeExtensionNode,
411        directives: &mut IndexMap<String, serde_json::Value>,
412        implements: &mut Vec<String>,
413        fields: &mut Vec<Field>,
414        enum_values: &mut Vec<String>,
415        union_members: &mut Vec<String>,
416    ) -> Result<(), WesleyError> {
417        match ext {
418            TypeExtensionNode::Scalar(node) => {
419                if let Some(dirs) = node.directives() {
420                    self.extract_directives(dirs, directives)?;
421                }
422            }
423            TypeExtensionNode::Object(node) => {
424                if let Some(interfaces) = node.implements_interfaces() {
425                    collect_implements(interfaces, implements)?;
426                }
427                if let Some(dirs) = node.directives() {
428                    self.extract_directives(dirs, directives)?;
429                }
430                if let Some(fields_def) = node.fields_definition() {
431                    self.collect_fields(fields_def, fields)?;
432                }
433            }
434            TypeExtensionNode::Interface(node) => {
435                if let Some(interfaces) = node.implements_interfaces() {
436                    collect_implements(interfaces, implements)?;
437                }
438                if let Some(dirs) = node.directives() {
439                    self.extract_directives(dirs, directives)?;
440                }
441                if let Some(fields_def) = node.fields_definition() {
442                    self.collect_fields(fields_def, fields)?;
443                }
444            }
445            TypeExtensionNode::Union(node) => {
446                if let Some(dirs) = node.directives() {
447                    self.extract_directives(dirs, directives)?;
448                }
449                if let Some(member_types) = node.union_member_types() {
450                    collect_union_members(member_types, union_members)?;
451                }
452            }
453            TypeExtensionNode::Enum(node) => {
454                if let Some(dirs) = node.directives() {
455                    self.extract_directives(dirs, directives)?;
456                }
457                if let Some(values_def) = node.enum_values_definition() {
458                    collect_enum_values(values_def, enum_values)?;
459                }
460            }
461            TypeExtensionNode::InputObject(node) => {
462                if let Some(dirs) = node.directives() {
463                    self.extract_directives(dirs, directives)?;
464                }
465                if let Some(fields_def) = node.input_fields_definition() {
466                    self.collect_input_fields(fields_def, fields)?;
467                }
468            }
469        }
470
471        Ok(())
472    }
473
474    fn extract_directives(
475        &self,
476        dirs: cst::Directives,
477        map: &mut IndexMap<String, serde_json::Value>,
478    ) -> Result<(), WesleyError> {
479        for dir in dirs.directives() {
480            let dir_name = dir
481                .name()
482                .ok_or(WesleyError::LoweringError {
483                    message: "Directive missing name".to_string(),
484                    area: "directive".to_string(),
485                })?
486                .text()
487                .to_string();
488
489            let mut args_map = serde_json::Map::new();
490            if let Some(args) = dir.arguments() {
491                for arg in args.arguments() {
492                    let arg_name = arg.name().map(|n| n.text().to_string()).unwrap_or_default();
493                    if let Some(val) = arg.value() {
494                        args_map.insert(arg_name, directive_value_to_json(val)?);
495                    }
496                }
497            }
498
499            let val = if args_map.is_empty() {
500                serde_json::Value::Bool(true)
501            } else {
502                serde_json::Value::Object(args_map)
503            };
504
505            map.insert(dir_name, val);
506        }
507        Ok(())
508    }
509
510    fn collect_fields(
511        &self,
512        fields_def: cst::FieldsDefinition,
513        fields: &mut Vec<Field>,
514    ) -> Result<(), WesleyError> {
515        for field_def in fields_def.field_definitions() {
516            fields.push(self.build_field(field_def)?);
517        }
518
519        Ok(())
520    }
521
522    fn collect_input_fields(
523        &self,
524        fields_def: cst::InputFieldsDefinition,
525        fields: &mut Vec<Field>,
526    ) -> Result<(), WesleyError> {
527        for field_def in fields_def.input_value_definitions() {
528            fields.push(self.build_input_field(field_def)?);
529        }
530
531        Ok(())
532    }
533
534    fn build_field(&self, field_def: cst::FieldDefinition) -> Result<Field, WesleyError> {
535        let name = field_def
536            .name()
537            .ok_or(WesleyError::LoweringError {
538                message: "Field missing name".to_string(),
539                area: "field".to_string(),
540            })?
541            .text()
542            .to_string();
543
544        let type_node = field_def.ty().ok_or(WesleyError::LoweringError {
545            message: "Field missing type".to_string(),
546            area: "field".to_string(),
547        })?;
548
549        let mut field_directives = IndexMap::new();
550        if let Some(dirs) = field_def.directives() {
551            self.extract_directives(dirs, &mut field_directives)?;
552        }
553
554        Ok(Field {
555            name,
556            r#type: self.build_type_reference(type_node)?,
557            arguments: field_arguments_from_definition(field_def.arguments_definition())?,
558            default_value: None,
559            directives: field_directives,
560            description: description_from(field_def.description()),
561        })
562    }
563
564    fn build_input_field(
565        &self,
566        field_def: cst::InputValueDefinition,
567    ) -> Result<Field, WesleyError> {
568        let name = field_def
569            .name()
570            .ok_or(WesleyError::LoweringError {
571                message: "Input field missing name".to_string(),
572                area: "field".to_string(),
573            })?
574            .text()
575            .to_string();
576
577        let type_node = field_def.ty().ok_or(WesleyError::LoweringError {
578            message: "Input field missing type".to_string(),
579            area: "field".to_string(),
580        })?;
581
582        let mut field_directives = IndexMap::new();
583        if let Some(dirs) = field_def.directives() {
584            self.extract_directives(dirs, &mut field_directives)?;
585        }
586        let default_value = field_def
587            .default_value()
588            .and_then(|default_value| default_value.value())
589            .map(directive_value_to_json)
590            .transpose()?;
591
592        Ok(Field {
593            name,
594            r#type: self.build_type_reference(type_node)?,
595            arguments: Vec::new(),
596            default_value,
597            directives: field_directives,
598            description: description_from(field_def.description()),
599        })
600    }
601
602    fn build_type_reference(&self, type_node: cst::Type) -> Result<TypeReference, WesleyError> {
603        type_reference_from_type(type_node, true)
604    }
605}
606
607fn aggregate_for(
608    aggregates: &mut BTreeMap<String, TypeAggregate>,
609    name: String,
610    kind: TypeKind,
611) -> Result<&mut TypeAggregate, WesleyError> {
612    use std::collections::btree_map::Entry;
613
614    match aggregates.entry(name.clone()) {
615        Entry::Vacant(entry) => Ok(entry.insert(TypeAggregate {
616            name,
617            kind,
618            definitions: Vec::new(),
619            extensions: Vec::new(),
620        })),
621        Entry::Occupied(entry) => {
622            let aggregate = entry.into_mut();
623            if aggregate.kind != kind {
624                return Err(lowering_error_value(
625                    "type",
626                    format!(
627                        "Type '{}' is declared as both {:?} and {:?}",
628                        aggregate.name, aggregate.kind, kind
629                    ),
630                ));
631            }
632            Ok(aggregate)
633        }
634    }
635}
636
637fn type_node_name(name: Option<cst::Name>, message: &str) -> Result<String, WesleyError> {
638    name.map(|name| name.text().to_string())
639        .ok_or_else(|| lowering_error_value("type", message.to_string()))
640}
641
642fn description_from(description: Option<cst::Description>) -> Option<String> {
643    description
644        .and_then(|description| description.string_value())
645        .map(String::from)
646}
647
648fn collect_implements(
649    interfaces: cst::ImplementsInterfaces,
650    implements: &mut Vec<String>,
651) -> Result<(), WesleyError> {
652    for named_type in interfaces.named_types() {
653        let name = named_type_name_for_lowering(named_type, "Implemented interface missing name")?;
654        push_unique(implements, name);
655    }
656
657    Ok(())
658}
659
660fn collect_union_members(
661    member_types: cst::UnionMemberTypes,
662    union_members: &mut Vec<String>,
663) -> Result<(), WesleyError> {
664    for named_type in member_types.named_types() {
665        let name = named_type_name_for_lowering(named_type, "Union member missing name")?;
666        push_unique(union_members, name);
667    }
668
669    Ok(())
670}
671
672fn collect_enum_values(
673    values_def: cst::EnumValuesDefinition,
674    enum_values: &mut Vec<String>,
675) -> Result<(), WesleyError> {
676    for value_def in values_def.enum_value_definitions() {
677        let name = value_def
678            .enum_value()
679            .and_then(|enum_value| enum_value.name())
680            .map(|name| name.text().to_string())
681            .ok_or_else(|| lowering_error_value("enum", "Enum value missing name".to_string()))?;
682        push_unique(enum_values, name);
683    }
684
685    Ok(())
686}
687
688fn named_type_name_for_lowering(
689    named_type: cst::NamedType,
690    message: &str,
691) -> Result<String, WesleyError> {
692    named_type
693        .name()
694        .map(|name| name.text().to_string())
695        .ok_or_else(|| lowering_error_value("type", message.to_string()))
696}
697
698#[derive(Debug)]
699struct TypeReferenceShape {
700    base: String,
701    nullable: bool,
702    list_wrappers: Vec<TypeListWrapper>,
703    leaf_nullable: bool,
704}
705
706fn type_reference_from_type(
707    type_node: cst::Type,
708    nullable: bool,
709) -> Result<TypeReference, WesleyError> {
710    let shape = type_reference_shape_from_type(type_node, nullable)?;
711    let is_list = !shape.list_wrappers.is_empty();
712    let list_item_nullable = if is_list {
713        Some(
714            shape
715                .list_wrappers
716                .get(1)
717                .map(|wrapper| wrapper.nullable)
718                .unwrap_or(shape.leaf_nullable),
719        )
720    } else {
721        None
722    };
723    let has_nested_lists = shape.list_wrappers.len() > 1;
724
725    Ok(TypeReference {
726        base: shape.base,
727        nullable: shape.nullable,
728        is_list,
729        list_item_nullable,
730        list_wrappers: if has_nested_lists {
731            shape.list_wrappers
732        } else {
733            Vec::new()
734        },
735        leaf_nullable: if has_nested_lists {
736            Some(shape.leaf_nullable)
737        } else {
738            None
739        },
740    })
741}
742
743fn type_reference_shape_from_type(
744    type_node: cst::Type,
745    nullable: bool,
746) -> Result<TypeReferenceShape, WesleyError> {
747    match type_node {
748        cst::Type::NamedType(named_type) => Ok(TypeReferenceShape {
749            base: named_type_name_for_lowering(named_type, "Type reference missing name")?,
750            nullable,
751            list_wrappers: Vec::new(),
752            leaf_nullable: nullable,
753        }),
754        cst::Type::ListType(list_type) => {
755            let item_type = list_type.ty().ok_or_else(|| {
756                lowering_error_value("type", "List type missing item type".to_string())
757            })?;
758            let item_ref = type_reference_shape_from_type(item_type, true)?;
759            let mut list_wrappers = vec![TypeListWrapper { nullable }];
760            list_wrappers.extend(item_ref.list_wrappers);
761
762            Ok(TypeReferenceShape {
763                base: item_ref.base,
764                nullable,
765                list_wrappers,
766                leaf_nullable: item_ref.leaf_nullable,
767            })
768        }
769        cst::Type::NonNullType(non_null_type) => {
770            if let Some(named_type) = non_null_type.named_type() {
771                Ok(TypeReferenceShape {
772                    base: named_type_name_for_lowering(
773                        named_type,
774                        "Non-null type reference missing name",
775                    )?,
776                    nullable: false,
777                    list_wrappers: Vec::new(),
778                    leaf_nullable: false,
779                })
780            } else if let Some(list_type) = non_null_type.list_type() {
781                let item_type = list_type.ty().ok_or_else(|| {
782                    lowering_error_value("type", "Non-null list type missing item type".to_string())
783                })?;
784                let item_ref = type_reference_shape_from_type(item_type, true)?;
785                let mut list_wrappers = vec![TypeListWrapper { nullable: false }];
786                list_wrappers.extend(item_ref.list_wrappers);
787
788                Ok(TypeReferenceShape {
789                    base: item_ref.base,
790                    nullable: false,
791                    list_wrappers,
792                    leaf_nullable: item_ref.leaf_nullable,
793                })
794            } else {
795                Err(lowering_error_value(
796                    "type",
797                    "Non-null type missing inner type".to_string(),
798                ))
799            }
800        }
801    }
802}
803
804fn field_arguments_from_definition(
805    arguments_definition: Option<cst::ArgumentsDefinition>,
806) -> Result<Vec<FieldArgument>, WesleyError> {
807    let Some(arguments_definition) = arguments_definition else {
808        return Ok(Vec::new());
809    };
810
811    arguments_definition
812        .input_value_definitions()
813        .map(field_argument_from_input_value)
814        .collect()
815}
816
817fn field_argument_from_input_value(
818    input_value: cst::InputValueDefinition,
819) -> Result<FieldArgument, WesleyError> {
820    let name = input_value
821        .name()
822        .map(|name| name.text().to_string())
823        .ok_or_else(|| {
824            lowering_error_value("field argument", "Field argument missing name".into())
825        })?;
826    let type_node = input_value.ty().ok_or_else(|| {
827        lowering_error_value(
828            "field argument",
829            format!("Field argument '{name}' missing type"),
830        )
831    })?;
832    let default_value = input_value
833        .default_value()
834        .and_then(|default_value| default_value.value())
835        .map(directive_value_to_json)
836        .transpose()?;
837
838    let mut directives = IndexMap::new();
839    if let Some(dirs) = input_value.directives() {
840        ApolloLoweringAdapter::new(0).extract_directives(dirs, &mut directives)?;
841    }
842
843    Ok(FieldArgument {
844        name,
845        description: description_from(input_value.description()),
846        r#type: type_reference_from_type(type_node, true)?,
847        default_value,
848        directives,
849    })
850}
851
852fn directive_value_to_json(value: cst::Value) -> Result<serde_json::Value, WesleyError> {
853    match value {
854        cst::Value::StringValue(value) => Ok(serde_json::Value::String(String::from(value))),
855        cst::Value::FloatValue(value) => {
856            let raw = value
857                .float_token()
858                .map(|token| token.text().to_string())
859                .unwrap_or_default();
860            let parsed = raw.parse::<f64>().map_err(|err| {
861                lowering_error_value(
862                    "directive",
863                    format!("Invalid float directive argument '{raw}': {err}"),
864                )
865            })?;
866            serde_json::Number::from_f64(parsed)
867                .map(serde_json::Value::Number)
868                .ok_or_else(|| {
869                    lowering_error_value(
870                        "directive",
871                        format!("Invalid finite float directive argument '{raw}'"),
872                    )
873                })
874        }
875        cst::Value::IntValue(value) => {
876            let raw = value
877                .int_token()
878                .map(|token| token.text().to_string())
879                .unwrap_or_default();
880            raw.parse::<i64>()
881                .map(|parsed| serde_json::Value::Number(parsed.into()))
882                .map_err(|err| {
883                    lowering_error_value(
884                        "directive",
885                        format!("Invalid integer directive argument '{raw}': {err}"),
886                    )
887                })
888        }
889        cst::Value::BooleanValue(value) => Ok(serde_json::Value::Bool(
890            value.true_token().is_some() && value.false_token().is_none(),
891        )),
892        cst::Value::NullValue(_) => Ok(serde_json::Value::Null),
893        cst::Value::EnumValue(value) => {
894            let name = value
895                .name()
896                .map(|name| name.text().to_string())
897                .ok_or_else(|| {
898                    lowering_error_value(
899                        "directive",
900                        "Enum directive value missing name".to_string(),
901                    )
902                })?;
903            Ok(serde_json::Value::String(name))
904        }
905        cst::Value::ListValue(list) => {
906            let mut values = Vec::new();
907            for value in list.values() {
908                values.push(directive_value_to_json(value)?);
909            }
910            Ok(serde_json::Value::Array(values))
911        }
912        cst::Value::ObjectValue(object) => {
913            let mut map = serde_json::Map::new();
914            for field in object.object_fields() {
915                let name = field
916                    .name()
917                    .map(|name| name.text().to_string())
918                    .ok_or_else(|| {
919                        lowering_error_value(
920                            "directive",
921                            "Object directive value field missing name".to_string(),
922                        )
923                    })?;
924                let value = field.value().ok_or_else(|| {
925                    lowering_error_value(
926                        "directive",
927                        format!("Object directive value field '{name}' missing value"),
928                    )
929                })?;
930                map.insert(name, directive_value_to_json(value)?);
931            }
932            Ok(serde_json::Value::Object(map))
933        }
934        cst::Value::Variable(variable) => Err(lowering_error_value(
935            "directive",
936            format!(
937                "Directive argument values cannot be variables: {}",
938                variable.text()
939            ),
940        )),
941    }
942}
943
944fn lowering_error_value(area: &str, message: String) -> WesleyError {
945    WesleyError::LoweringError {
946        message,
947        area: area.to_string(),
948    }
949}
950
951/// Resolves response-path field selections from a single GraphQL operation.
952pub fn resolve_operation_selections(operation_sdl: &str) -> Result<Vec<String>, WesleyError> {
953    let parsed = parse_operation_document(operation_sdl)?;
954    let op = parsed.only_operation()?;
955    let mut selections = Vec::new();
956
957    if let Some(selection_set) = op.selection_set() {
958        collect_selection_paths(
959            &selection_set,
960            "",
961            &parsed.fragments,
962            &mut Vec::new(),
963            &mut selections,
964        )?;
965    }
966
967    Ok(selections)
968}
969
970/// Resolves schema-coordinate field selections from a single GraphQL operation.
971pub fn resolve_operation_selections_with_schema(
972    schema_sdl: &str,
973    operation_sdl: &str,
974) -> Result<Vec<String>, WesleyError> {
975    let adapter = ApolloLoweringAdapter::new(0);
976    let ir = adapter.parse_and_lower(schema_sdl)?;
977    let root_types = extract_root_types(schema_sdl)?;
978
979    let parsed = parse_operation_document(operation_sdl)?;
980    let op = parsed.only_operation()?;
981    let mut selections = Vec::new();
982
983    if let Some(selection_set) = op.selection_set() {
984        let root_type = root_types.root_for_operation(op)?;
985        let schema = SchemaIndex::new(&ir);
986        collect_schema_coordinates(
987            &selection_set,
988            root_type,
989            &schema,
990            &parsed.fragments,
991            &mut Vec::new(),
992            &mut selections,
993        )?;
994    }
995
996    Ok(selections)
997}
998
999/// Compiles runtime-provided SDL plus one GraphQL operation into an optic artifact.
1000///
1001/// This is a compiler-only entry point. It validates and inspects the declared
1002/// operation shape, but it does not execute the operation, grant authority, or
1003/// verify runtime law satisfaction.
1004pub fn compile_runtime_optic(
1005    sdl: &str,
1006    operation_source: &str,
1007    selected_operation: Option<&str>,
1008) -> Result<OpticArtifact, WesleyError> {
1009    let adapter = ApolloLoweringAdapter::new(0);
1010    let ir = adapter.parse_and_lower(sdl)?;
1011    let schema_id = compute_registry_hash(&ir).map_err(|err| {
1012        lowering_error_value(
1013            "runtime optic",
1014            format!("Failed to compute schema identity: {err}"),
1015        )
1016    })?;
1017    let schema = SchemaIndex::new(&ir);
1018    reject_runtime_optic_unsupported_schema_features(&ir)?;
1019    let root_types = extract_root_types(sdl)?;
1020    let schema_operations = list_schema_operations_sdl(sdl)?;
1021
1022    let parsed = parse_operation_document(operation_source)?;
1023    let op = parsed.selected_operation(selected_operation)?;
1024    let kind = operation_kind(op)?;
1025    let root_type = root_types.root_for_operation(op)?;
1026    let root_field = selected_root_field(op)?;
1027    let root_field_name = required_name(root_field.name(), "Root field selection missing name")?;
1028    let schema_operation =
1029        schema_operation_for_selected_field(&schema_operations, kind, &root_field_name)?;
1030    reject_runtime_optic_variable_defaults(op)?;
1031    let variable_types = variable_definition_types(op)?;
1032    validate_runtime_optic_executable_selection(
1033        &root_field,
1034        root_type,
1035        &schema,
1036        &parsed.fragments,
1037    )?;
1038    let root_arguments =
1039        root_argument_bindings(&root_field, schema_operation, &variable_types, &schema)?;
1040    let selection_arguments = selection_argument_bindings(
1041        &root_field,
1042        &schema_operation.result_type,
1043        &schema,
1044        &parsed.fragments,
1045        &variable_types,
1046    )?;
1047
1048    let operation_name = op.name().map(|name| name.text().to_string());
1049    let directives =
1050        directive_records_for_operation(op, root_type, &root_field, &schema, &parsed.fragments)?;
1051    let root_coordinate = format!("{root_type}.{root_field_name}");
1052    let declared_footprint = footprint_from_directives(&directives, &root_coordinate)?;
1053    let variable_shape = variable_codec_shape(op, schema_operation)?;
1054    let payload_shape = payload_codec_shape(
1055        &root_field,
1056        &schema_operation.result_type,
1057        &schema,
1058        &parsed.fragments,
1059    )?;
1060
1061    let identity_seed = serde_json::json!({
1062        "kind": kind,
1063        "name": operation_name,
1064        "rootArguments": root_arguments,
1065        "rootField": root_field_name,
1066        "schemaId": schema_id,
1067        "selectionArguments": selection_arguments,
1068        "variableShape": variable_shape,
1069        "payloadShape": payload_shape,
1070        "directives": directives,
1071    });
1072    let operation_id = stable_json_hash(&identity_seed, "runtime optic operation identity")?;
1073    let law_claims =
1074        law_claims_for_operation(&operation_id, &directives, declared_footprint.as_ref())?;
1075    let requirements = admission_requirements_from_footprint(declared_footprint.as_ref());
1076    let requirements_digest = stable_json_hash(
1077        &serde_json::json!({
1078            "declaredFootprint": &declared_footprint,
1079            "lawClaims": &law_claims,
1080            "requirements": &requirements,
1081        }),
1082        "runtime optic requirements digest",
1083    )?;
1084    let artifact_hash = stable_json_hash(
1085        &serde_json::json!({
1086            "directives": &directives,
1087            "operationId": &operation_id,
1088            "payloadShape": &payload_shape,
1089            "requirementsDigest": &requirements_digest,
1090            "schemaId": &schema_id,
1091            "variableShape": &variable_shape,
1092        }),
1093        "runtime optic artifact hash",
1094    )?;
1095    let artifact_id = artifact_hash.clone();
1096    let registration = OpticRegistrationDescriptor {
1097        artifact_id: artifact_id.clone(),
1098        artifact_hash: artifact_hash.clone(),
1099        schema_id: schema_id.clone(),
1100        operation_id: operation_id.clone(),
1101        requirements_digest: requirements_digest.clone(),
1102    };
1103
1104    Ok(OpticArtifact {
1105        artifact_id,
1106        artifact_hash,
1107        schema_id,
1108        requirements_digest,
1109        operation: OpticOperation {
1110            operation_id,
1111            name: operation_name,
1112            kind,
1113            root_field: root_field_name,
1114            root_arguments,
1115            selection_arguments,
1116            variable_shape,
1117            payload_shape,
1118            directives,
1119            declared_footprint,
1120            law_claims,
1121        },
1122        requirements,
1123        registration,
1124    })
1125}
1126
1127/// Compiles runtime-provided SDL plus one GraphQL operation into a registration descriptor.
1128///
1129/// This returns the cross-process descriptor an application can present to Echo
1130/// when registering the full artifact. Echo returns the runtime-local opaque
1131/// `OpticArtifactHandle` after it accepts the artifact.
1132pub fn compile_runtime_optic_registration(
1133    sdl: &str,
1134    operation_source: &str,
1135    selected_operation: Option<&str>,
1136) -> Result<OpticRegistrationDescriptor, WesleyError> {
1137    Ok(compile_runtime_optic(sdl, operation_source, selected_operation)?.registration)
1138}
1139
1140/// Extracts arguments from operation directives with the requested directive name.
1141pub fn extract_operation_directive_args(
1142    operation_sdl: &str,
1143    directive_name: &str,
1144) -> Result<Vec<OperationDirectiveArgs>, WesleyError> {
1145    let parsed = parse_operation_document(operation_sdl)?;
1146    let op = parsed.only_operation()?;
1147    let mut directives = Vec::new();
1148
1149    let Some(operation_directives) = op.directives() else {
1150        return Ok(directives);
1151    };
1152
1153    for directive in operation_directives.directives() {
1154        let name = required_name(directive.name(), "Directive missing name")?;
1155        if name != directive_name {
1156            continue;
1157        }
1158
1159        directives.push(OperationDirectiveArgs {
1160            directive_name: name,
1161            arguments: extract_directive_arguments(directive.arguments())?,
1162        });
1163    }
1164
1165    Ok(directives)
1166}
1167
1168fn extract_directive_arguments(
1169    arguments: Option<cst::Arguments>,
1170) -> Result<IndexMap<String, serde_json::Value>, WesleyError> {
1171    let mut values = IndexMap::new();
1172
1173    let Some(arguments) = arguments else {
1174        return Ok(values);
1175    };
1176
1177    for argument in arguments.arguments() {
1178        let name = required_name(argument.name(), "Directive argument missing name")?;
1179        let value = argument.value().ok_or_else(|| {
1180            operation_error_value(format!("Directive argument '{name}' missing value"))
1181        })?;
1182        values.insert(name, directive_value_to_json(value)?);
1183    }
1184
1185    Ok(values)
1186}
1187
1188struct ParsedOperationDocument {
1189    operations: Vec<cst::OperationDefinition>,
1190    fragments: BTreeMap<String, cst::FragmentDefinition>,
1191}
1192
1193impl ParsedOperationDocument {
1194    fn only_operation(&self) -> Result<&cst::OperationDefinition, WesleyError> {
1195        match self.operations.len() {
1196            0 => operation_error("No GraphQL operation found".to_string()),
1197            1 => Ok(&self.operations[0]),
1198            count => operation_error(format!(
1199                "Expected exactly one GraphQL operation, found {count}"
1200            )),
1201        }
1202    }
1203
1204    fn selected_operation(
1205        &self,
1206        selected_operation: Option<&str>,
1207    ) -> Result<&cst::OperationDefinition, WesleyError> {
1208        let Some(selected_operation) = selected_operation else {
1209            return self.only_operation();
1210        };
1211
1212        self.operations
1213            .iter()
1214            .find(|operation| {
1215                operation
1216                    .name()
1217                    .map(|name| name.text() == selected_operation)
1218                    .unwrap_or(false)
1219            })
1220            .ok_or_else(|| {
1221                operation_error_value(format!(
1222                    "Selected GraphQL operation '{selected_operation}' not found"
1223                ))
1224            })
1225    }
1226}
1227
1228fn parse_operation_document(operation_sdl: &str) -> Result<ParsedOperationDocument, WesleyError> {
1229    let parser = Parser::new(operation_sdl);
1230    let cst = parser.parse();
1231
1232    let errors = cst.errors().collect::<Vec<_>>();
1233    if !errors.is_empty() {
1234        let err = &errors[0];
1235        return Err(WesleyError::ParseError {
1236            message: err.message().to_string(),
1237            line: None,
1238            column: None,
1239        });
1240    }
1241
1242    let doc = cst.document();
1243    let mut operations = Vec::new();
1244    let mut fragments = BTreeMap::new();
1245
1246    for def in doc.definitions() {
1247        match def {
1248            cst::Definition::OperationDefinition(op) => {
1249                operations.push(op);
1250            }
1251            cst::Definition::FragmentDefinition(fragment) => {
1252                let name = fragment_name(&fragment)?;
1253                if fragments.insert(name.clone(), fragment).is_some() {
1254                    return operation_error(format!("Duplicate fragment definition '{name}'"));
1255                }
1256            }
1257            _ => {}
1258        }
1259    }
1260
1261    Ok(ParsedOperationDocument {
1262        operations,
1263        fragments,
1264    })
1265}
1266
1267fn collect_selection_paths(
1268    selection_set: &cst::SelectionSet,
1269    prefix: &str,
1270    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1271    active_fragments: &mut Vec<String>,
1272    actual_selections: &mut Vec<String>,
1273) -> Result<(), WesleyError> {
1274    for selection in selection_set.selections() {
1275        match selection {
1276            cst::Selection::Field(field) => {
1277                let field_name = required_name(field.name(), "Field selection missing name")?;
1278                let path = if prefix.is_empty() {
1279                    field_name
1280                } else {
1281                    format!("{prefix}.{field_name}")
1282                };
1283
1284                push_unique(actual_selections, path.clone());
1285
1286                if let Some(nested_selection_set) = field.selection_set() {
1287                    collect_selection_paths(
1288                        &nested_selection_set,
1289                        &path,
1290                        fragments,
1291                        active_fragments,
1292                        actual_selections,
1293                    )?;
1294                }
1295            }
1296            cst::Selection::FragmentSpread(spread) => {
1297                let name = spread
1298                    .fragment_name()
1299                    .and_then(|fragment_name| fragment_name.name())
1300                    .map(|name| name.text().to_string())
1301                    .ok_or_else(|| {
1302                        operation_error_value("Fragment spread missing name".to_string())
1303                    })?;
1304
1305                if active_fragments.contains(&name) {
1306                    return operation_error(format!(
1307                        "Cyclic fragment spread detected for fragment '{name}'"
1308                    ));
1309                }
1310
1311                let fragment = fragments.get(&name).ok_or_else(|| {
1312                    operation_error_value(format!("Unknown fragment spread '{name}'"))
1313                })?;
1314
1315                active_fragments.push(name);
1316                if let Some(fragment_selection_set) = fragment.selection_set() {
1317                    collect_selection_paths(
1318                        &fragment_selection_set,
1319                        prefix,
1320                        fragments,
1321                        active_fragments,
1322                        actual_selections,
1323                    )?;
1324                }
1325                active_fragments.pop();
1326            }
1327            cst::Selection::InlineFragment(fragment) => {
1328                if let Some(inline_selection_set) = fragment.selection_set() {
1329                    collect_selection_paths(
1330                        &inline_selection_set,
1331                        prefix,
1332                        fragments,
1333                        active_fragments,
1334                        actual_selections,
1335                    )?;
1336                }
1337            }
1338        }
1339    }
1340
1341    Ok(())
1342}
1343
1344fn collect_schema_coordinates(
1345    selection_set: &cst::SelectionSet,
1346    parent_type: &str,
1347    schema: &SchemaIndex<'_>,
1348    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1349    active_fragments: &mut Vec<String>,
1350    actual_selections: &mut Vec<String>,
1351) -> Result<(), WesleyError> {
1352    for selection in selection_set.selections() {
1353        match selection {
1354            cst::Selection::Field(field) => {
1355                let field_name = required_name(field.name(), "Field selection missing name")?;
1356                let schema_field = schema.field(parent_type, &field_name)?;
1357                let coordinate = format!("{parent_type}.{field_name}");
1358                push_unique(actual_selections, coordinate);
1359
1360                if let Some(nested_selection_set) = field.selection_set() {
1361                    let nested_parent = schema_field.r#type.base.as_str();
1362                    schema.require_type(nested_parent)?;
1363                    collect_schema_coordinates(
1364                        &nested_selection_set,
1365                        nested_parent,
1366                        schema,
1367                        fragments,
1368                        active_fragments,
1369                        actual_selections,
1370                    )?;
1371                }
1372            }
1373            cst::Selection::FragmentSpread(spread) => {
1374                let name = spread
1375                    .fragment_name()
1376                    .and_then(|fragment_name| fragment_name.name())
1377                    .map(|name| name.text().to_string())
1378                    .ok_or_else(|| {
1379                        operation_error_value("Fragment spread missing name".to_string())
1380                    })?;
1381
1382                if active_fragments.contains(&name) {
1383                    return operation_error(format!(
1384                        "Cyclic fragment spread detected for fragment '{name}'"
1385                    ));
1386                }
1387
1388                let fragment = fragments.get(&name).ok_or_else(|| {
1389                    operation_error_value(format!("Unknown fragment spread '{name}'"))
1390                })?;
1391                let fragment_parent = fragment_type_condition(fragment)?;
1392                validate_fragment_type_condition(parent_type, &fragment_parent, schema, &name)?;
1393
1394                active_fragments.push(name);
1395                if let Some(fragment_selection_set) = fragment.selection_set() {
1396                    collect_schema_coordinates(
1397                        &fragment_selection_set,
1398                        &fragment_parent,
1399                        schema,
1400                        fragments,
1401                        active_fragments,
1402                        actual_selections,
1403                    )?;
1404                }
1405                active_fragments.pop();
1406            }
1407            cst::Selection::InlineFragment(fragment) => {
1408                let inline_parent = if let Some(type_condition) = fragment.type_condition() {
1409                    named_type_name(type_condition.named_type(), "Inline fragment missing type")?
1410                } else {
1411                    parent_type.to_string()
1412                };
1413                validate_fragment_type_condition(parent_type, &inline_parent, schema, "inline")?;
1414
1415                if let Some(inline_selection_set) = fragment.selection_set() {
1416                    collect_schema_coordinates(
1417                        &inline_selection_set,
1418                        &inline_parent,
1419                        schema,
1420                        fragments,
1421                        active_fragments,
1422                        actual_selections,
1423                    )?;
1424                }
1425            }
1426        }
1427    }
1428
1429    Ok(())
1430}
1431
1432fn operation_kind(op: &cst::OperationDefinition) -> Result<OperationKind, WesleyError> {
1433    let Some(operation_type) = op.operation_type() else {
1434        return Ok(OperationKind::Query);
1435    };
1436
1437    if operation_type.query_token().is_some() {
1438        Ok(OperationKind::Query)
1439    } else if operation_type.mutation_token().is_some() {
1440        Ok(OperationKind::Mutation)
1441    } else if operation_type.subscription_token().is_some() {
1442        Ok(OperationKind::Subscription)
1443    } else {
1444        operation_error("Unknown GraphQL operation type".to_string())
1445    }
1446}
1447
1448fn selected_root_field(op: &cst::OperationDefinition) -> Result<cst::Field, WesleyError> {
1449    let selection_set = op.selection_set().ok_or_else(|| {
1450        operation_error_value("Runtime optic operation missing selection set".to_string())
1451    })?;
1452    let mut fields = Vec::new();
1453
1454    for selection in selection_set.selections() {
1455        match selection {
1456            cst::Selection::Field(field) => fields.push(field),
1457            cst::Selection::FragmentSpread(_) | cst::Selection::InlineFragment(_) => {
1458                return operation_error(
1459                    "Runtime optic v0 requires a concrete top-level field selection".to_string(),
1460                );
1461            }
1462        }
1463    }
1464
1465    match fields.len() {
1466        0 => operation_error("Runtime optic operation selects no root field".to_string()),
1467        1 => Ok(fields.remove(0)),
1468        count => operation_error(format!(
1469            "Runtime optic v0 expects exactly one root field selection, found {count}"
1470        )),
1471    }
1472}
1473
1474fn schema_operation_for_selected_field<'a>(
1475    schema_operations: &'a [SchemaOperation],
1476    kind: OperationKind,
1477    root_field_name: &str,
1478) -> Result<&'a SchemaOperation, WesleyError> {
1479    let operation_type = OperationType::from(kind);
1480    schema_operations
1481        .iter()
1482        .find(|operation| {
1483            operation.operation_type == operation_type && operation.field_name == root_field_name
1484        })
1485        .ok_or_else(|| {
1486            operation_error_value(format!(
1487                "Schema root operation '{root_field_name}' not found for {kind:?}"
1488            ))
1489        })
1490}
1491
1492fn reject_runtime_optic_unsupported_schema_features(ir: &WesleyIR) -> Result<(), WesleyError> {
1493    for type_def in &ir.types {
1494        if type_def.kind == TypeKind::Interface && !type_def.implements.is_empty() {
1495            return operation_error(format!(
1496                "Runtime optic v0 does not support interface inheritance on '{}'",
1497                type_def.name
1498            ));
1499        }
1500    }
1501
1502    Ok(())
1503}
1504
1505fn reject_runtime_optic_variable_defaults(
1506    op: &cst::OperationDefinition,
1507) -> Result<(), WesleyError> {
1508    let Some(variable_definitions) = op.variable_definitions() else {
1509        return Ok(());
1510    };
1511
1512    for variable in variable_definitions.variable_definitions() {
1513        if variable.default_value().is_some() {
1514            let name = variable
1515                .variable()
1516                .and_then(|variable| variable.name())
1517                .map(|name| name.text().to_string())
1518                .unwrap_or_else(|| "<unknown>".to_string());
1519            return operation_error(format!(
1520                "Runtime optic v0 does not support default value for variable '${name}'"
1521            ));
1522        }
1523    }
1524
1525    Ok(())
1526}
1527
1528fn validate_runtime_optic_executable_selection(
1529    root_field: &cst::Field,
1530    root_type: &str,
1531    schema: &SchemaIndex<'_>,
1532    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1533) -> Result<(), WesleyError> {
1534    validate_runtime_optic_field_selection(
1535        root_field,
1536        root_type,
1537        schema,
1538        fragments,
1539        &mut Vec::new(),
1540    )
1541}
1542
1543fn validate_runtime_optic_selection_set(
1544    selection_set: &cst::SelectionSet,
1545    parent_type: &str,
1546    schema: &SchemaIndex<'_>,
1547    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1548    active_fragments: &mut Vec<String>,
1549) -> Result<(), WesleyError> {
1550    let mut response_signatures = BTreeMap::new();
1551    validate_runtime_optic_selection_set_into(
1552        selection_set,
1553        parent_type,
1554        schema,
1555        fragments,
1556        active_fragments,
1557        &mut response_signatures,
1558    )
1559}
1560
1561fn validate_runtime_optic_selection_set_into(
1562    selection_set: &cst::SelectionSet,
1563    parent_type: &str,
1564    schema: &SchemaIndex<'_>,
1565    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1566    active_fragments: &mut Vec<String>,
1567    response_signatures: &mut BTreeMap<String, RuntimeOpticFieldSignature>,
1568) -> Result<(), WesleyError> {
1569    for selection in selection_set.selections() {
1570        match selection {
1571            cst::Selection::Field(field) => {
1572                let (response_name, signature) =
1573                    runtime_optic_field_signature(&field, parent_type, schema)?;
1574                if let Some(existing) = response_signatures.get(&response_name) {
1575                    if existing != &signature {
1576                        return operation_error(format!(
1577                            "Runtime optic response name '{response_name}' has conflicting field selections"
1578                        ));
1579                    }
1580                } else {
1581                    response_signatures.insert(response_name, signature);
1582                }
1583
1584                validate_runtime_optic_field_selection(
1585                    &field,
1586                    parent_type,
1587                    schema,
1588                    fragments,
1589                    active_fragments,
1590                )?;
1591            }
1592            cst::Selection::FragmentSpread(spread) => {
1593                let name = spread
1594                    .fragment_name()
1595                    .and_then(|fragment_name| fragment_name.name())
1596                    .map(|name| name.text().to_string())
1597                    .ok_or_else(|| {
1598                        operation_error_value("Fragment spread missing name".to_string())
1599                    })?;
1600
1601                if active_fragments.contains(&name) {
1602                    return operation_error(format!(
1603                        "Cyclic fragment spread detected for fragment '{name}'"
1604                    ));
1605                }
1606
1607                let fragment = fragments.get(&name).ok_or_else(|| {
1608                    operation_error_value(format!("Unknown fragment spread '{name}'"))
1609                })?;
1610                let fragment_parent = fragment_type_condition(fragment)?;
1611                validate_fragment_type_condition(parent_type, &fragment_parent, schema, &name)?;
1612
1613                active_fragments.push(name);
1614                if let Some(fragment_selection_set) = fragment.selection_set() {
1615                    validate_runtime_optic_selection_set_into(
1616                        &fragment_selection_set,
1617                        &fragment_parent,
1618                        schema,
1619                        fragments,
1620                        active_fragments,
1621                        response_signatures,
1622                    )?;
1623                }
1624                active_fragments.pop();
1625            }
1626            cst::Selection::InlineFragment(fragment) => {
1627                let inline_parent = if let Some(type_condition) = fragment.type_condition() {
1628                    named_type_name(type_condition.named_type(), "Inline fragment missing type")?
1629                } else {
1630                    parent_type.to_string()
1631                };
1632                validate_fragment_type_condition(parent_type, &inline_parent, schema, "inline")?;
1633
1634                if let Some(inline_selection_set) = fragment.selection_set() {
1635                    validate_runtime_optic_selection_set_into(
1636                        &inline_selection_set,
1637                        &inline_parent,
1638                        schema,
1639                        fragments,
1640                        active_fragments,
1641                        response_signatures,
1642                    )?;
1643                }
1644            }
1645        }
1646    }
1647
1648    Ok(())
1649}
1650
1651fn validate_runtime_optic_field_selection(
1652    field: &cst::Field,
1653    parent_type: &str,
1654    schema: &SchemaIndex<'_>,
1655    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1656    active_fragments: &mut Vec<String>,
1657) -> Result<(), WesleyError> {
1658    let field_name = required_name(field.name(), "Field selection missing name")?;
1659    reject_runtime_optic_unsupported_field_name(&field_name)?;
1660    let schema_field = schema.field(parent_type, &field_name)?;
1661    let has_selection_set = field.selection_set().is_some();
1662    let is_composite = is_composite_output_type(&schema_field.r#type, schema)?;
1663
1664    match (is_composite, has_selection_set) {
1665        (true, false) => operation_error(format!(
1666            "Runtime optic field '{parent_type}.{field_name}' returns composite type '{}' and requires a subselection",
1667            schema_field.r#type.base
1668        )),
1669        (false, true) => operation_error(format!(
1670            "Runtime optic field '{parent_type}.{field_name}' returns leaf type '{}' and must not have a subselection",
1671            schema_field.r#type.base
1672        )),
1673        _ => {
1674            if let Some(selection_set) = field.selection_set() {
1675                validate_runtime_optic_selection_set(
1676                    &selection_set,
1677                    &schema_field.r#type.base,
1678                    schema,
1679                    fragments,
1680                    active_fragments,
1681                )?;
1682            }
1683            Ok(())
1684        }
1685    }
1686}
1687
1688#[derive(Clone, PartialEq)]
1689struct RuntimeOpticFieldSignature {
1690    parent_type: String,
1691    field_name: String,
1692    arguments_canonical_json: String,
1693    type_ref: TypeReference,
1694}
1695
1696fn runtime_optic_field_signature(
1697    field: &cst::Field,
1698    parent_type: &str,
1699    schema: &SchemaIndex<'_>,
1700) -> Result<(String, RuntimeOpticFieldSignature), WesleyError> {
1701    let field_name = required_name(field.name(), "Field selection missing name")?;
1702    reject_runtime_optic_unsupported_field_name(&field_name)?;
1703    let response_name = response_field_name(field)?;
1704    let schema_field = schema.field(parent_type, &field_name)?;
1705
1706    Ok((
1707        response_name,
1708        RuntimeOpticFieldSignature {
1709            parent_type: parent_type.to_string(),
1710            field_name,
1711            arguments_canonical_json: field_arguments_canonical_json(field.arguments())?,
1712            type_ref: schema_field.r#type.clone(),
1713        },
1714    ))
1715}
1716
1717fn field_arguments_canonical_json(
1718    arguments: Option<cst::Arguments>,
1719) -> Result<String, WesleyError> {
1720    let mut values = IndexMap::new();
1721
1722    if let Some(arguments) = arguments {
1723        for argument in arguments.arguments() {
1724            let name = required_name(argument.name(), "Field argument missing name")?;
1725            if values.contains_key(&name) {
1726                return operation_error(format!(
1727                    "Runtime optic field argument '{name}' is declared more than once"
1728                ));
1729            }
1730            let value = argument.value().ok_or_else(|| {
1731                operation_error_value(format!("Field argument '{name}' missing value"))
1732            })?;
1733            values.insert(name, executable_value_to_json(value)?);
1734        }
1735    }
1736
1737    stable_json_string(&values, "runtime optic field arguments")
1738}
1739
1740fn reject_runtime_optic_unsupported_field_name(field_name: &str) -> Result<(), WesleyError> {
1741    if field_name == "__typename" {
1742        return operation_error(
1743            "Runtime optic v0 does not support __typename selections".to_string(),
1744        );
1745    }
1746
1747    Ok(())
1748}
1749
1750fn is_composite_output_type(
1751    type_ref: &TypeReference,
1752    schema: &SchemaIndex<'_>,
1753) -> Result<bool, WesleyError> {
1754    match schema.type_kind(&type_ref.base) {
1755        Some(TypeKind::Object | TypeKind::Interface | TypeKind::Union) => Ok(true),
1756        Some(TypeKind::Enum | TypeKind::Scalar) => Ok(false),
1757        Some(TypeKind::InputObject) => operation_error(format!(
1758            "Runtime optic field references input object '{}' as an output type",
1759            type_ref.base
1760        )),
1761        None if is_builtin_scalar(&type_ref.base) => Ok(false),
1762        None => operation_error(format!(
1763            "Runtime optic field references unknown output type '{}'",
1764            type_ref.base
1765        )),
1766    }
1767}
1768
1769fn directive_records_for_operation(
1770    op: &cst::OperationDefinition,
1771    root_type: &str,
1772    root_field: &cst::Field,
1773    schema: &SchemaIndex<'_>,
1774    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1775) -> Result<Vec<DirectiveRecord>, WesleyError> {
1776    let operation_coordinate = op
1777        .name()
1778        .map(|name| format!("Operation.{}", name.text()))
1779        .unwrap_or_else(|| "Operation.<anonymous>".to_string());
1780    let mut records = Vec::new();
1781
1782    push_directive_records(&operation_coordinate, op.directives(), &mut records)?;
1783    collect_field_directive_records(
1784        root_field,
1785        root_type,
1786        schema,
1787        fragments,
1788        &mut Vec::new(),
1789        "",
1790        &mut records,
1791    )?;
1792
1793    Ok(records)
1794}
1795
1796fn collect_field_directive_records(
1797    field: &cst::Field,
1798    parent_type: &str,
1799    schema: &SchemaIndex<'_>,
1800    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1801    active_fragments: &mut Vec<String>,
1802    context_coordinate: &str,
1803    records: &mut Vec<DirectiveRecord>,
1804) -> Result<(), WesleyError> {
1805    let field_name = required_name(field.name(), "Field selection missing name")?;
1806    let schema_field = schema.field(parent_type, &field_name)?;
1807    let coordinate = format!("{parent_type}.{field_name}");
1808    push_directive_records(&coordinate, field.directives(), records)?;
1809
1810    if let Some(selection_set) = field.selection_set() {
1811        let nested_parent = schema_field.r#type.base.as_str();
1812        schema.require_type(nested_parent)?;
1813        collect_selection_directive_records(
1814            &selection_set,
1815            nested_parent,
1816            schema,
1817            fragments,
1818            active_fragments,
1819            &coordinate,
1820            records,
1821        )?;
1822    } else if !context_coordinate.is_empty() {
1823        let _ = context_coordinate;
1824    }
1825
1826    Ok(())
1827}
1828
1829fn collect_selection_directive_records(
1830    selection_set: &cst::SelectionSet,
1831    parent_type: &str,
1832    schema: &SchemaIndex<'_>,
1833    fragments: &BTreeMap<String, cst::FragmentDefinition>,
1834    active_fragments: &mut Vec<String>,
1835    context_coordinate: &str,
1836    records: &mut Vec<DirectiveRecord>,
1837) -> Result<(), WesleyError> {
1838    for selection in selection_set.selections() {
1839        match selection {
1840            cst::Selection::Field(field) => {
1841                collect_field_directive_records(
1842                    &field,
1843                    parent_type,
1844                    schema,
1845                    fragments,
1846                    active_fragments,
1847                    context_coordinate,
1848                    records,
1849                )?;
1850            }
1851            cst::Selection::FragmentSpread(spread) => {
1852                let name = spread
1853                    .fragment_name()
1854                    .and_then(|fragment_name| fragment_name.name())
1855                    .map(|name| name.text().to_string())
1856                    .ok_or_else(|| {
1857                        operation_error_value("Fragment spread missing name".to_string())
1858                    })?;
1859
1860                if active_fragments.contains(&name) {
1861                    return operation_error(format!(
1862                        "Cyclic fragment spread detected for fragment '{name}'"
1863                    ));
1864                }
1865
1866                let fragment = fragments.get(&name).ok_or_else(|| {
1867                    operation_error_value(format!("Unknown fragment spread '{name}'"))
1868                })?;
1869                let fragment_parent = fragment_type_condition(fragment)?;
1870                validate_fragment_type_condition(parent_type, &fragment_parent, schema, &name)?;
1871
1872                let spread_coordinate = if context_coordinate.is_empty() {
1873                    format!("{parent_type}...{name}")
1874                } else {
1875                    format!("{context_coordinate}...{name}")
1876                };
1877                push_directive_records(&spread_coordinate, spread.directives(), records)?;
1878                push_directive_records(
1879                    &format!("Fragment.{name}"),
1880                    fragment.directives(),
1881                    records,
1882                )?;
1883
1884                active_fragments.push(name);
1885                if let Some(fragment_selection_set) = fragment.selection_set() {
1886                    collect_selection_directive_records(
1887                        &fragment_selection_set,
1888                        &fragment_parent,
1889                        schema,
1890                        fragments,
1891                        active_fragments,
1892                        context_coordinate,
1893                        records,
1894                    )?;
1895                }
1896                active_fragments.pop();
1897            }
1898            cst::Selection::InlineFragment(fragment) => {
1899                let inline_parent = if let Some(type_condition) = fragment.type_condition() {
1900                    named_type_name(type_condition.named_type(), "Inline fragment missing type")?
1901                } else {
1902                    parent_type.to_string()
1903                };
1904                validate_fragment_type_condition(parent_type, &inline_parent, schema, "inline")?;
1905
1906                let inline_coordinate = if context_coordinate.is_empty() {
1907                    format!("{parent_type}...on {inline_parent}")
1908                } else {
1909                    format!("{context_coordinate}...on {inline_parent}")
1910                };
1911                push_directive_records(&inline_coordinate, fragment.directives(), records)?;
1912
1913                if let Some(inline_selection_set) = fragment.selection_set() {
1914                    collect_selection_directive_records(
1915                        &inline_selection_set,
1916                        &inline_parent,
1917                        schema,
1918                        fragments,
1919                        active_fragments,
1920                        &inline_coordinate,
1921                        records,
1922                    )?;
1923                }
1924            }
1925        }
1926    }
1927
1928    Ok(())
1929}
1930
1931fn push_directive_records(
1932    coordinate: &str,
1933    directives: Option<cst::Directives>,
1934    records: &mut Vec<DirectiveRecord>,
1935) -> Result<(), WesleyError> {
1936    let Some(directives) = directives else {
1937        return Ok(());
1938    };
1939
1940    for directive in directives.directives() {
1941        let name = required_name(directive.name(), "Directive missing name")?;
1942        let arguments = extract_executable_directive_arguments(directive.arguments())?;
1943        let arguments_canonical_json = stable_json_string(&arguments, "runtime optic directive")?;
1944        records.push(DirectiveRecord {
1945            coordinate: coordinate.to_string(),
1946            name,
1947            arguments_canonical_json,
1948        });
1949    }
1950
1951    Ok(())
1952}
1953
1954fn extract_executable_directive_arguments(
1955    arguments: Option<cst::Arguments>,
1956) -> Result<IndexMap<String, serde_json::Value>, WesleyError> {
1957    let mut values = IndexMap::new();
1958
1959    let Some(arguments) = arguments else {
1960        return Ok(values);
1961    };
1962
1963    for argument in arguments.arguments() {
1964        let name = required_name(argument.name(), "Directive argument missing name")?;
1965        let value = argument.value().ok_or_else(|| {
1966            operation_error_value(format!("Directive argument '{name}' missing value"))
1967        })?;
1968        if values
1969            .insert(name.clone(), executable_value_to_json(value)?)
1970            .is_some()
1971        {
1972            return operation_error(format!(
1973                "Directive argument '{name}' is declared more than once"
1974            ));
1975        }
1976    }
1977
1978    Ok(values)
1979}
1980
1981fn footprint_from_directives(
1982    directives: &[DirectiveRecord],
1983    root_coordinate: &str,
1984) -> Result<Option<Footprint>, WesleyError> {
1985    let mut footprint = None;
1986
1987    for directive in directives
1988        .iter()
1989        .filter(|directive| directive.name == "wes_footprint")
1990    {
1991        if directive.coordinate != root_coordinate {
1992            return operation_error(format!(
1993                "Runtime optic @wes_footprint is only supported on selected root field '{root_coordinate}', found on '{}'",
1994                directive.coordinate
1995            ));
1996        }
1997
1998        if footprint.is_some() {
1999            return operation_error("Runtime optic declares multiple footprints".to_string());
2000        }
2001
2002        let arguments: serde_json::Value =
2003            serde_json::from_str(&directive.arguments_canonical_json).map_err(|err| {
2004                operation_error_value(format!("Invalid canonical footprint arguments: {err}"))
2005            })?;
2006        footprint = Some(Footprint {
2007            reads: required_string_array(&arguments, "reads")?,
2008            writes: required_string_array(&arguments, "writes")?,
2009            forbids: optional_string_array(&arguments, "forbids")?,
2010        });
2011    }
2012
2013    Ok(footprint)
2014}
2015
2016fn required_string_array(
2017    arguments: &serde_json::Value,
2018    name: &str,
2019) -> Result<Vec<String>, WesleyError> {
2020    let value = arguments
2021        .get(name)
2022        .ok_or_else(|| operation_error_value(format!("Footprint argument '{name}' is required")))?;
2023    string_array(value, name)
2024}
2025
2026fn optional_string_array(
2027    arguments: &serde_json::Value,
2028    name: &str,
2029) -> Result<Vec<String>, WesleyError> {
2030    let Some(value) = arguments.get(name) else {
2031        return Ok(Vec::new());
2032    };
2033
2034    string_array(value, name)
2035}
2036
2037fn string_array(value: &serde_json::Value, name: &str) -> Result<Vec<String>, WesleyError> {
2038    let serde_json::Value::Array(items) = value else {
2039        return operation_error(format!(
2040            "Footprint argument '{name}' must be a string array"
2041        ));
2042    };
2043
2044    let mut labels = Vec::new();
2045    let mut seen = BTreeSet::new();
2046    for item in items {
2047        let label = item
2048            .as_str()
2049            .map(|value| value.to_string())
2050            .ok_or_else(|| {
2051                operation_error_value(format!(
2052                    "Footprint argument '{name}' contains a non-string value"
2053                ))
2054            })?;
2055        if !seen.insert(label.clone()) {
2056            return operation_error(format!(
2057                "Footprint argument '{name}' contains duplicate label '{label}'"
2058            ));
2059        }
2060        labels.push(label);
2061    }
2062
2063    Ok(labels)
2064}
2065
2066fn variable_definition_types(
2067    op: &cst::OperationDefinition,
2068) -> Result<BTreeMap<String, TypeReference>, WesleyError> {
2069    let mut variables = BTreeMap::new();
2070
2071    let Some(variable_definitions) = op.variable_definitions() else {
2072        return Ok(variables);
2073    };
2074
2075    for variable in variable_definitions.variable_definitions() {
2076        let name = variable
2077            .variable()
2078            .and_then(|variable| variable.name())
2079            .map(|name| name.text().to_string())
2080            .ok_or_else(|| operation_error_value("Variable definition missing name".to_string()))?;
2081        let type_node = variable.ty().ok_or_else(|| {
2082            operation_error_value(format!("Variable definition '{name}' missing type"))
2083        })?;
2084        let type_ref = type_reference_from_type(type_node, true)?;
2085
2086        if variables.insert(name.clone(), type_ref).is_some() {
2087            return operation_error(format!("Duplicate variable definition '${name}'"));
2088        }
2089    }
2090
2091    Ok(variables)
2092}
2093
2094fn root_argument_bindings(
2095    root_field: &cst::Field,
2096    schema_operation: &SchemaOperation,
2097    variable_types: &BTreeMap<String, TypeReference>,
2098    schema: &SchemaIndex<'_>,
2099) -> Result<Vec<RootArgumentBinding>, WesleyError> {
2100    let expected_arguments = schema_operation
2101        .arguments
2102        .iter()
2103        .map(|argument| (argument.name.as_str(), argument))
2104        .collect::<BTreeMap<_, _>>();
2105    let mut supplied = BTreeSet::new();
2106    let mut bindings = Vec::new();
2107
2108    if let Some(arguments) = root_field.arguments() {
2109        for argument in arguments.arguments() {
2110            let name = required_name(argument.name(), "Root field argument missing name")?;
2111            if !supplied.insert(name.clone()) {
2112                return operation_error(format!(
2113                    "Runtime optic root field '{}' declares duplicate argument '{name}'",
2114                    schema_operation.field_name
2115                ));
2116            }
2117
2118            let expected = expected_arguments.get(name.as_str()).ok_or_else(|| {
2119                operation_error_value(format!(
2120                    "Runtime optic root field '{}' declares unknown argument '{name}'",
2121                    schema_operation.field_name
2122                ))
2123            })?;
2124            let value = argument.value().ok_or_else(|| {
2125                operation_error_value(format!("Root field argument '{name}' missing value"))
2126            })?;
2127
2128            validate_root_argument_value(value.clone(), expected, variable_types, schema)?;
2129            let value_canonical_json =
2130                stable_json_string(&executable_value_to_json(value)?, "runtime optic argument")?;
2131            bindings.push(RootArgumentBinding {
2132                name,
2133                type_ref: expected.r#type.clone(),
2134                value_canonical_json,
2135            });
2136        }
2137    }
2138
2139    for expected in &schema_operation.arguments {
2140        if !expected.r#type.nullable
2141            && expected.default_value.is_none()
2142            && !supplied.contains(&expected.name)
2143        {
2144            return operation_error(format!(
2145                "Runtime optic root field '{}' missing required argument '{}'",
2146                schema_operation.field_name, expected.name
2147            ));
2148        }
2149    }
2150
2151    bindings.sort_by(|left, right| left.name.cmp(&right.name));
2152    Ok(bindings)
2153}
2154
2155fn selection_argument_bindings(
2156    root_field: &cst::Field,
2157    result_type: &TypeReference,
2158    schema: &SchemaIndex<'_>,
2159    fragments: &BTreeMap<String, cst::FragmentDefinition>,
2160    variable_types: &BTreeMap<String, TypeReference>,
2161) -> Result<Vec<SelectionArgumentBinding>, WesleyError> {
2162    let mut bindings = Vec::new();
2163    let context = SelectionArgumentBindingContext {
2164        schema,
2165        fragments,
2166        variable_types,
2167    };
2168
2169    if let Some(selection_set) = root_field.selection_set() {
2170        collect_selection_argument_bindings(
2171            &selection_set,
2172            &result_type.base,
2173            &context,
2174            &mut Vec::new(),
2175            "",
2176            &mut bindings,
2177        )?;
2178    }
2179
2180    Ok(bindings)
2181}
2182
2183struct SelectionArgumentBindingContext<'a> {
2184    schema: &'a SchemaIndex<'a>,
2185    fragments: &'a BTreeMap<String, cst::FragmentDefinition>,
2186    variable_types: &'a BTreeMap<String, TypeReference>,
2187}
2188
2189fn collect_selection_argument_bindings(
2190    selection_set: &cst::SelectionSet,
2191    parent_type: &str,
2192    context: &SelectionArgumentBindingContext<'_>,
2193    active_fragments: &mut Vec<String>,
2194    prefix: &str,
2195    bindings: &mut Vec<SelectionArgumentBinding>,
2196) -> Result<(), WesleyError> {
2197    for selection in selection_set.selections() {
2198        match selection {
2199            cst::Selection::Field(field) => {
2200                let field_name = required_name(field.name(), "Field selection missing name")?;
2201                let response_name = response_field_name(&field)?;
2202                let schema_field = context.schema.field(parent_type, &field_name)?;
2203                let path = if prefix.is_empty() {
2204                    response_name
2205                } else {
2206                    format!("{prefix}.{response_name}")
2207                };
2208
2209                append_field_argument_bindings(
2210                    &field,
2211                    &path,
2212                    schema_field,
2213                    context.variable_types,
2214                    context.schema,
2215                    bindings,
2216                )?;
2217
2218                if let Some(nested_selection_set) = field.selection_set() {
2219                    let nested_parent = schema_field.r#type.base.as_str();
2220                    context.schema.require_type(nested_parent)?;
2221                    collect_selection_argument_bindings(
2222                        &nested_selection_set,
2223                        nested_parent,
2224                        context,
2225                        active_fragments,
2226                        &path,
2227                        bindings,
2228                    )?;
2229                }
2230            }
2231            cst::Selection::FragmentSpread(spread) => {
2232                let name = spread
2233                    .fragment_name()
2234                    .and_then(|fragment_name| fragment_name.name())
2235                    .map(|name| name.text().to_string())
2236                    .ok_or_else(|| {
2237                        operation_error_value("Fragment spread missing name".to_string())
2238                    })?;
2239
2240                if active_fragments.contains(&name) {
2241                    return operation_error(format!(
2242                        "Cyclic fragment spread detected for fragment '{name}'"
2243                    ));
2244                }
2245
2246                let fragment = context.fragments.get(&name).ok_or_else(|| {
2247                    operation_error_value(format!("Unknown fragment spread '{name}'"))
2248                })?;
2249                let fragment_parent = fragment_type_condition(fragment)?;
2250                validate_fragment_type_condition(
2251                    parent_type,
2252                    &fragment_parent,
2253                    context.schema,
2254                    &name,
2255                )?;
2256
2257                active_fragments.push(name);
2258                if let Some(fragment_selection_set) = fragment.selection_set() {
2259                    collect_selection_argument_bindings(
2260                        &fragment_selection_set,
2261                        &fragment_parent,
2262                        context,
2263                        active_fragments,
2264                        prefix,
2265                        bindings,
2266                    )?;
2267                }
2268                active_fragments.pop();
2269            }
2270            cst::Selection::InlineFragment(fragment) => {
2271                let inline_parent = if let Some(type_condition) = fragment.type_condition() {
2272                    named_type_name(type_condition.named_type(), "Inline fragment missing type")?
2273                } else {
2274                    parent_type.to_string()
2275                };
2276                validate_fragment_type_condition(
2277                    parent_type,
2278                    &inline_parent,
2279                    context.schema,
2280                    "inline",
2281                )?;
2282
2283                if let Some(inline_selection_set) = fragment.selection_set() {
2284                    collect_selection_argument_bindings(
2285                        &inline_selection_set,
2286                        &inline_parent,
2287                        context,
2288                        active_fragments,
2289                        prefix,
2290                        bindings,
2291                    )?;
2292                }
2293            }
2294        }
2295    }
2296
2297    Ok(())
2298}
2299
2300fn append_field_argument_bindings(
2301    field: &cst::Field,
2302    path: &str,
2303    schema_field: &Field,
2304    variable_types: &BTreeMap<String, TypeReference>,
2305    schema: &SchemaIndex<'_>,
2306    bindings: &mut Vec<SelectionArgumentBinding>,
2307) -> Result<(), WesleyError> {
2308    let expected_arguments = schema_field
2309        .arguments
2310        .iter()
2311        .map(|argument| (argument.name.as_str(), argument))
2312        .collect::<BTreeMap<_, _>>();
2313    let mut supplied = BTreeSet::new();
2314    let mut field_bindings = Vec::new();
2315
2316    if let Some(arguments) = field.arguments() {
2317        for argument in arguments.arguments() {
2318            let name = required_name(argument.name(), "Field argument missing name")?;
2319            if !supplied.insert(name.clone()) {
2320                return operation_error(format!(
2321                    "Runtime optic field '{path}' declares duplicate argument '{name}'"
2322                ));
2323            }
2324
2325            let expected = expected_arguments.get(name.as_str()).ok_or_else(|| {
2326                operation_error_value(format!(
2327                    "Runtime optic field '{path}' declares unknown argument '{name}'"
2328                ))
2329            })?;
2330            let value = argument.value().ok_or_else(|| {
2331                operation_error_value(format!("Field argument '{name}' missing value"))
2332            })?;
2333
2334            validate_field_argument_value(value.clone(), expected, variable_types, schema)?;
2335            let value_canonical_json = stable_json_string(
2336                &executable_value_to_json(value)?,
2337                "runtime optic selection argument",
2338            )?;
2339            field_bindings.push(SelectionArgumentBinding {
2340                path: path.to_string(),
2341                name,
2342                type_ref: expected.r#type.clone(),
2343                value_canonical_json,
2344            });
2345        }
2346    }
2347
2348    for expected in &schema_field.arguments {
2349        if !expected.r#type.nullable
2350            && expected.default_value.is_none()
2351            && !supplied.contains(&expected.name)
2352        {
2353            return operation_error(format!(
2354                "Runtime optic field '{path}' missing required argument '{}'",
2355                expected.name
2356            ));
2357        }
2358    }
2359
2360    field_bindings.sort_by(|left, right| left.name.cmp(&right.name));
2361    bindings.extend(field_bindings);
2362    Ok(())
2363}
2364
2365fn validate_root_argument_value(
2366    value: cst::Value,
2367    expected: &OperationArgument,
2368    variable_types: &BTreeMap<String, TypeReference>,
2369    schema: &SchemaIndex<'_>,
2370) -> Result<(), WesleyError> {
2371    validate_argument_value(
2372        value,
2373        "Root argument",
2374        &expected.name,
2375        &expected.r#type,
2376        variable_types,
2377        schema,
2378    )
2379}
2380
2381fn validate_field_argument_value(
2382    value: cst::Value,
2383    expected: &FieldArgument,
2384    variable_types: &BTreeMap<String, TypeReference>,
2385    schema: &SchemaIndex<'_>,
2386) -> Result<(), WesleyError> {
2387    validate_argument_value(
2388        value,
2389        "Field argument",
2390        &expected.name,
2391        &expected.r#type,
2392        variable_types,
2393        schema,
2394    )
2395}
2396
2397fn validate_argument_value(
2398    value: cst::Value,
2399    argument_context: &str,
2400    argument_name: &str,
2401    expected_type: &TypeReference,
2402    variable_types: &BTreeMap<String, TypeReference>,
2403    schema: &SchemaIndex<'_>,
2404) -> Result<(), WesleyError> {
2405    match value {
2406        cst::Value::Variable(variable) => {
2407            let variable_name = variable
2408                .name()
2409                .map(|name| name.text().to_string())
2410                .ok_or_else(|| operation_error_value("Variable reference missing name".into()))?;
2411            let variable_type = variable_types.get(&variable_name).ok_or_else(|| {
2412                operation_error_value(format!(
2413                    "{argument_context} '{argument_name}' references undefined variable '${variable_name}'"
2414                ))
2415            })?;
2416
2417            if !variable_type_is_compatible(variable_type, expected_type) {
2418                return operation_error(format!(
2419                    "Variable '${variable_name}' has type '{}' but argument '{}' expects '{}'",
2420                    display_type_ref(variable_type),
2421                    argument_name,
2422                    display_type_ref(expected_type)
2423                ));
2424            }
2425
2426            Ok(())
2427        }
2428        literal => validate_literal_value(
2429            literal,
2430            argument_context,
2431            argument_name,
2432            expected_type,
2433            schema,
2434        ),
2435    }
2436}
2437
2438fn validate_literal_value(
2439    value: cst::Value,
2440    argument_context: &str,
2441    argument_name: &str,
2442    expected_type: &TypeReference,
2443    schema: &SchemaIndex<'_>,
2444) -> Result<(), WesleyError> {
2445    match value {
2446        cst::Value::NullValue(_) => {
2447            if expected_type.nullable {
2448                Ok(())
2449            } else {
2450                operation_error(format!(
2451                    "{argument_context} '{argument_name}' is non-null but received null",
2452                ))
2453            }
2454        }
2455        cst::Value::StringValue(_) => validate_named_scalar_literal(
2456            argument_context,
2457            argument_name,
2458            "String",
2459            expected_type,
2460            schema,
2461        ),
2462        cst::Value::IntValue(_) => {
2463            if expected_type.base == "Float" && !is_list_type(expected_type) {
2464                Ok(())
2465            } else {
2466                validate_named_scalar_literal(
2467                    argument_context,
2468                    argument_name,
2469                    "Int",
2470                    expected_type,
2471                    schema,
2472                )
2473            }
2474        }
2475        cst::Value::FloatValue(_) => validate_named_scalar_literal(
2476            argument_context,
2477            argument_name,
2478            "Float",
2479            expected_type,
2480            schema,
2481        ),
2482        cst::Value::BooleanValue(_) => validate_named_scalar_literal(
2483            argument_context,
2484            argument_name,
2485            "Boolean",
2486            expected_type,
2487            schema,
2488        ),
2489        cst::Value::EnumValue(value) => validate_enum_literal(
2490            value,
2491            argument_context,
2492            argument_name,
2493            expected_type,
2494            schema,
2495        ),
2496        cst::Value::ListValue(list) => {
2497            if !is_list_type(expected_type) {
2498                return literal_type_error(argument_context, argument_name, "List", expected_type);
2499            }
2500            let item_type = list_item_type_ref(expected_type);
2501            for item in list.values() {
2502                validate_literal_value(item, argument_context, argument_name, &item_type, schema)?;
2503            }
2504            Ok(())
2505        }
2506        cst::Value::ObjectValue(object) => validate_object_literal(
2507            object,
2508            argument_context,
2509            argument_name,
2510            expected_type,
2511            schema,
2512        ),
2513        cst::Value::Variable(_) => operation_error(format!(
2514            "{argument_context} '{argument_name}' received nested variable value"
2515        )),
2516    }
2517}
2518
2519fn validate_named_scalar_literal(
2520    argument_context: &str,
2521    argument_name: &str,
2522    actual: &str,
2523    expected_type: &TypeReference,
2524    schema: &SchemaIndex<'_>,
2525) -> Result<(), WesleyError> {
2526    let builtin_matches = match actual {
2527        "String" => matches!(expected_type.base.as_str(), "String" | "ID"),
2528        "Int" => matches!(expected_type.base.as_str(), "Int" | "Float"),
2529        "Float" => expected_type.base == "Float",
2530        "Boolean" => expected_type.base == "Boolean",
2531        _ => false,
2532    };
2533
2534    if builtin_matches && !is_list_type(expected_type) {
2535        return Ok(());
2536    }
2537
2538    if schema.type_kind(&expected_type.base) == Some(TypeKind::Scalar)
2539        && !is_builtin_scalar(&expected_type.base)
2540        && !is_list_type(expected_type)
2541    {
2542        return Ok(());
2543    }
2544
2545    literal_type_error(argument_context, argument_name, actual, expected_type)
2546}
2547
2548fn validate_enum_literal(
2549    value: cst::EnumValue,
2550    argument_context: &str,
2551    argument_name: &str,
2552    expected_type: &TypeReference,
2553    schema: &SchemaIndex<'_>,
2554) -> Result<(), WesleyError> {
2555    let name = value
2556        .name()
2557        .map(|name| name.text().to_string())
2558        .ok_or_else(|| operation_error_value("Enum argument value missing name".into()))?;
2559
2560    if is_list_type(expected_type) {
2561        return literal_type_error(argument_context, argument_name, "Enum", expected_type);
2562    }
2563
2564    match schema.type_kind(&expected_type.base) {
2565        Some(TypeKind::Enum) => {
2566            let type_def = schema.require_type(&expected_type.base)?;
2567            if type_def.enum_values.contains(&name) {
2568                Ok(())
2569            } else {
2570                operation_error(format!(
2571                    "{argument_context} '{argument_name}' received unknown enum value '{name}' for '{}'",
2572                    expected_type.base
2573                ))
2574            }
2575        }
2576        Some(TypeKind::Scalar) if !is_builtin_scalar(&expected_type.base) => Ok(()),
2577        _ => literal_type_error(argument_context, argument_name, "Enum", expected_type),
2578    }
2579}
2580
2581fn validate_object_literal(
2582    object: cst::ObjectValue,
2583    argument_context: &str,
2584    argument_name: &str,
2585    expected_type: &TypeReference,
2586    schema: &SchemaIndex<'_>,
2587) -> Result<(), WesleyError> {
2588    if is_list_type(expected_type) {
2589        return literal_type_error(argument_context, argument_name, "Object", expected_type);
2590    }
2591
2592    let type_def = schema.require_type(&expected_type.base)?;
2593    if type_def.kind == TypeKind::Scalar && !is_builtin_scalar(&expected_type.base) {
2594        return Ok(());
2595    }
2596    if type_def.kind != TypeKind::InputObject {
2597        return literal_type_error(argument_context, argument_name, "Object", expected_type);
2598    }
2599
2600    let expected_fields = type_def
2601        .fields
2602        .iter()
2603        .map(|field| (field.name.as_str(), field))
2604        .collect::<BTreeMap<_, _>>();
2605    let mut supplied = BTreeSet::new();
2606
2607    for field in object.object_fields() {
2608        let name = field
2609            .name()
2610            .map(|name| name.text().to_string())
2611            .ok_or_else(|| operation_error_value("Object argument field missing name".into()))?;
2612        if !supplied.insert(name.clone()) {
2613            return operation_error(format!(
2614                "{argument_context} '{argument_name}' declares duplicate input field '{name}'"
2615            ));
2616        }
2617
2618        let expected_field = expected_fields.get(name.as_str()).ok_or_else(|| {
2619            operation_error_value(format!(
2620                "{argument_context} '{argument_name}' declares unknown input field '{name}'"
2621            ))
2622        })?;
2623        let value = field.value().ok_or_else(|| {
2624            operation_error_value(format!("Object argument field '{name}' missing value"))
2625        })?;
2626        validate_literal_value(value, "Input field", &name, &expected_field.r#type, schema)?;
2627    }
2628
2629    for expected_field in &type_def.fields {
2630        if !expected_field.r#type.nullable
2631            && expected_field.default_value.is_none()
2632            && !supplied.contains(&expected_field.name)
2633        {
2634            return operation_error(format!(
2635                "{argument_context} '{argument_name}' missing required input field '{}'",
2636                expected_field.name
2637            ));
2638        }
2639    }
2640
2641    Ok(())
2642}
2643
2644fn executable_value_to_json(value: cst::Value) -> Result<serde_json::Value, WesleyError> {
2645    match value {
2646        cst::Value::Variable(variable) => {
2647            let name = variable
2648                .name()
2649                .map(|name| name.text().to_string())
2650                .ok_or_else(|| operation_error_value("Variable reference missing name".into()))?;
2651            Ok(serde_json::json!({ "$variable": name }))
2652        }
2653        cst::Value::StringValue(value) => Ok(serde_json::Value::String(String::from(value))),
2654        cst::Value::FloatValue(value) => {
2655            let raw = value
2656                .float_token()
2657                .map(|token| token.text().to_string())
2658                .unwrap_or_default();
2659            let parsed = raw.parse::<f64>().map_err(|err| {
2660                operation_error_value(format!("Invalid float argument value '{raw}': {err}"))
2661            })?;
2662            serde_json::Number::from_f64(parsed)
2663                .map(serde_json::Value::Number)
2664                .ok_or_else(|| {
2665                    operation_error_value(format!("Invalid finite float argument value '{raw}'"))
2666                })
2667        }
2668        cst::Value::IntValue(value) => {
2669            let raw = value
2670                .int_token()
2671                .map(|token| token.text().to_string())
2672                .unwrap_or_default();
2673            raw.parse::<i64>()
2674                .map(|parsed| serde_json::Value::Number(parsed.into()))
2675                .map_err(|err| {
2676                    operation_error_value(format!("Invalid integer argument value '{raw}': {err}"))
2677                })
2678        }
2679        cst::Value::BooleanValue(value) => Ok(serde_json::Value::Bool(
2680            value.true_token().is_some() && value.false_token().is_none(),
2681        )),
2682        cst::Value::NullValue(_) => Ok(serde_json::Value::Null),
2683        cst::Value::EnumValue(value) => {
2684            let name = value
2685                .name()
2686                .map(|name| name.text().to_string())
2687                .ok_or_else(|| operation_error_value("Enum argument value missing name".into()))?;
2688            Ok(serde_json::Value::String(name))
2689        }
2690        cst::Value::ListValue(list) => {
2691            let mut values = Vec::new();
2692            for value in list.values() {
2693                values.push(executable_value_to_json(value)?);
2694            }
2695            Ok(serde_json::Value::Array(values))
2696        }
2697        cst::Value::ObjectValue(object) => {
2698            let mut map = serde_json::Map::new();
2699            for field in object.object_fields() {
2700                let name = field
2701                    .name()
2702                    .map(|name| name.text().to_string())
2703                    .ok_or_else(|| {
2704                        operation_error_value("Object argument field missing name".into())
2705                    })?;
2706                let value = field.value().ok_or_else(|| {
2707                    operation_error_value(format!("Object argument field '{name}' missing value"))
2708                })?;
2709                map.insert(name, executable_value_to_json(value)?);
2710            }
2711            Ok(serde_json::Value::Object(map))
2712        }
2713    }
2714}
2715
2716fn literal_type_error(
2717    argument_context: &str,
2718    argument_name: &str,
2719    actual: &str,
2720    expected: &TypeReference,
2721) -> Result<(), WesleyError> {
2722    operation_error(format!(
2723        "{argument_context} '{argument_name}' received {actual} value but expects '{}'",
2724        display_type_ref(expected)
2725    ))
2726}
2727
2728fn variable_type_is_compatible(actual: &TypeReference, expected: &TypeReference) -> bool {
2729    actual.base == expected.base
2730        && actual.is_list == expected.is_list
2731        && actual.list_wrappers == expected.list_wrappers
2732        && (!actual.nullable || expected.nullable)
2733        && list_items_are_compatible(actual, expected)
2734}
2735
2736fn list_items_are_compatible(actual: &TypeReference, expected: &TypeReference) -> bool {
2737    if matches!(
2738        (actual.list_item_nullable, expected.list_item_nullable),
2739        (Some(true), Some(false))
2740    ) {
2741        return false;
2742    }
2743
2744    !matches!(
2745        (
2746            actual.leaf_nullable.or(actual.list_item_nullable),
2747            expected.leaf_nullable.or(expected.list_item_nullable),
2748        ),
2749        (Some(true), Some(false))
2750    )
2751}
2752
2753fn list_item_type_ref(type_ref: &TypeReference) -> TypeReference {
2754    if !type_ref.list_wrappers.is_empty() {
2755        let leaf_nullable = type_ref
2756            .leaf_nullable
2757            .unwrap_or_else(|| type_ref.list_item_nullable.unwrap_or(true));
2758        return type_ref_from_list_shape(
2759            type_ref.base.clone(),
2760            type_ref.list_wrappers[1..].to_vec(),
2761            leaf_nullable,
2762        );
2763    }
2764
2765    type_ref_from_list_shape(
2766        type_ref.base.clone(),
2767        Vec::new(),
2768        type_ref.list_item_nullable.unwrap_or(true),
2769    )
2770}
2771
2772fn type_ref_from_list_shape(
2773    base: String,
2774    list_wrappers: Vec<TypeListWrapper>,
2775    leaf_nullable: bool,
2776) -> TypeReference {
2777    if list_wrappers.is_empty() {
2778        return TypeReference {
2779            base,
2780            nullable: leaf_nullable,
2781            is_list: false,
2782            list_item_nullable: None,
2783            list_wrappers: Vec::new(),
2784            leaf_nullable: None,
2785        };
2786    }
2787
2788    let list_item_nullable = Some(
2789        list_wrappers
2790            .get(1)
2791            .map(|wrapper| wrapper.nullable)
2792            .unwrap_or(leaf_nullable),
2793    );
2794    let has_nested_lists = list_wrappers.len() > 1;
2795
2796    TypeReference {
2797        base,
2798        nullable: list_wrappers[0].nullable,
2799        is_list: true,
2800        list_item_nullable,
2801        list_wrappers: if has_nested_lists {
2802            list_wrappers
2803        } else {
2804            Vec::new()
2805        },
2806        leaf_nullable: if has_nested_lists {
2807            Some(leaf_nullable)
2808        } else {
2809            None
2810        },
2811    }
2812}
2813
2814fn display_type_ref(type_ref: &TypeReference) -> String {
2815    let mut rendered = if type_ref.is_list {
2816        format!(
2817            "[{}{}]",
2818            type_ref.base,
2819            if type_ref.list_item_nullable == Some(false) {
2820                "!"
2821            } else {
2822                ""
2823            }
2824        )
2825    } else {
2826        type_ref.base.clone()
2827    };
2828
2829    if !type_ref.nullable {
2830        rendered.push('!');
2831    }
2832
2833    rendered
2834}
2835
2836fn is_builtin_scalar(name: &str) -> bool {
2837    matches!(name, "ID" | "String" | "Int" | "Float" | "Boolean")
2838}
2839
2840fn variable_codec_shape(
2841    op: &cst::OperationDefinition,
2842    schema_operation: &SchemaOperation,
2843) -> Result<CodecShape, WesleyError> {
2844    let type_name = op
2845        .name()
2846        .map(|name| format!("{}Variables", name.text()))
2847        .unwrap_or_else(|| format!("{}Variables", schema_operation.field_name));
2848
2849    let fields = if let Some(variable_definitions) = op.variable_definitions() {
2850        variable_definitions
2851            .variable_definitions()
2852            .map(variable_codec_field)
2853            .collect::<Result<Vec<_>, _>>()?
2854    } else {
2855        schema_operation
2856            .arguments
2857            .iter()
2858            .map(|argument| CodecField {
2859                name: argument.name.clone(),
2860                type_ref: argument.r#type.clone(),
2861                required: argument.default_value.is_none() && !argument.r#type.nullable,
2862                list: is_list_type(&argument.r#type),
2863            })
2864            .collect()
2865    };
2866
2867    Ok(CodecShape { type_name, fields })
2868}
2869
2870fn variable_codec_field(variable: cst::VariableDefinition) -> Result<CodecField, WesleyError> {
2871    let name = variable
2872        .variable()
2873        .and_then(|variable| variable.name())
2874        .map(|name| name.text().to_string())
2875        .ok_or_else(|| operation_error_value("Variable definition missing name".to_string()))?;
2876    let type_node = variable.ty().ok_or_else(|| {
2877        operation_error_value(format!("Variable definition '{name}' missing type"))
2878    })?;
2879    let type_ref = type_reference_from_type(type_node, true)?;
2880
2881    Ok(CodecField {
2882        name,
2883        required: variable.default_value().is_none() && !type_ref.nullable,
2884        list: is_list_type(&type_ref),
2885        type_ref,
2886    })
2887}
2888
2889fn payload_codec_shape(
2890    root_field: &cst::Field,
2891    result_type: &TypeReference,
2892    schema: &SchemaIndex<'_>,
2893    fragments: &BTreeMap<String, cst::FragmentDefinition>,
2894) -> Result<CodecShape, WesleyError> {
2895    let mut fields = Vec::new();
2896    let context = PayloadCodecContext { schema, fragments };
2897
2898    if let Some(selection_set) = root_field.selection_set() {
2899        collect_payload_codec_fields(
2900            &selection_set,
2901            &result_type.base,
2902            &context,
2903            &mut Vec::new(),
2904            "",
2905            !result_type.nullable,
2906            &mut fields,
2907        )?;
2908    }
2909
2910    Ok(CodecShape {
2911        type_name: result_type.base.to_string(),
2912        fields,
2913    })
2914}
2915
2916struct PayloadCodecContext<'a> {
2917    schema: &'a SchemaIndex<'a>,
2918    fragments: &'a BTreeMap<String, cst::FragmentDefinition>,
2919}
2920
2921fn collect_payload_codec_fields(
2922    selection_set: &cst::SelectionSet,
2923    parent_type: &str,
2924    context: &PayloadCodecContext<'_>,
2925    active_fragments: &mut Vec<String>,
2926    prefix: &str,
2927    parent_path_required: bool,
2928    fields: &mut Vec<CodecField>,
2929) -> Result<(), WesleyError> {
2930    for selection in selection_set.selections() {
2931        match selection {
2932            cst::Selection::Field(field) => {
2933                let field_name = required_name(field.name(), "Field selection missing name")?;
2934                let response_name = response_field_name(&field)?;
2935                let schema_field = context.schema.field(parent_type, &field_name)?;
2936                let path = if prefix.is_empty() {
2937                    response_name
2938                } else {
2939                    format!("{prefix}.{response_name}")
2940                };
2941                let field_required = parent_path_required && !schema_field.r#type.nullable;
2942
2943                push_unique_codec_field(
2944                    fields,
2945                    CodecField {
2946                        name: path.clone(),
2947                        type_ref: schema_field.r#type.clone(),
2948                        required: field_required,
2949                        list: is_list_type(&schema_field.r#type),
2950                    },
2951                );
2952
2953                if let Some(nested_selection_set) = field.selection_set() {
2954                    let nested_parent = schema_field.r#type.base.as_str();
2955                    context.schema.require_type(nested_parent)?;
2956                    collect_payload_codec_fields(
2957                        &nested_selection_set,
2958                        nested_parent,
2959                        context,
2960                        active_fragments,
2961                        &path,
2962                        field_required,
2963                        fields,
2964                    )?;
2965                }
2966            }
2967            cst::Selection::FragmentSpread(spread) => {
2968                let name = spread
2969                    .fragment_name()
2970                    .and_then(|fragment_name| fragment_name.name())
2971                    .map(|name| name.text().to_string())
2972                    .ok_or_else(|| {
2973                        operation_error_value("Fragment spread missing name".to_string())
2974                    })?;
2975
2976                if active_fragments.contains(&name) {
2977                    return operation_error(format!(
2978                        "Cyclic fragment spread detected for fragment '{name}'"
2979                    ));
2980                }
2981
2982                let fragment = context.fragments.get(&name).ok_or_else(|| {
2983                    operation_error_value(format!("Unknown fragment spread '{name}'"))
2984                })?;
2985                let fragment_parent = fragment_type_condition(fragment)?;
2986                validate_fragment_type_condition(
2987                    parent_type,
2988                    &fragment_parent,
2989                    context.schema,
2990                    &name,
2991                )?;
2992
2993                active_fragments.push(name);
2994                if let Some(fragment_selection_set) = fragment.selection_set() {
2995                    collect_payload_codec_fields(
2996                        &fragment_selection_set,
2997                        &fragment_parent,
2998                        context,
2999                        active_fragments,
3000                        prefix,
3001                        parent_path_required,
3002                        fields,
3003                    )?;
3004                }
3005                active_fragments.pop();
3006            }
3007            cst::Selection::InlineFragment(fragment) => {
3008                let inline_parent = if let Some(type_condition) = fragment.type_condition() {
3009                    named_type_name(type_condition.named_type(), "Inline fragment missing type")?
3010                } else {
3011                    parent_type.to_string()
3012                };
3013                validate_fragment_type_condition(
3014                    parent_type,
3015                    &inline_parent,
3016                    context.schema,
3017                    "inline",
3018                )?;
3019
3020                if let Some(inline_selection_set) = fragment.selection_set() {
3021                    collect_payload_codec_fields(
3022                        &inline_selection_set,
3023                        &inline_parent,
3024                        context,
3025                        active_fragments,
3026                        prefix,
3027                        parent_path_required,
3028                        fields,
3029                    )?;
3030                }
3031            }
3032        }
3033    }
3034
3035    Ok(())
3036}
3037
3038fn response_field_name(field: &cst::Field) -> Result<String, WesleyError> {
3039    field
3040        .alias()
3041        .and_then(|alias| alias.name())
3042        .or_else(|| field.name())
3043        .map(|name| name.text().to_string())
3044        .ok_or_else(|| operation_error_value("Field selection missing response name".into()))
3045}
3046
3047fn push_unique_codec_field(fields: &mut Vec<CodecField>, field: CodecField) {
3048    if !fields.iter().any(|existing| existing.name == field.name) {
3049        fields.push(field);
3050    }
3051}
3052
3053fn law_claims_for_operation(
3054    operation_id: &str,
3055    directives: &[DirectiveRecord],
3056    footprint: Option<&Footprint>,
3057) -> Result<Vec<LawClaimTemplate>, WesleyError> {
3058    let mut claims = Vec::new();
3059    let mut seen = BTreeSet::new();
3060
3061    push_law_claim(
3062        &mut claims,
3063        &mut seen,
3064        operation_id,
3065        "shape.valid.v1",
3066        vec![EvidenceKind::Compiler],
3067    );
3068    push_law_claim(
3069        &mut claims,
3070        &mut seen,
3071        operation_id,
3072        "codec.canonical.v1",
3073        vec![EvidenceKind::Compiler, EvidenceKind::Codec],
3074    );
3075
3076    for law_id in law_ids_from_directives(directives)? {
3077        push_law_claim(
3078            &mut claims,
3079            &mut seen,
3080            operation_id,
3081            &law_id,
3082            vec![EvidenceKind::HostPolicy, EvidenceKind::DomainVerifier],
3083        );
3084    }
3085
3086    if footprint.is_some() {
3087        push_law_claim(
3088            &mut claims,
3089            &mut seen,
3090            operation_id,
3091            "footprint.closed.v1",
3092            vec![EvidenceKind::RuntimeTrace],
3093        );
3094    }
3095
3096    Ok(claims)
3097}
3098
3099fn admission_requirements_from_footprint(
3100    footprint: Option<&Footprint>,
3101) -> OpticAdmissionRequirements {
3102    let mut required_permissions = Vec::new();
3103    let mut forbidden_resources = Vec::new();
3104
3105    if let Some(footprint) = footprint {
3106        for resource in &footprint.reads {
3107            required_permissions.push(PermissionRequirement {
3108                action: PermissionAction::Read,
3109                resource: resource.clone(),
3110                source: "wes_footprint.reads".to_string(),
3111            });
3112        }
3113
3114        for resource in &footprint.writes {
3115            required_permissions.push(PermissionRequirement {
3116                action: PermissionAction::Write,
3117                resource: resource.clone(),
3118                source: "wes_footprint.writes".to_string(),
3119            });
3120        }
3121
3122        forbidden_resources = footprint.forbids.clone();
3123    }
3124
3125    OpticAdmissionRequirements {
3126        identity: IdentityRequirement {
3127            required: true,
3128            accepted_principal_kinds: Vec::new(),
3129        },
3130        required_permissions,
3131        forbidden_resources,
3132    }
3133}
3134
3135fn push_law_claim(
3136    claims: &mut Vec<LawClaimTemplate>,
3137    seen: &mut BTreeSet<String>,
3138    operation_id: &str,
3139    law_id: &str,
3140    required_evidence: Vec<EvidenceKind>,
3141) {
3142    if !seen.insert(law_id.to_string()) {
3143        return;
3144    }
3145
3146    let claim_id = compute_content_hash(&format!("law-claim:{operation_id}:{law_id}"));
3147    claims.push(LawClaimTemplate {
3148        law_id: law_id.to_string(),
3149        claim_id,
3150        operation_id: operation_id.to_string(),
3151        required_evidence,
3152    });
3153}
3154
3155fn law_ids_from_directives(directives: &[DirectiveRecord]) -> Result<Vec<String>, WesleyError> {
3156    let mut law_ids = Vec::new();
3157
3158    for directive in directives
3159        .iter()
3160        .filter(|directive| directive.name == "wes_law")
3161    {
3162        let arguments: serde_json::Value =
3163            serde_json::from_str(&directive.arguments_canonical_json).map_err(|err| {
3164                operation_error_value(format!("Invalid canonical law arguments: {err}"))
3165            })?;
3166        let law_id = arguments
3167            .get("id")
3168            .and_then(serde_json::Value::as_str)
3169            .ok_or_else(|| {
3170                operation_error_value("Directive 'wes_law' requires string argument 'id'".into())
3171            })?;
3172        law_ids.push(law_id.to_string());
3173    }
3174
3175    Ok(law_ids)
3176}
3177
3178fn is_list_type(type_ref: &TypeReference) -> bool {
3179    type_ref.is_list || !type_ref.list_wrappers.is_empty()
3180}
3181
3182fn stable_json_hash<T: serde::Serialize>(value: &T, area: &str) -> Result<String, WesleyError> {
3183    let canonical = stable_json_string(value, area)?;
3184    Ok(compute_content_hash(&canonical))
3185}
3186
3187fn stable_json_string<T: serde::Serialize>(value: &T, area: &str) -> Result<String, WesleyError> {
3188    to_canonical_json(value)
3189        .map_err(|err| lowering_error_value(area, format!("Failed to serialize JSON: {err}")))
3190}
3191
3192struct SchemaIndex<'a> {
3193    types: HashMap<&'a str, &'a TypeDefinition>,
3194}
3195
3196impl<'a> SchemaIndex<'a> {
3197    fn new(ir: &'a WesleyIR) -> Self {
3198        let types = ir
3199            .types
3200            .iter()
3201            .map(|type_def| (type_def.name.as_str(), type_def))
3202            .collect::<HashMap<_, _>>();
3203        Self { types }
3204    }
3205
3206    fn require_type(&self, name: &str) -> Result<&'a TypeDefinition, WesleyError> {
3207        self.types
3208            .get(name)
3209            .copied()
3210            .ok_or_else(|| operation_error_value(format!("Unknown selection parent type '{name}'")))
3211    }
3212
3213    fn type_kind(&self, name: &str) -> Option<TypeKind> {
3214        self.types.get(name).map(|type_def| type_def.kind)
3215    }
3216
3217    fn possible_runtime_types(&self, name: &str) -> Result<BTreeSet<String>, WesleyError> {
3218        let type_def = self.require_type(name)?;
3219        match type_def.kind {
3220            TypeKind::Object => Ok(BTreeSet::from([name.to_string()])),
3221            TypeKind::Interface => Ok(self
3222                .types
3223                .values()
3224                .filter(|candidate| {
3225                    candidate.kind == TypeKind::Object
3226                        && candidate
3227                            .implements
3228                            .iter()
3229                            .any(|interface| interface == name)
3230                })
3231                .map(|candidate| candidate.name.clone())
3232                .collect()),
3233            TypeKind::Union => Ok(type_def.union_members.iter().cloned().collect()),
3234            _ => operation_error(format!("Type '{name}' is not a composite fragment parent")),
3235        }
3236    }
3237
3238    fn field(&self, parent_type: &str, field_name: &str) -> Result<&'a Field, WesleyError> {
3239        let type_def = self.require_type(parent_type)?;
3240        type_def
3241            .fields
3242            .iter()
3243            .find(|field| field.name == field_name)
3244            .ok_or_else(|| {
3245                operation_error_value(format!(
3246                    "Type '{parent_type}' does not define selected field '{field_name}'"
3247                ))
3248            })
3249    }
3250}
3251
3252struct RootTypes {
3253    query: String,
3254    mutation: String,
3255    subscription: String,
3256}
3257
3258impl Default for RootTypes {
3259    fn default() -> Self {
3260        Self {
3261            query: "Query".to_string(),
3262            mutation: "Mutation".to_string(),
3263            subscription: "Subscription".to_string(),
3264        }
3265    }
3266}
3267
3268impl RootTypes {
3269    fn operation_types_for_type(&self, type_name: &str) -> Vec<OperationType> {
3270        let mut operation_types = Vec::new();
3271
3272        if self.query == type_name {
3273            operation_types.push(OperationType::Query);
3274        }
3275        if self.mutation == type_name {
3276            operation_types.push(OperationType::Mutation);
3277        }
3278        if self.subscription == type_name {
3279            operation_types.push(OperationType::Subscription);
3280        }
3281
3282        operation_types
3283    }
3284
3285    fn root_for_operation(&self, op: &cst::OperationDefinition) -> Result<&str, WesleyError> {
3286        let Some(operation_type) = op.operation_type() else {
3287            return Ok(self.query.as_str());
3288        };
3289
3290        if operation_type.query_token().is_some() {
3291            Ok(self.query.as_str())
3292        } else if operation_type.mutation_token().is_some() {
3293            Ok(self.mutation.as_str())
3294        } else if operation_type.subscription_token().is_some() {
3295            Ok(self.subscription.as_str())
3296        } else {
3297            operation_error("Unknown GraphQL operation type".to_string())
3298        }
3299    }
3300}
3301
3302fn extract_root_types(schema_sdl: &str) -> Result<RootTypes, WesleyError> {
3303    let parser = Parser::new(schema_sdl);
3304    let cst = parser.parse();
3305
3306    let errors = cst.errors().collect::<Vec<_>>();
3307    if !errors.is_empty() {
3308        let err = &errors[0];
3309        return Err(WesleyError::ParseError {
3310            message: err.message().to_string(),
3311            line: None,
3312            column: None,
3313        });
3314    }
3315
3316    let mut root_types = RootTypes::default();
3317
3318    for def in cst.document().definitions() {
3319        match def {
3320            cst::Definition::SchemaDefinition(schema) => {
3321                update_root_types(schema.root_operation_type_definitions(), &mut root_types)?;
3322            }
3323            cst::Definition::SchemaExtension(schema) => {
3324                update_root_types(schema.root_operation_type_definitions(), &mut root_types)?;
3325            }
3326            _ => {}
3327        }
3328    }
3329
3330    Ok(root_types)
3331}
3332
3333fn collect_schema_operations_from_object(
3334    name: Option<cst::Name>,
3335    fields_definition: Option<cst::FieldsDefinition>,
3336    root_types: &RootTypes,
3337    operations: &mut Vec<SchemaOperation>,
3338) -> Result<(), WesleyError> {
3339    let type_name = type_node_name(name, "Object type missing name")?;
3340    let operation_types = root_types.operation_types_for_type(&type_name);
3341    if operation_types.is_empty() {
3342        return Ok(());
3343    }
3344
3345    let Some(fields_definition) = fields_definition else {
3346        return Ok(());
3347    };
3348
3349    for field_def in fields_definition.field_definitions() {
3350        for operation_type in &operation_types {
3351            operations.push(schema_operation_from_field(
3352                *operation_type,
3353                &type_name,
3354                field_def.clone(),
3355            )?);
3356        }
3357    }
3358
3359    Ok(())
3360}
3361
3362fn schema_operation_from_field(
3363    operation_type: OperationType,
3364    root_type_name: &str,
3365    field_def: cst::FieldDefinition,
3366) -> Result<SchemaOperation, WesleyError> {
3367    let field_name = field_def
3368        .name()
3369        .map(|name| name.text().to_string())
3370        .ok_or_else(|| {
3371            lowering_error_value("schema operation", "Root field missing name".into())
3372        })?;
3373    let result_type = field_def.ty().ok_or_else(|| {
3374        lowering_error_value(
3375            "schema operation",
3376            format!("Root field '{field_name}' missing result type"),
3377        )
3378    })?;
3379
3380    let mut directives = IndexMap::new();
3381    if let Some(dirs) = field_def.directives() {
3382        ApolloLoweringAdapter::new(0).extract_directives(dirs, &mut directives)?;
3383    }
3384
3385    Ok(SchemaOperation {
3386        operation_type,
3387        root_type_name: root_type_name.to_string(),
3388        field_name,
3389        arguments: operation_arguments_from_definition(field_def.arguments_definition())?,
3390        result_type: type_reference_from_type(result_type, true)?,
3391        directives,
3392    })
3393}
3394
3395fn operation_arguments_from_definition(
3396    arguments_definition: Option<cst::ArgumentsDefinition>,
3397) -> Result<Vec<OperationArgument>, WesleyError> {
3398    let Some(arguments_definition) = arguments_definition else {
3399        return Ok(Vec::new());
3400    };
3401
3402    arguments_definition
3403        .input_value_definitions()
3404        .map(operation_argument_from_input_value)
3405        .collect()
3406}
3407
3408fn operation_argument_from_input_value(
3409    input_value: cst::InputValueDefinition,
3410) -> Result<OperationArgument, WesleyError> {
3411    let name = input_value
3412        .name()
3413        .map(|name| name.text().to_string())
3414        .ok_or_else(|| {
3415            lowering_error_value("schema operation", "Operation argument missing name".into())
3416        })?;
3417    let type_node = input_value.ty().ok_or_else(|| {
3418        lowering_error_value(
3419            "schema operation",
3420            format!("Operation argument '{name}' missing type"),
3421        )
3422    })?;
3423    let default_value = input_value
3424        .default_value()
3425        .and_then(|default_value| default_value.value())
3426        .map(directive_value_to_json)
3427        .transpose()?;
3428
3429    let mut directives = IndexMap::new();
3430    if let Some(dirs) = input_value.directives() {
3431        ApolloLoweringAdapter::new(0).extract_directives(dirs, &mut directives)?;
3432    }
3433
3434    Ok(OperationArgument {
3435        name,
3436        r#type: type_reference_from_type(type_node, true)?,
3437        default_value,
3438        directives,
3439    })
3440}
3441
3442fn update_root_types(
3443    root_defs: cst::CstChildren<cst::RootOperationTypeDefinition>,
3444    root_types: &mut RootTypes,
3445) -> Result<(), WesleyError> {
3446    for root_def in root_defs {
3447        let operation_type = root_def.operation_type().ok_or_else(|| {
3448            operation_error_value("Schema root operation missing operation type".to_string())
3449        })?;
3450        let named_type = named_type_name(
3451            root_def.named_type(),
3452            "Schema root operation missing named type",
3453        )?;
3454
3455        if operation_type.query_token().is_some() {
3456            root_types.query = named_type;
3457        } else if operation_type.mutation_token().is_some() {
3458            root_types.mutation = named_type;
3459        } else if operation_type.subscription_token().is_some() {
3460            root_types.subscription = named_type;
3461        }
3462    }
3463
3464    Ok(())
3465}
3466
3467fn push_unique(values: &mut Vec<String>, value: String) {
3468    if !values.contains(&value) {
3469        values.push(value);
3470    }
3471}
3472
3473fn fragment_name(fragment: &cst::FragmentDefinition) -> Result<String, WesleyError> {
3474    fragment
3475        .fragment_name()
3476        .and_then(|fragment_name| fragment_name.name())
3477        .map(|name| name.text().to_string())
3478        .ok_or_else(|| operation_error_value("Fragment definition missing name".to_string()))
3479}
3480
3481fn fragment_type_condition(fragment: &cst::FragmentDefinition) -> Result<String, WesleyError> {
3482    let type_condition = fragment.type_condition().ok_or_else(|| {
3483        operation_error_value("Fragment definition missing type condition".to_string())
3484    })?;
3485    named_type_name(
3486        type_condition.named_type(),
3487        "Fragment definition missing type condition",
3488    )
3489}
3490
3491fn validate_fragment_type_condition(
3492    parent_type: &str,
3493    condition_type: &str,
3494    schema: &SchemaIndex<'_>,
3495    context: &str,
3496) -> Result<(), WesleyError> {
3497    let parent_possible = schema.possible_runtime_types(parent_type)?;
3498    let condition_possible = schema.possible_runtime_types(condition_type)?;
3499
3500    if parent_possible.is_disjoint(&condition_possible) {
3501        return operation_error(format!(
3502            "Fragment '{context}' type condition '{condition_type}' cannot apply to parent type '{parent_type}'"
3503        ));
3504    }
3505
3506    Ok(())
3507}
3508
3509fn named_type_name(name: Option<cst::NamedType>, message: &str) -> Result<String, WesleyError> {
3510    name.and_then(|named_type| named_type.name())
3511        .map(|name| name.text().to_string())
3512        .ok_or_else(|| operation_error_value(message.to_string()))
3513}
3514
3515fn required_name(name: Option<cst::Name>, message: &str) -> Result<String, WesleyError> {
3516    name.map(|name| name.text().to_string())
3517        .ok_or_else(|| operation_error_value(message.to_string()))
3518}
3519
3520fn operation_error<T>(message: String) -> Result<T, WesleyError> {
3521    Err(operation_error_value(message))
3522}
3523
3524fn operation_error_value(message: String) -> WesleyError {
3525    WesleyError::LoweringError {
3526        message,
3527        area: "operation".to_string(),
3528    }
3529}