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