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