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