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