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