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