1use std::collections::{HashMap, HashSet};
40
41use tracing::warn;
42
43use crate::Result;
44use crate::google::api::FieldBehavior;
45use crate::parsing::types::BaseType;
46use crate::parsing::{CodeGenMetadata, HttpPattern, MessageField, MethodMetadata, ServiceInfo};
47use crate::utils::strings;
48
49pub(crate) use types::MethodPlanner;
50pub use types::{
51 BodyField, ChildLink, EmitShape, GenerationPlan, ManagedResource, MethodPlan, MethodShape,
52 PathParam, QueryParam, RequestParam, RequestType, ResourceHierarchy, ServicePlan,
53 SkippedMethod, extract_managed_resources, split_body_fields,
54};
55pub(crate) use types::{emit_shape, is_collection_method};
56
57mod types;
58
59pub fn analyze_metadata(metadata: &CodeGenMetadata) -> Result<GenerationPlan> {
72 let mut service_infos: Vec<_> = metadata.services.values().collect();
78 service_infos.sort_by(|a, b| a.name.cmp(&b.name));
79
80 let plans = service_infos
81 .into_iter()
82 .map(|service_info| analyze_service(metadata, service_info))
83 .collect::<Result<Vec<_>>>()?;
84
85 let global_map = build_global_parent_map(&plans, metadata);
88 let mut services = plans
89 .into_iter()
90 .map(|mut plan| {
91 plan.hierarchy = derive_ordered_hierarchy(&plan, &global_map, metadata)?;
92 plan.resource_accessor_params = derive_resource_accessor_params(&plan);
93 Ok(plan)
94 })
95 .collect::<Result<Vec<_>>>()?;
96
97 assign_direct_children(&mut services);
100
101 Ok(GenerationPlan {
102 services,
103 skipped_methods: Vec::new(),
104 })
105}
106
107type GlobalParentMap = HashMap<(String, String), String>;
112
113fn build_global_parent_map(plans: &[ServicePlan], metadata: &CodeGenMetadata) -> GlobalParentMap {
122 let mut map: GlobalParentMap = HashMap::new();
123
124 for plan in plans {
125 let managed_type = match plan.managed_resources.first() {
126 Some(r) => &r.descriptor.r#type,
127 None => continue,
128 };
129 if managed_type.is_empty() {
130 continue;
131 }
132
133 for method in &plan.methods {
134 if method.request_type != RequestType::List {
135 continue;
136 }
137 for param in method.query_parameters() {
138 let Some(ref rr) = param.resource_reference else {
139 continue;
140 };
141 if rr.child_type != *managed_type {
143 continue;
144 }
145 if let Some(parent_type) = resolve_parent_type_from_field(¶m.name, metadata) {
146 map.entry((parent_type, managed_type.clone()))
147 .or_insert_with(|| param.name.clone());
148 }
149 }
150 }
151 }
152
153 map
154}
155
156fn derive_ordered_hierarchy(
163 plan: &ServicePlan,
164 global_map: &GlobalParentMap,
165 metadata: &CodeGenMetadata,
166) -> Result<Vec<ResourceHierarchy>> {
167 let managed_type = match plan.managed_resources.first() {
168 Some(r) => r.descriptor.r#type.clone(),
169 None => return Ok(vec![]),
170 };
171
172 let mut ancestors: Vec<(usize, String, String)> = global_map
174 .iter()
175 .filter(|((_, child), _)| child == &managed_type)
176 .map(|((parent_type, _), field_name)| {
177 let depth = compute_depth(parent_type, global_map, &mut HashSet::new())?;
178 Ok((depth, parent_type.clone(), field_name.clone()))
179 })
180 .collect::<Result<Vec<_>>>()?;
181
182 if ancestors.is_empty() {
183 return Ok(vec![]);
184 }
185
186 ancestors.sort_by_key(|(depth, _, _)| *depth);
188
189 Ok(ancestors
190 .into_iter()
191 .map(|(_, parent_type, field_name)| {
192 let parent_singular = field_name.strip_suffix("_name").and_then(|stem| {
193 metadata
194 .resource_from_singular(stem)
195 .map(|_| stem.to_string())
196 });
197 ResourceHierarchy {
198 child_resource_type: managed_type.clone(),
199 parent_resource_type: parent_type,
200 parent_field_name: field_name,
201 parent_singular,
202 }
203 })
204 .collect())
205}
206
207fn compute_depth(
211 resource_type: &str,
212 map: &GlobalParentMap,
213 visited: &mut HashSet<String>,
214) -> Result<usize> {
215 if !visited.insert(resource_type.to_string()) {
216 return Err(crate::Error::Build(format!(
217 "Cycle detected in resource hierarchy at type: {resource_type}"
218 )));
219 }
220 let parent = map
221 .iter()
222 .find(|((_, child), _)| child == resource_type)
223 .map(|((parent, _), _)| parent.clone());
224
225 match parent {
226 None => Ok(0),
227 Some(parent_type) => Ok(1 + compute_depth(&parent_type, map, visited)?),
228 }
229}
230
231fn resolve_parent_type_from_field(field_name: &str, metadata: &CodeGenMetadata) -> Option<String> {
236 field_name
237 .strip_suffix("_name")
238 .and_then(|s| metadata.resource_from_singular(s))
239 .map(|rd| rd.r#type.clone())
240}
241
242pub(crate) fn is_standard_list_field(name: &str) -> bool {
249 matches!(
250 name,
251 "page_token" | "page_size" | "max_results" | "order_by" | "filter"
252 )
253}
254
255pub(crate) fn derive_accessor_parent_fields(plan: &ServicePlan) -> Vec<String> {
269 let Some(resource) = plan.managed_resources.first() else {
270 return vec![];
271 };
272
273 let resource_type = &resource.descriptor.r#type;
275 if !plan.hierarchy.is_empty() && !resource_type.is_empty() {
276 let annotation_parents: Vec<String> = plan
277 .hierarchy
278 .iter()
279 .filter(|h| &h.child_resource_type == resource_type)
280 .map(|h| h.parent_field_name.clone())
281 .collect();
282 if !annotation_parents.is_empty() {
283 return annotation_parents;
284 }
285 }
286
287 let has_explicit_name_field = !resource.descriptor.name_field.is_empty();
289
290 let get_path_param_name = plan
291 .methods
292 .iter()
293 .find(|m| m.request_type == RequestType::Get)
294 .and_then(|m| m.path_parameters().next().map(|p| p.name.clone()));
295
296 let parent_params: Vec<String> = plan
297 .methods
298 .iter()
299 .find(|m| m.request_type == RequestType::List)
300 .map(|m| {
301 m.parameters
302 .iter()
303 .filter(|p| !p.is_path_param() && !p.is_optional())
304 .filter(|p| matches!(p.field_type().base_type, BaseType::String))
305 .filter(|p| !is_standard_list_field(p.name()))
306 .map(|p| p.name().to_string())
307 .collect()
308 })
309 .unwrap_or_default();
310
311 let should_decompose = has_explicit_name_field
312 || (get_path_param_name.as_deref() == Some("name") && !parent_params.is_empty());
313
314 if should_decompose {
315 parent_params
316 } else {
317 vec![]
318 }
319}
320
321fn derive_resource_accessor_params(plan: &ServicePlan) -> Option<Vec<String>> {
328 let resource = plan.managed_resources.first()?;
329 let mut params = derive_accessor_parent_fields(plan);
330 params.push(format!("{}_name", resource.descriptor.singular));
331 Some(params)
332}
333
334fn assign_direct_children(services: &mut [ServicePlan]) {
340 let candidates: Vec<(Vec<String>, String, String)> = services
343 .iter()
344 .filter_map(|s| {
345 let params = s.resource_accessor_params.clone()?;
346 let singular = s.managed_resources.first()?.descriptor.singular.clone();
347 Some((params, s.base_path.clone(), singular))
348 })
349 .collect();
350
351 for plan in services.iter_mut() {
352 let Some(parent_params) = plan.resource_accessor_params.clone() else {
353 continue;
354 };
355 let mut children: Vec<ChildLink> = candidates
356 .iter()
357 .filter(|(child_params, child_base_path, _)| {
358 *child_base_path != plan.base_path
359 && child_params.len() == parent_params.len() + 1
360 && child_params[..parent_params.len()] == parent_params[..]
361 })
362 .map(
363 |(child_params, child_base_path, child_singular)| ChildLink {
364 child_singular: child_singular.clone(),
365 child_base_path: child_base_path.clone(),
366 child_accessor_params: child_params.clone(),
367 },
368 )
369 .collect();
370 children.sort_by(|a, b| a.child_singular.cmp(&b.child_singular));
371 plan.direct_children = children;
372 }
373}
374
375fn analyze_service(metadata: &CodeGenMetadata, info: &ServiceInfo) -> Result<ServicePlan> {
377 let handler_name = strings::service_to_handler_name(&info.name);
378 let base_path = strings::service_to_base_path(&info.name);
379
380 let mut method_plans = info
384 .methods
385 .iter()
386 .map(|method| analyze_method(metadata, method))
387 .collect::<Result<Vec<_>>>()?;
388
389 let managed_resources = types::extract_managed_resources(metadata, &method_plans);
390
391 let service_has_resource = !managed_resources.is_empty();
395 for plan in &mut method_plans {
396 plan.shape = types::method_shape(
397 &plan.request_type,
398 &plan.metadata.method_name,
399 service_has_resource,
400 );
401 }
402
403 Ok(ServicePlan {
404 service_name: info.name.clone(),
405 handler_name,
406 base_path,
407 package: info.package.clone(),
408 methods: method_plans,
409 managed_resources,
410 documentation: info.documentation.clone(),
411 hierarchy: vec![], resource_accessor_params: None, direct_children: vec![], })
415}
416
417pub(crate) fn analyze_method(
429 metadata: &CodeGenMetadata,
430 method: &MethodMetadata,
431) -> Result<MethodPlan> {
432 let Some(http_method) = method.http_method().map(|m| m.to_string()) else {
433 return analyze_routeless_method(metadata, method);
436 };
437
438 let planner = MethodPlanner::try_new(method, metadata)?;
439 let request_type = planner.request_type();
440 let has_response = planner.has_response();
441 let output_resource_type = planner.output_resource_type();
442 let http_pattern = planner.into_http_pattern();
443
444 let input_fields = metadata.get_message_fields(&method.input_type);
445 let (path_params, query_params, body_fields) = extract_request_fields(method, &input_fields)?;
446
447 let parameters = path_params
448 .into_iter()
449 .map(Into::into)
450 .chain(query_params.into_iter().map(Into::into))
451 .chain(body_fields.into_iter().map(Into::into))
452 .collect();
453
454 let has_request_body =
456 types::request_has_body(&request_type) || !method.http_rule.body.is_empty();
457 let needs_request_parts = types::request_needs_request_parts(&request_type);
458 let scoped_verb = types::scoped_verb(&request_type);
459
460 Ok(MethodPlan {
461 metadata: method.clone(),
462 handler_function_name: strings::operation_to_method_name(&method.method_name),
463 http_method,
464 parameters,
465 has_response,
466 request_type,
467 output_resource_type,
468 http_pattern,
469 has_request_body,
470 needs_request_parts,
471 scoped_verb,
472 shape: types::MethodShape::Unbound,
475 has_http_route: true,
476 })
477}
478
479fn analyze_routeless_method(
491 metadata: &CodeGenMetadata,
492 method: &MethodMetadata,
493) -> Result<MethodPlan> {
494 let input_fields = metadata.get_message_fields(&method.input_type);
495 let body_fields = routeless_body_fields(&input_fields);
496
497 let request_type =
502 RequestType::Custom(crate::google::api::http_rule::Pattern::Post(String::new()));
503 let has_response = !method.output_type.is_empty() && !method.output_type.ends_with("Empty");
504
505 Ok(MethodPlan {
506 metadata: method.clone(),
507 handler_function_name: strings::operation_to_method_name(&method.method_name),
508 http_method: String::new(),
509 parameters: body_fields.into_iter().map(Into::into).collect(),
510 has_response,
511 request_type,
512 output_resource_type: None,
513 http_pattern: HttpPattern::default(),
514 has_request_body: true,
516 needs_request_parts: false,
517 scoped_verb: None,
518 shape: types::MethodShape::Unbound,
519 has_http_route: false,
520 })
521}
522
523fn routeless_body_fields(input_fields: &[MessageField]) -> Vec<BodyField> {
530 input_fields
531 .iter()
532 .filter(|field| !field.field_behavior.contains(&FieldBehavior::OutputOnly))
533 .map(body_field)
534 .collect()
535}
536
537fn body_field(field: &MessageField) -> BodyField {
545 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
546 return BodyField {
547 name: field.name.clone(),
548 field_type: field.unified_type.clone().optional(),
549 repeated: false,
550 required: false,
551 oneof_variants: field.oneof_variants.clone(),
552 documentation: field.documentation.clone(),
553 };
554 }
555
556 let required = field.field_behavior.contains(&FieldBehavior::Required);
557 let needs_optional_marker = !required
558 && !field.unified_type.is_repeated
559 && matches!(field.unified_type.base_type, BaseType::Message(_));
560 let field_type = if needs_optional_marker {
561 field.unified_type.clone().optional()
562 } else {
563 field.unified_type.clone()
564 };
565 BodyField {
566 name: field.name.clone(),
567 field_type,
568 repeated: field.unified_type.is_repeated,
569 required,
570 oneof_variants: None,
571 documentation: field.documentation.clone(),
572 }
573}
574
575fn extract_request_fields(
585 method: &MethodMetadata,
586 input_fields: &[MessageField],
587) -> Result<(Vec<PathParam>, Vec<QueryParam>, Vec<BodyField>)> {
588 let mut path_params = Vec::new();
589 let mut query_params = Vec::new();
590 let mut body_fields = Vec::new();
591
592 let path_param_names = method.http_pattern.parameter_names();
593 let body_spec = method.http_rule.body.as_str();
594
595 let fields_by_name: HashMap<&str, &MessageField> =
597 input_fields.iter().map(|f| (f.name.as_str(), f)).collect();
598
599 let mut processed_fields = HashSet::new();
600
601 for path_param_name in path_param_names {
605 if let Some(field) = fields_by_name.get(path_param_name.as_str()) {
606 path_params.push(PathParam {
609 name: field.name.clone(),
610 field_type: field.unified_type.clone(),
611 documentation: field.documentation.clone(),
612 });
613 processed_fields.insert(field.name.as_str());
614 } else {
615 warn!(
617 "Method {}.{}: URL path placeholder `{}` does not match any request field; \
618 the param will be omitted from the path",
619 method.service_name, method.method_name, path_param_name
620 );
621 }
622 }
623
624 for field in input_fields {
626 let field_name = field.name.as_str();
627
628 if processed_fields.contains(field_name) {
629 continue;
630 }
631
632 if field.field_behavior.contains(&FieldBehavior::OutputOnly) {
634 processed_fields.insert(field_name);
635 continue;
636 }
637
638 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
640 body_fields.push(body_field(field));
641 processed_fields.insert(field_name);
642 continue;
643 }
644
645 let is_body = match body_spec {
646 "*" => true,
647 "" => false,
648 specific => specific == field_name,
649 };
650
651 if is_body {
652 body_fields.push(body_field(field));
653 } else {
654 query_params.push(QueryParam {
655 name: field.name.clone(),
656 json_name: field.json_name.clone(),
657 field_type: field.unified_type.clone(),
658 documentation: field.documentation.clone(),
659 resource_reference: field.resource_reference.clone(),
660 });
661 }
662 processed_fields.insert(field_name);
663 }
664
665 Ok((path_params, query_params, body_fields))
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::google::api::{HttpRule, ResourceDescriptor, http_rule::Pattern};
672 use crate::parsing::types::UnifiedType;
673 use crate::parsing::{CodeGenMetadata, HttpPattern, MessageInfo, MethodMetadata, ServiceInfo};
674 use std::collections::HashMap;
675
676 fn make_metadata_with_catalog() -> CodeGenMetadata {
677 let catalog_resource = ResourceDescriptor {
678 r#type: "example.io/Catalog".to_string(),
679 pattern: vec!["catalogs/{catalog}".to_string()],
680 name_field: "name".to_string(),
681 history: 0,
682 plural: "catalogs".to_string(),
683 singular: "catalog".to_string(),
684 style: vec![],
685 };
686 let catalog_info = MessageInfo {
687 name: "Catalog".to_string(),
688 fields: vec![],
689 resource_descriptor: Some(catalog_resource),
690 documentation: None,
691 };
692 let mut messages = HashMap::new();
693 messages.insert("Catalog".to_string(), catalog_info);
694 CodeGenMetadata {
695 messages,
696 ..Default::default()
697 }
698 }
699
700 fn make_get_method() -> MethodMetadata {
701 MethodMetadata {
702 service_name: "CatalogService".to_string(),
703 method_name: "GetCatalog".to_string(),
704 input_type: "GetCatalogRequest".to_string(),
705 output_type: "Catalog".to_string(),
706 operation: None,
707 http_rule: HttpRule {
708 selector: "".to_string(),
709 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
710 body: "".to_string(),
711 response_body: "".to_string(),
712 additional_bindings: vec![],
713 },
714 http_pattern: HttpPattern::parse("/catalogs/{name}"),
715 documentation: None,
716 }
717 }
718
719 #[test]
720 fn test_managed_resources_extraction() {
721 let metadata = make_metadata_with_catalog();
722 let service_info = ServiceInfo {
723 name: "CatalogService".to_string(),
724 package: "example.catalogs.v1".to_string(),
725 documentation: None,
726 methods: vec![make_get_method()],
727 };
728 let service_plan = analyze_service(&metadata, &service_info).unwrap();
729
730 assert_eq!(service_plan.managed_resources.len(), 1);
731 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
732 assert_eq!(
733 service_plan.managed_resources[0].descriptor.r#type,
734 "example.io/Catalog"
735 );
736 assert_eq!(
737 service_plan.managed_resources[0].descriptor.singular,
738 "catalog"
739 );
740 assert_eq!(
741 service_plan.managed_resources[0].descriptor.plural,
742 "catalogs"
743 );
744 }
745
746 #[test]
747 fn test_no_duplicate_managed_resources() {
748 let metadata = make_metadata_with_catalog();
749 let update_method = MethodMetadata {
750 service_name: "CatalogService".to_string(),
751 method_name: "UpdateCatalog".to_string(),
752 input_type: "UpdateCatalogRequest".to_string(),
753 output_type: "Catalog".to_string(),
754 operation: None,
755 http_rule: HttpRule {
756 selector: "".to_string(),
757 pattern: Some(Pattern::Patch("/catalogs/{name}".to_string())),
758 body: "*".to_string(),
759 response_body: "".to_string(),
760 additional_bindings: vec![],
761 },
762 http_pattern: HttpPattern::parse("/catalogs/{name}"),
763 documentation: None,
764 };
765 let service_info = ServiceInfo {
766 name: "CatalogService".to_string(),
767 package: "example.catalogs.v1".to_string(),
768 documentation: None,
769 methods: vec![make_get_method(), update_method],
770 };
771 let service_plan = analyze_service(&metadata, &service_info).unwrap();
772
773 assert_eq!(service_plan.managed_resources.len(), 1);
774 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
775 }
776
777 #[test]
778 fn test_analyze_method_missing_http_pattern_is_routeless() {
779 let metadata = CodeGenMetadata::default();
780 let method = MethodMetadata {
781 service_name: "SomeService".to_string(),
782 method_name: "SomeMethod".to_string(),
783 input_type: "".to_string(),
784 output_type: "".to_string(),
785 operation: None,
786 http_rule: HttpRule {
787 selector: "".to_string(),
788 pattern: None,
789 body: "".to_string(),
790 response_body: "".to_string(),
791 additional_bindings: vec![],
792 },
793 http_pattern: HttpPattern::parse(""),
794 documentation: None,
795 };
796 let plan = analyze_method(&metadata, &method).unwrap();
799 assert!(!plan.has_http_route);
800 assert!(plan.http_method.is_empty());
801 }
802
803 #[test]
804 fn test_routeless_method_partitions_body_by_field_behavior() {
805 use crate::parsing::MessageInfo;
806
807 let req = MessageInfo {
809 name: "DoThingRequest".to_string(),
810 fields: vec![
811 {
812 let mut f = make_string_field("name", false);
813 f.field_behavior = vec![FieldBehavior::Required];
814 f
815 },
816 make_string_field("comment", false),
817 ],
818 resource_descriptor: None,
819 documentation: None,
820 };
821 let mut messages = HashMap::new();
822 messages.insert("DoThingRequest".to_string(), req);
823 let metadata = CodeGenMetadata {
824 messages,
825 ..Default::default()
826 };
827
828 let method = MethodMetadata {
829 service_name: "ThingService".to_string(),
830 method_name: "DoThing".to_string(),
831 input_type: "DoThingRequest".to_string(),
832 output_type: "DoThingResponse".to_string(),
833 operation: None,
834 http_rule: HttpRule {
835 selector: "".to_string(),
836 pattern: None, body: "".to_string(),
838 response_body: "".to_string(),
839 additional_bindings: vec![],
840 },
841 http_pattern: HttpPattern::default(),
842 documentation: None,
843 };
844
845 let plan = analyze_method(&metadata, &method).unwrap();
846 assert!(!plan.has_http_route);
847
848 assert_eq!(plan.path_parameters().count(), 0);
850 assert_eq!(plan.query_parameters().count(), 0);
851 let body: Vec<_> = plan.body_fields().collect();
852 assert_eq!(body.len(), 2);
853
854 let name = body.iter().find(|b| b.name == "name").unwrap();
856 let comment = body.iter().find(|b| b.name == "comment").unwrap();
857 assert!(name.required, "REQUIRED field should be a constructor arg");
858 assert!(
859 !comment.required,
860 "unmarked field should be an optional with_* setter"
861 );
862 assert_eq!(plan.handler_function_name, "do_thing");
863 assert!(plan.has_response);
864 }
865
866 #[test]
867 fn test_routeless_method_drops_output_only_fields() {
868 use crate::parsing::MessageInfo;
869
870 let req = MessageInfo {
871 name: "MakeReq".to_string(),
872 fields: vec![make_string_field("input", false), {
873 let mut f = make_string_field("server_set", false);
874 f.field_behavior = vec![FieldBehavior::OutputOnly];
875 f
876 }],
877 resource_descriptor: None,
878 documentation: None,
879 };
880 let mut messages = HashMap::new();
881 messages.insert("MakeReq".to_string(), req);
882 let metadata = CodeGenMetadata {
883 messages,
884 ..Default::default()
885 };
886
887 let method = MethodMetadata {
888 service_name: "S".to_string(),
889 method_name: "Make".to_string(),
890 input_type: "MakeReq".to_string(),
891 output_type: "MakeResp".to_string(),
892 operation: None,
893 http_rule: HttpRule::default(),
894 http_pattern: HttpPattern::default(),
895 documentation: None,
896 };
897
898 let plan = analyze_method(&metadata, &method).unwrap();
899 let body: Vec<_> = plan.body_fields().collect();
900 assert_eq!(body.len(), 1, "OUTPUT_ONLY field must be dropped");
901 assert_eq!(body[0].name, "input");
902 }
903
904 fn make_string_field(name: &str, optional: bool) -> MessageField {
907 use crate::parsing::types::BaseType;
908 MessageField {
909 name: name.to_string(),
910 json_name: None,
911 unified_type: UnifiedType {
912 base_type: BaseType::String,
913 is_optional: optional,
914 is_repeated: false,
915 },
916 documentation: None,
917 oneof_variants: None,
918 field_behavior: vec![],
919 is_sensitive: false,
920 resource_reference: None,
921 }
922 }
923
924 fn make_repeated_field(name: &str) -> MessageField {
925 use crate::parsing::types::BaseType;
926 MessageField {
927 name: name.to_string(),
928 json_name: None,
929 unified_type: UnifiedType {
930 base_type: BaseType::String,
931 is_optional: false,
932 is_repeated: true,
933 },
934 documentation: None,
935 oneof_variants: None,
936 field_behavior: vec![],
937 is_sensitive: false,
938 resource_reference: None,
939 }
940 }
941
942 fn make_method_with_pattern(pattern: Pattern, body: &str, path: &str) -> MethodMetadata {
943 MethodMetadata {
944 service_name: "Svc".to_string(),
945 method_name: "Method".to_string(),
946 input_type: "".to_string(),
947 output_type: "".to_string(),
948 operation: None,
949 http_rule: HttpRule {
950 selector: "".to_string(),
951 pattern: Some(pattern),
952 body: body.to_string(),
953 response_body: "".to_string(),
954 additional_bindings: vec![],
955 },
956 http_pattern: HttpPattern::parse(path),
957 documentation: None,
958 }
959 }
960
961 #[test]
962 fn test_extract_path_params_in_url_order() {
963 let method =
964 make_method_with_pattern(Pattern::Get("/a/{x}/b/{y}".to_string()), "", "/a/{x}/b/{y}");
965 let fields = vec![make_string_field("y", false), make_string_field("x", false)];
966 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
967 assert_eq!(path.len(), 2);
969 assert_eq!(path[0].name, "x");
970 assert_eq!(path[1].name, "y");
971 assert!(query.is_empty());
972 assert!(body.is_empty());
973 }
974
975 #[test]
976 fn test_extract_body_wildcard() {
977 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
978 let fields = vec![
979 make_string_field("name", false),
980 make_string_field("description", true),
981 ];
982 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
983 assert!(path.is_empty());
984 assert!(query.is_empty());
985 assert_eq!(body.len(), 2);
986 }
987
988 #[test]
989 fn test_extract_specific_body_field() {
990 let method = make_method_with_pattern(
991 Pattern::Patch("/items/{name}".to_string()),
992 "payload",
993 "/items/{name}",
994 );
995 let fields = vec![
996 make_string_field("name", false), make_string_field("payload", false), make_string_field("extra", true), ];
1000 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1001 assert_eq!(path.len(), 1);
1002 assert_eq!(path[0].name, "name");
1003 assert_eq!(body.len(), 1);
1004 assert_eq!(body[0].name, "payload");
1005 assert_eq!(query.len(), 1);
1006 assert_eq!(query[0].name, "extra");
1007 }
1008
1009 #[test]
1010 fn test_extract_no_body_spec_all_query() {
1011 let method = make_method_with_pattern(Pattern::Get("/items".to_string()), "", "/items");
1012 let fields = vec![
1013 make_string_field("filter", true),
1014 make_string_field("page_size", true),
1015 ];
1016 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1017 assert!(path.is_empty());
1018 assert_eq!(query.len(), 2);
1019 assert!(body.is_empty());
1020 }
1021
1022 #[test]
1023 fn test_extract_repeated_field_becomes_body_with_repeated_flag() {
1024 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
1025 let fields = vec![make_repeated_field("tags")];
1026 let (_, _, body) = extract_request_fields(&method, &fields).unwrap();
1027 assert_eq!(body.len(), 1);
1028 assert!(body[0].repeated);
1029 }
1030
1031 fn make_three_level_metadata() -> (CodeGenMetadata, ServiceInfo, ServiceInfo, ServiceInfo) {
1040 use crate::google::api::ResourceReference;
1041 use crate::parsing::types::BaseType;
1042
1043 let mut messages = HashMap::new();
1044
1045 messages.insert(
1047 "Catalog".to_string(),
1048 MessageInfo {
1049 name: "Catalog".to_string(),
1050 fields: vec![],
1051 resource_descriptor: Some(ResourceDescriptor {
1052 r#type: "example.io/Catalog".to_string(),
1053 pattern: vec!["catalogs/{catalog}".to_string()],
1054 name_field: "name".to_string(),
1055 history: 0,
1056 plural: "catalogs".to_string(),
1057 singular: "catalog".to_string(),
1058 style: vec![],
1059 }),
1060 documentation: None,
1061 },
1062 );
1063
1064 messages.insert(
1066 "Schema".to_string(),
1067 MessageInfo {
1068 name: "Schema".to_string(),
1069 fields: vec![],
1070 resource_descriptor: Some(ResourceDescriptor {
1071 r#type: "example.io/Schema".to_string(),
1072 pattern: vec!["schemas/{schema}".to_string()],
1073 name_field: "name".to_string(),
1074 history: 0,
1075 plural: "schemas".to_string(),
1076 singular: "schema".to_string(),
1077 style: vec![],
1078 }),
1079 documentation: None,
1080 },
1081 );
1082
1083 messages.insert(
1085 "Table".to_string(),
1086 MessageInfo {
1087 name: "Table".to_string(),
1088 fields: vec![],
1089 resource_descriptor: Some(ResourceDescriptor {
1090 r#type: "example.io/Table".to_string(),
1091 pattern: vec!["tables/{table}".to_string()],
1092 name_field: "full_name".to_string(),
1093 history: 0,
1094 plural: "tables".to_string(),
1095 singular: "table".to_string(),
1096 style: vec![],
1097 }),
1098 documentation: None,
1099 },
1100 );
1101
1102 messages.insert(
1104 "ListCatalogsRequest".to_string(),
1105 MessageInfo {
1106 name: "ListCatalogsRequest".to_string(),
1107 fields: vec![],
1108 resource_descriptor: None,
1109 documentation: None,
1110 },
1111 );
1112
1113 messages.insert(
1115 "ListSchemasRequest".to_string(),
1116 MessageInfo {
1117 name: "ListSchemasRequest".to_string(),
1118 fields: vec![MessageField {
1119 name: "catalog_name".to_string(),
1120 json_name: None,
1121 unified_type: UnifiedType {
1122 base_type: BaseType::String,
1123 is_optional: false,
1124 is_repeated: false,
1125 },
1126 documentation: None,
1127 oneof_variants: None,
1128 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1129 is_sensitive: false,
1130 resource_reference: Some(ResourceReference {
1131 r#type: String::new(),
1132 child_type: "example.io/Schema".to_string(),
1133 }),
1134 }],
1135 resource_descriptor: None,
1136 documentation: None,
1137 },
1138 );
1139
1140 messages.insert(
1142 "ListTablesRequest".to_string(),
1143 MessageInfo {
1144 name: "ListTablesRequest".to_string(),
1145 fields: vec![
1146 MessageField {
1147 name: "catalog_name".to_string(),
1148 json_name: None,
1149 unified_type: UnifiedType {
1150 base_type: BaseType::String,
1151 is_optional: false,
1152 is_repeated: false,
1153 },
1154 documentation: None,
1155 oneof_variants: None,
1156 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1157 is_sensitive: false,
1158 resource_reference: Some(ResourceReference {
1159 r#type: String::new(),
1160 child_type: "example.io/Table".to_string(),
1161 }),
1162 },
1163 MessageField {
1164 name: "schema_name".to_string(),
1165 json_name: None,
1166 unified_type: UnifiedType {
1167 base_type: BaseType::String,
1168 is_optional: false,
1169 is_repeated: false,
1170 },
1171 documentation: None,
1172 oneof_variants: None,
1173 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1174 is_sensitive: false,
1175 resource_reference: Some(ResourceReference {
1176 r#type: String::new(),
1177 child_type: "example.io/Table".to_string(),
1178 }),
1179 },
1180 ],
1181 resource_descriptor: None,
1182 documentation: None,
1183 },
1184 );
1185
1186 let metadata = CodeGenMetadata {
1187 messages,
1188 ..Default::default()
1189 };
1190
1191 let catalog_svc = ServiceInfo {
1192 name: "CatalogService".to_string(),
1193 package: "example.v1".to_string(),
1194 documentation: None,
1195 methods: vec![
1196 MethodMetadata {
1197 service_name: "CatalogService".to_string(),
1198 method_name: "ListCatalogs".to_string(),
1199 input_type: "ListCatalogsRequest".to_string(),
1200 output_type: "ListCatalogsResponse".to_string(),
1201 operation: None,
1202 http_rule: HttpRule {
1203 selector: "".to_string(),
1204 pattern: Some(Pattern::Get("/catalogs".to_string())),
1205 body: "".to_string(),
1206 response_body: "".to_string(),
1207 additional_bindings: vec![],
1208 },
1209 http_pattern: HttpPattern::parse("/catalogs"),
1210 documentation: None,
1211 },
1212 MethodMetadata {
1213 service_name: "CatalogService".to_string(),
1214 method_name: "GetCatalog".to_string(),
1215 input_type: "GetCatalogRequest".to_string(),
1216 output_type: "Catalog".to_string(),
1217 operation: None,
1218 http_rule: HttpRule {
1219 selector: "".to_string(),
1220 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
1221 body: "".to_string(),
1222 response_body: "".to_string(),
1223 additional_bindings: vec![],
1224 },
1225 http_pattern: HttpPattern::parse("/catalogs/{name}"),
1226 documentation: None,
1227 },
1228 ],
1229 };
1230
1231 let schema_svc = ServiceInfo {
1232 name: "SchemaService".to_string(),
1233 package: "example.v1".to_string(),
1234 documentation: None,
1235 methods: vec![
1236 MethodMetadata {
1237 service_name: "SchemaService".to_string(),
1238 method_name: "ListSchemas".to_string(),
1239 input_type: "ListSchemasRequest".to_string(),
1240 output_type: "ListSchemasResponse".to_string(),
1241 operation: None,
1242 http_rule: HttpRule {
1243 selector: "".to_string(),
1244 pattern: Some(Pattern::Get("/schemas".to_string())),
1245 body: "".to_string(),
1246 response_body: "".to_string(),
1247 additional_bindings: vec![],
1248 },
1249 http_pattern: HttpPattern::parse("/schemas"),
1250 documentation: None,
1251 },
1252 MethodMetadata {
1253 service_name: "SchemaService".to_string(),
1254 method_name: "GetSchema".to_string(),
1255 input_type: "GetSchemaRequest".to_string(),
1256 output_type: "Schema".to_string(),
1257 operation: None,
1258 http_rule: HttpRule {
1259 selector: "".to_string(),
1260 pattern: Some(Pattern::Get("/schemas/{full_name}".to_string())),
1261 body: "".to_string(),
1262 response_body: "".to_string(),
1263 additional_bindings: vec![],
1264 },
1265 http_pattern: HttpPattern::parse("/schemas/{full_name}"),
1266 documentation: None,
1267 },
1268 ],
1269 };
1270
1271 let table_svc = ServiceInfo {
1272 name: "TableService".to_string(),
1273 package: "example.v1".to_string(),
1274 documentation: None,
1275 methods: vec![
1276 MethodMetadata {
1277 service_name: "TableService".to_string(),
1278 method_name: "ListTables".to_string(),
1279 input_type: "ListTablesRequest".to_string(),
1280 output_type: "ListTablesResponse".to_string(),
1281 operation: None,
1282 http_rule: HttpRule {
1283 selector: "".to_string(),
1284 pattern: Some(Pattern::Get("/tables".to_string())),
1285 body: "".to_string(),
1286 response_body: "".to_string(),
1287 additional_bindings: vec![],
1288 },
1289 http_pattern: HttpPattern::parse("/tables"),
1290 documentation: None,
1291 },
1292 MethodMetadata {
1293 service_name: "TableService".to_string(),
1294 method_name: "GetTable".to_string(),
1295 input_type: "GetTableRequest".to_string(),
1296 output_type: "Table".to_string(),
1297 operation: None,
1298 http_rule: HttpRule {
1299 selector: "".to_string(),
1300 pattern: Some(Pattern::Get("/tables/{full_name}".to_string())),
1301 body: "".to_string(),
1302 response_body: "".to_string(),
1303 additional_bindings: vec![],
1304 },
1305 http_pattern: HttpPattern::parse("/tables/{full_name}"),
1306 documentation: None,
1307 },
1308 ],
1309 };
1310
1311 (metadata, catalog_svc, schema_svc, table_svc)
1312 }
1313
1314 fn make_plans_from_fixture() -> (Vec<ServicePlan>, CodeGenMetadata) {
1316 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1317 let mut plans = Vec::new();
1318 for svc in &[&catalog_svc, &schema_svc, &table_svc] {
1319 let plan = analyze_service(&metadata, svc).unwrap();
1320 plans.push(plan);
1321 }
1322 (plans, metadata)
1323 }
1324
1325 #[test]
1326 fn test_build_global_parent_map() {
1327 let (plans, metadata) = make_plans_from_fixture();
1328 let map = build_global_parent_map(&plans, &metadata);
1329
1330 assert_eq!(
1332 map.get(&(
1333 "example.io/Catalog".to_string(),
1334 "example.io/Schema".to_string()
1335 )),
1336 Some(&"catalog_name".to_string()),
1337 "Catalog→Schema mapping missing"
1338 );
1339 assert_eq!(
1341 map.get(&(
1342 "example.io/Schema".to_string(),
1343 "example.io/Table".to_string()
1344 )),
1345 Some(&"schema_name".to_string()),
1346 "Schema→Table mapping missing"
1347 );
1348 assert_eq!(
1350 map.get(&(
1351 "example.io/Catalog".to_string(),
1352 "example.io/Table".to_string()
1353 )),
1354 Some(&"catalog_name".to_string()),
1355 "Catalog→Table flat-API mapping missing"
1356 );
1357 }
1358
1359 #[test]
1360 fn test_derive_ordered_hierarchy_three_levels() {
1361 let (plans, metadata) = make_plans_from_fixture();
1362 let map = build_global_parent_map(&plans, &metadata);
1363 let table_plan = plans
1364 .iter()
1365 .find(|p| p.service_name == "TableService")
1366 .unwrap();
1367 let hierarchy = derive_ordered_hierarchy(table_plan, &map, &metadata)
1368 .expect("no cycles in test fixture");
1369
1370 assert_eq!(hierarchy.len(), 2, "expected 2 ancestors for Table");
1371 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1373 assert_eq!(hierarchy[1].parent_field_name, "schema_name");
1374 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1375 assert_eq!(hierarchy[1].parent_resource_type, "example.io/Schema");
1376 assert_eq!(hierarchy[0].parent_singular, Some("catalog".to_string()));
1377 assert_eq!(hierarchy[1].parent_singular, Some("schema".to_string()));
1378 assert!(
1380 hierarchy
1381 .iter()
1382 .all(|h| h.child_resource_type == "example.io/Table")
1383 );
1384 }
1385
1386 #[test]
1387 fn test_derive_ordered_hierarchy_two_levels() {
1388 let (plans, metadata) = make_plans_from_fixture();
1389 let map = build_global_parent_map(&plans, &metadata);
1390 let schema_plan = plans
1391 .iter()
1392 .find(|p| p.service_name == "SchemaService")
1393 .unwrap();
1394 let hierarchy = derive_ordered_hierarchy(schema_plan, &map, &metadata)
1395 .expect("no cycles in test fixture");
1396
1397 assert_eq!(hierarchy.len(), 1);
1398 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1399 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1400 }
1401
1402 #[test]
1403 fn test_derive_ordered_hierarchy_root_resource() {
1404 let (plans, metadata) = make_plans_from_fixture();
1405 let map = build_global_parent_map(&plans, &metadata);
1406 let catalog_plan = plans
1407 .iter()
1408 .find(|p| p.service_name == "CatalogService")
1409 .unwrap();
1410 let hierarchy = derive_ordered_hierarchy(catalog_plan, &map, &metadata)
1411 .expect("no cycles in test fixture");
1412
1413 assert!(
1414 hierarchy.is_empty(),
1415 "root resource should have empty hierarchy"
1416 );
1417 }
1418
1419 #[test]
1420 fn test_compute_depth_cycle_guard() {
1421 let mut map: GlobalParentMap = HashMap::new();
1423 map.insert(("A".to_string(), "B".to_string()), "a_name".to_string());
1424 map.insert(("B".to_string(), "A".to_string()), "b_name".to_string());
1425
1426 assert!(compute_depth("A", &map, &mut HashSet::new()).is_err());
1427 assert!(compute_depth("B", &map, &mut HashSet::new()).is_err());
1428 }
1429
1430 #[test]
1431 fn test_full_analyze_metadata_hierarchy_ordering() {
1432 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1433 let mut services_map = HashMap::new();
1434 services_map.insert("CatalogService".to_string(), catalog_svc);
1435 services_map.insert("SchemaService".to_string(), schema_svc);
1436 services_map.insert("TableService".to_string(), table_svc);
1437 let full_metadata = CodeGenMetadata {
1438 messages: metadata.messages,
1439 services: services_map,
1440 ..Default::default()
1441 };
1442
1443 let plan = analyze_metadata(&full_metadata).unwrap();
1444 let table_svc_plan = plan
1445 .services
1446 .iter()
1447 .find(|s| s.service_name == "TableService")
1448 .expect("TableService plan not found");
1449
1450 assert_eq!(table_svc_plan.hierarchy.len(), 2);
1451 assert_eq!(
1452 table_svc_plan.hierarchy[0].parent_field_name,
1453 "catalog_name"
1454 );
1455 assert_eq!(table_svc_plan.hierarchy[1].parent_field_name, "schema_name");
1456 }
1457
1458 #[test]
1459 fn resource_accessor_params_nested_and_flat() {
1460 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1461 let mut services_map = HashMap::new();
1462 services_map.insert("CatalogService".to_string(), catalog_svc);
1463 services_map.insert("SchemaService".to_string(), schema_svc);
1464 services_map.insert("TableService".to_string(), table_svc);
1465 let full_metadata = CodeGenMetadata {
1466 messages: metadata.messages,
1467 services: services_map,
1468 ..Default::default()
1469 };
1470
1471 let plan = analyze_metadata(&full_metadata).unwrap();
1472 let params = |name: &str| {
1473 plan.services
1474 .iter()
1475 .find(|s| s.service_name == name)
1476 .unwrap()
1477 .resource_accessor_params
1478 .clone()
1479 };
1480
1481 assert_eq!(
1483 params("TableService"),
1484 Some(vec![
1485 "catalog_name".to_string(),
1486 "schema_name".to_string(),
1487 "table_name".to_string(),
1488 ])
1489 );
1490 assert_eq!(
1492 params("SchemaService"),
1493 Some(vec!["catalog_name".to_string(), "schema_name".to_string()])
1494 );
1495 assert_eq!(
1497 params("CatalogService"),
1498 Some(vec!["catalog_name".to_string()])
1499 );
1500 }
1501
1502 #[test]
1503 fn direct_children_via_accessor_param_prefix() {
1504 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1505 let mut services_map = HashMap::new();
1506 services_map.insert("CatalogService".to_string(), catalog_svc);
1507 services_map.insert("SchemaService".to_string(), schema_svc);
1508 services_map.insert("TableService".to_string(), table_svc);
1509 let full_metadata = CodeGenMetadata {
1510 messages: metadata.messages,
1511 services: services_map,
1512 ..Default::default()
1513 };
1514
1515 let plan = analyze_metadata(&full_metadata).unwrap();
1516 let children = |name: &str| -> Vec<String> {
1517 plan.services
1518 .iter()
1519 .find(|s| s.service_name == name)
1520 .unwrap()
1521 .direct_children
1522 .iter()
1523 .map(|c| c.child_singular.clone())
1524 .collect()
1525 };
1526
1527 assert_eq!(children("CatalogService"), vec!["schema".to_string()]);
1530 assert_eq!(children("SchemaService"), vec!["table".to_string()]);
1532 assert!(children("TableService").is_empty());
1534
1535 let catalog = plan
1537 .services
1538 .iter()
1539 .find(|s| s.service_name == "CatalogService")
1540 .unwrap();
1541 let schema_link = &catalog.direct_children[0];
1542 let schema_base_path = plan
1544 .services
1545 .iter()
1546 .find(|s| s.service_name == "SchemaService")
1547 .unwrap()
1548 .base_path
1549 .clone();
1550 assert_eq!(schema_link.child_base_path, schema_base_path);
1551 assert_eq!(
1552 schema_link.child_accessor_params,
1553 vec!["catalog_name".to_string(), "schema_name".to_string()]
1554 );
1555 }
1556
1557 #[test]
1563 fn standard_list_fields_recognized() {
1564 for f in [
1565 "page_token",
1566 "page_size",
1567 "max_results",
1568 "order_by",
1569 "filter",
1570 ] {
1571 assert!(
1572 is_standard_list_field(f),
1573 "{f} should be a standard list field"
1574 );
1575 }
1576 for f in ["name", "catalog_name", "schema_name"] {
1577 assert!(
1578 !is_standard_list_field(f),
1579 "{f} should NOT be a standard list field"
1580 );
1581 }
1582 }
1583}