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