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