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 =
469 types::request_has_body(&request_type) || !method.http_rule.body.is_empty();
470 let needs_request_parts = types::request_needs_request_parts(&request_type);
471 let scoped_verb = types::scoped_verb(&request_type);
472
473 Ok(MethodPlan {
474 metadata: method.clone(),
475 handler_function_name: method.method_name.to_case(Case::Snake),
476 http_method,
477 parameters,
478 has_response,
479 request_type,
480 output_resource_type,
481 http_pattern,
482 has_request_body,
483 needs_request_parts,
484 scoped_verb,
485 shape: types::MethodShape::Unbound,
488 has_http_route: true,
489 })
490}
491
492fn analyze_routeless_method(
504 metadata: &CodeGenMetadata,
505 method: &MethodMetadata,
506) -> Result<MethodPlan> {
507 let input_fields = metadata.get_message_fields(&method.input_type);
508 let body_fields = routeless_body_fields(&input_fields);
509
510 let request_type =
515 RequestType::Custom(crate::google::api::http_rule::Pattern::Post(String::new()));
516 let has_response = !method.output_type.is_empty() && !method.output_type.ends_with("Empty");
517
518 Ok(MethodPlan {
519 metadata: method.clone(),
520 handler_function_name: method.method_name.to_case(Case::Snake),
521 http_method: String::new(),
522 parameters: body_fields.into_iter().map(Into::into).collect(),
523 has_response,
524 request_type,
525 output_resource_type: None,
526 http_pattern: HttpPattern::default(),
527 has_request_body: true,
529 needs_request_parts: false,
530 scoped_verb: None,
531 shape: types::MethodShape::Unbound,
532 has_http_route: false,
533 })
534}
535
536fn routeless_body_fields(input_fields: &[MessageField]) -> Vec<BodyField> {
543 let mut body_fields = Vec::new();
544 for field in input_fields {
545 if field.field_behavior.contains(&FieldBehavior::OutputOnly) {
546 continue;
547 }
548
549 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
550 body_fields.push(BodyField {
551 name: field.name.clone(),
552 field_type: field.unified_type.clone().optional(),
553 repeated: false,
554 required: false,
555 oneof_variants: field.oneof_variants.clone(),
556 documentation: field.documentation.clone(),
557 });
558 continue;
559 }
560
561 let required = field.field_behavior.contains(&FieldBehavior::Required);
562 let needs_optional_marker = !required
565 && !field.unified_type.is_repeated
566 && matches!(
567 field.unified_type.base_type,
568 BaseType::Message(_) | BaseType::OneOf(_)
569 );
570 let field_type = if needs_optional_marker {
571 field.unified_type.clone().optional()
572 } else {
573 field.unified_type.clone()
574 };
575 body_fields.push(BodyField {
576 name: field.name.clone(),
577 field_type,
578 repeated: field.unified_type.is_repeated,
579 required,
580 oneof_variants: None,
581 documentation: field.documentation.clone(),
582 });
583 }
584 body_fields
585}
586
587fn extract_request_fields(
597 method: &MethodMetadata,
598 input_fields: &[MessageField],
599) -> Result<(Vec<PathParam>, Vec<QueryParam>, Vec<BodyField>)> {
600 let mut path_params = Vec::new();
601 let mut query_params = Vec::new();
602 let mut body_fields = Vec::new();
603
604 let path_param_names = method.http_pattern.parameter_names();
605 let body_spec = method.http_rule.body.as_str();
606
607 let fields_by_name: HashMap<&str, &MessageField> =
609 input_fields.iter().map(|f| (f.name.as_str(), f)).collect();
610
611 let mut processed_fields = HashSet::new();
612
613 for path_param_name in path_param_names {
617 if let Some(field) = fields_by_name.get(path_param_name.as_str()) {
618 path_params.push(PathParam {
621 name: field.name.clone(),
622 field_type: field.unified_type.clone(),
623 documentation: field.documentation.clone(),
624 });
625 processed_fields.insert(field.name.as_str());
626 } else {
627 warn!(
629 "Method {}.{}: URL path placeholder `{}` does not match any request field; \
630 the param will be omitted from the path",
631 method.service_name, method.method_name, path_param_name
632 );
633 }
634 }
635
636 for field in input_fields {
638 let field_name = field.name.as_str();
639
640 if processed_fields.contains(field_name) {
641 continue;
642 }
643
644 if field.field_behavior.contains(&FieldBehavior::OutputOnly) {
646 processed_fields.insert(field_name);
647 continue;
648 }
649
650 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
652 body_fields.push(BodyField {
653 name: field.name.clone(),
654 field_type: field.unified_type.clone().optional(),
655 repeated: false,
656 required: false,
657 oneof_variants: field.oneof_variants.clone(),
658 documentation: field.documentation.clone(),
659 });
660 processed_fields.insert(field_name);
661 continue;
662 }
663
664 let is_body = match body_spec {
665 "*" => true,
666 "" => false,
667 specific => specific == field_name,
668 };
669
670 if is_body {
671 let required = field.field_behavior.contains(&FieldBehavior::Required);
672 let needs_optional_marker = !required
677 && !field.unified_type.is_repeated
678 && matches!(
679 field.unified_type.base_type,
680 BaseType::Message(_) | BaseType::OneOf(_)
681 );
682 let field_type = if needs_optional_marker {
683 field.unified_type.clone().optional()
684 } else {
685 field.unified_type.clone()
686 };
687 body_fields.push(BodyField {
688 name: field.name.clone(),
689 field_type,
690 repeated: field.unified_type.is_repeated,
691 required,
692 oneof_variants: None,
693 documentation: field.documentation.clone(),
694 });
695 } else {
696 query_params.push(QueryParam {
697 name: field.name.clone(),
698 json_name: field.json_name.clone(),
699 field_type: field.unified_type.clone(),
700 documentation: field.documentation.clone(),
701 resource_reference: field.resource_reference.clone(),
702 });
703 }
704 processed_fields.insert(field_name);
705 }
706
707 Ok((path_params, query_params, body_fields))
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::google::api::{HttpRule, ResourceDescriptor, http_rule::Pattern};
714 use crate::parsing::types::UnifiedType;
715 use crate::parsing::{CodeGenMetadata, HttpPattern, MessageInfo, MethodMetadata, ServiceInfo};
716 use std::collections::HashMap;
717
718 fn make_metadata_with_catalog() -> CodeGenMetadata {
719 let catalog_resource = ResourceDescriptor {
720 r#type: "example.io/Catalog".to_string(),
721 pattern: vec!["catalogs/{catalog}".to_string()],
722 name_field: "name".to_string(),
723 history: 0,
724 plural: "catalogs".to_string(),
725 singular: "catalog".to_string(),
726 style: vec![],
727 };
728 let catalog_info = MessageInfo {
729 name: "Catalog".to_string(),
730 fields: vec![],
731 resource_descriptor: Some(catalog_resource),
732 documentation: None,
733 };
734 let mut messages = HashMap::new();
735 messages.insert("Catalog".to_string(), catalog_info);
736 CodeGenMetadata {
737 messages,
738 ..Default::default()
739 }
740 }
741
742 fn make_get_method() -> MethodMetadata {
743 MethodMetadata {
744 service_name: "CatalogService".to_string(),
745 method_name: "GetCatalog".to_string(),
746 input_type: "GetCatalogRequest".to_string(),
747 output_type: "Catalog".to_string(),
748 operation: None,
749 http_rule: HttpRule {
750 selector: "".to_string(),
751 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
752 body: "".to_string(),
753 response_body: "".to_string(),
754 additional_bindings: vec![],
755 },
756 http_pattern: HttpPattern::parse("/catalogs/{name}"),
757 documentation: None,
758 }
759 }
760
761 #[test]
762 fn test_managed_resources_extraction() {
763 let metadata = make_metadata_with_catalog();
764 let service_info = ServiceInfo {
765 name: "CatalogService".to_string(),
766 package: "example.catalogs.v1".to_string(),
767 documentation: None,
768 methods: vec![make_get_method()],
769 };
770 let (service_plan, skipped) = analyze_service(&metadata, &service_info).unwrap();
771
772 assert!(skipped.is_empty());
773 assert_eq!(service_plan.managed_resources.len(), 1);
774 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
775 assert_eq!(
776 service_plan.managed_resources[0].descriptor.r#type,
777 "example.io/Catalog"
778 );
779 assert_eq!(
780 service_plan.managed_resources[0].descriptor.singular,
781 "catalog"
782 );
783 assert_eq!(
784 service_plan.managed_resources[0].descriptor.plural,
785 "catalogs"
786 );
787 }
788
789 #[test]
790 fn test_no_duplicate_managed_resources() {
791 let metadata = make_metadata_with_catalog();
792 let update_method = MethodMetadata {
793 service_name: "CatalogService".to_string(),
794 method_name: "UpdateCatalog".to_string(),
795 input_type: "UpdateCatalogRequest".to_string(),
796 output_type: "Catalog".to_string(),
797 operation: None,
798 http_rule: HttpRule {
799 selector: "".to_string(),
800 pattern: Some(Pattern::Patch("/catalogs/{name}".to_string())),
801 body: "*".to_string(),
802 response_body: "".to_string(),
803 additional_bindings: vec![],
804 },
805 http_pattern: HttpPattern::parse("/catalogs/{name}"),
806 documentation: None,
807 };
808 let service_info = ServiceInfo {
809 name: "CatalogService".to_string(),
810 package: "example.catalogs.v1".to_string(),
811 documentation: None,
812 methods: vec![make_get_method(), update_method],
813 };
814 let (service_plan, _skipped) = analyze_service(&metadata, &service_info).unwrap();
815
816 assert_eq!(service_plan.managed_resources.len(), 1);
817 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
818 }
819
820 #[test]
821 fn test_analyze_method_missing_http_pattern_is_routeless() {
822 let metadata = CodeGenMetadata::default();
823 let method = MethodMetadata {
824 service_name: "SomeService".to_string(),
825 method_name: "SomeMethod".to_string(),
826 input_type: "".to_string(),
827 output_type: "".to_string(),
828 operation: None,
829 http_rule: HttpRule {
830 selector: "".to_string(),
831 pattern: None,
832 body: "".to_string(),
833 response_body: "".to_string(),
834 additional_bindings: vec![],
835 },
836 http_pattern: HttpPattern::parse(""),
837 documentation: None,
838 };
839 let plan = analyze_method(&metadata, &method).unwrap();
842 assert!(!plan.has_http_route);
843 assert!(plan.http_method.is_empty());
844 }
845
846 #[test]
847 fn test_routeless_method_partitions_body_by_field_behavior() {
848 use crate::parsing::MessageInfo;
849
850 let req = MessageInfo {
852 name: "DoThingRequest".to_string(),
853 fields: vec![
854 {
855 let mut f = make_string_field("name", false);
856 f.field_behavior = vec![FieldBehavior::Required];
857 f
858 },
859 make_string_field("comment", false),
860 ],
861 resource_descriptor: None,
862 documentation: None,
863 };
864 let mut messages = HashMap::new();
865 messages.insert("DoThingRequest".to_string(), req);
866 let metadata = CodeGenMetadata {
867 messages,
868 ..Default::default()
869 };
870
871 let method = MethodMetadata {
872 service_name: "ThingService".to_string(),
873 method_name: "DoThing".to_string(),
874 input_type: "DoThingRequest".to_string(),
875 output_type: "DoThingResponse".to_string(),
876 operation: None,
877 http_rule: HttpRule {
878 selector: "".to_string(),
879 pattern: None, body: "".to_string(),
881 response_body: "".to_string(),
882 additional_bindings: vec![],
883 },
884 http_pattern: HttpPattern::default(),
885 documentation: None,
886 };
887
888 let plan = analyze_method(&metadata, &method).unwrap();
889 assert!(!plan.has_http_route);
890
891 assert_eq!(plan.path_parameters().count(), 0);
893 assert_eq!(plan.query_parameters().count(), 0);
894 let body: Vec<_> = plan.body_fields().collect();
895 assert_eq!(body.len(), 2);
896
897 let name = body.iter().find(|b| b.name == "name").unwrap();
899 let comment = body.iter().find(|b| b.name == "comment").unwrap();
900 assert!(name.required, "REQUIRED field should be a constructor arg");
901 assert!(
902 !comment.required,
903 "unmarked field should be an optional with_* setter"
904 );
905 assert_eq!(plan.handler_function_name, "do_thing");
906 assert!(plan.has_response);
907 }
908
909 #[test]
910 fn test_routeless_method_drops_output_only_fields() {
911 use crate::parsing::MessageInfo;
912
913 let req = MessageInfo {
914 name: "MakeReq".to_string(),
915 fields: vec![make_string_field("input", false), {
916 let mut f = make_string_field("server_set", false);
917 f.field_behavior = vec![FieldBehavior::OutputOnly];
918 f
919 }],
920 resource_descriptor: None,
921 documentation: None,
922 };
923 let mut messages = HashMap::new();
924 messages.insert("MakeReq".to_string(), req);
925 let metadata = CodeGenMetadata {
926 messages,
927 ..Default::default()
928 };
929
930 let method = MethodMetadata {
931 service_name: "S".to_string(),
932 method_name: "Make".to_string(),
933 input_type: "MakeReq".to_string(),
934 output_type: "MakeResp".to_string(),
935 operation: None,
936 http_rule: HttpRule::default(),
937 http_pattern: HttpPattern::default(),
938 documentation: None,
939 };
940
941 let plan = analyze_method(&metadata, &method).unwrap();
942 let body: Vec<_> = plan.body_fields().collect();
943 assert_eq!(body.len(), 1, "OUTPUT_ONLY field must be dropped");
944 assert_eq!(body[0].name, "input");
945 }
946
947 fn make_string_field(name: &str, optional: bool) -> MessageField {
950 use crate::parsing::types::BaseType;
951 MessageField {
952 name: name.to_string(),
953 json_name: None,
954 unified_type: UnifiedType {
955 base_type: BaseType::String,
956 is_optional: optional,
957 is_repeated: false,
958 },
959 documentation: None,
960 oneof_variants: None,
961 field_behavior: vec![],
962 is_sensitive: false,
963 resource_reference: None,
964 }
965 }
966
967 fn make_repeated_field(name: &str) -> MessageField {
968 use crate::parsing::types::BaseType;
969 MessageField {
970 name: name.to_string(),
971 json_name: None,
972 unified_type: UnifiedType {
973 base_type: BaseType::String,
974 is_optional: false,
975 is_repeated: true,
976 },
977 documentation: None,
978 oneof_variants: None,
979 field_behavior: vec![],
980 is_sensitive: false,
981 resource_reference: None,
982 }
983 }
984
985 fn make_method_with_pattern(pattern: Pattern, body: &str, path: &str) -> MethodMetadata {
986 MethodMetadata {
987 service_name: "Svc".to_string(),
988 method_name: "Method".to_string(),
989 input_type: "".to_string(),
990 output_type: "".to_string(),
991 operation: None,
992 http_rule: HttpRule {
993 selector: "".to_string(),
994 pattern: Some(pattern),
995 body: body.to_string(),
996 response_body: "".to_string(),
997 additional_bindings: vec![],
998 },
999 http_pattern: HttpPattern::parse(path),
1000 documentation: None,
1001 }
1002 }
1003
1004 #[test]
1005 fn test_extract_path_params_in_url_order() {
1006 let method =
1007 make_method_with_pattern(Pattern::Get("/a/{x}/b/{y}".to_string()), "", "/a/{x}/b/{y}");
1008 let fields = vec![make_string_field("y", false), make_string_field("x", false)];
1009 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1010 assert_eq!(path.len(), 2);
1012 assert_eq!(path[0].name, "x");
1013 assert_eq!(path[1].name, "y");
1014 assert!(query.is_empty());
1015 assert!(body.is_empty());
1016 }
1017
1018 #[test]
1019 fn test_extract_body_wildcard() {
1020 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
1021 let fields = vec![
1022 make_string_field("name", false),
1023 make_string_field("description", true),
1024 ];
1025 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1026 assert!(path.is_empty());
1027 assert!(query.is_empty());
1028 assert_eq!(body.len(), 2);
1029 }
1030
1031 #[test]
1032 fn test_extract_specific_body_field() {
1033 let method = make_method_with_pattern(
1034 Pattern::Patch("/items/{name}".to_string()),
1035 "payload",
1036 "/items/{name}",
1037 );
1038 let fields = vec![
1039 make_string_field("name", false), make_string_field("payload", false), make_string_field("extra", true), ];
1043 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1044 assert_eq!(path.len(), 1);
1045 assert_eq!(path[0].name, "name");
1046 assert_eq!(body.len(), 1);
1047 assert_eq!(body[0].name, "payload");
1048 assert_eq!(query.len(), 1);
1049 assert_eq!(query[0].name, "extra");
1050 }
1051
1052 #[test]
1053 fn test_extract_no_body_spec_all_query() {
1054 let method = make_method_with_pattern(Pattern::Get("/items".to_string()), "", "/items");
1055 let fields = vec![
1056 make_string_field("filter", true),
1057 make_string_field("page_size", true),
1058 ];
1059 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1060 assert!(path.is_empty());
1061 assert_eq!(query.len(), 2);
1062 assert!(body.is_empty());
1063 }
1064
1065 #[test]
1066 fn test_extract_repeated_field_becomes_body_with_repeated_flag() {
1067 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
1068 let fields = vec![make_repeated_field("tags")];
1069 let (_, _, body) = extract_request_fields(&method, &fields).unwrap();
1070 assert_eq!(body.len(), 1);
1071 assert!(body[0].repeated);
1072 }
1073
1074 fn make_three_level_metadata() -> (CodeGenMetadata, ServiceInfo, ServiceInfo, ServiceInfo) {
1083 use crate::google::api::ResourceReference;
1084 use crate::parsing::types::BaseType;
1085
1086 let mut messages = HashMap::new();
1087
1088 messages.insert(
1090 "Catalog".to_string(),
1091 MessageInfo {
1092 name: "Catalog".to_string(),
1093 fields: vec![],
1094 resource_descriptor: Some(ResourceDescriptor {
1095 r#type: "example.io/Catalog".to_string(),
1096 pattern: vec!["catalogs/{catalog}".to_string()],
1097 name_field: "name".to_string(),
1098 history: 0,
1099 plural: "catalogs".to_string(),
1100 singular: "catalog".to_string(),
1101 style: vec![],
1102 }),
1103 documentation: None,
1104 },
1105 );
1106
1107 messages.insert(
1109 "Schema".to_string(),
1110 MessageInfo {
1111 name: "Schema".to_string(),
1112 fields: vec![],
1113 resource_descriptor: Some(ResourceDescriptor {
1114 r#type: "example.io/Schema".to_string(),
1115 pattern: vec!["schemas/{schema}".to_string()],
1116 name_field: "name".to_string(),
1117 history: 0,
1118 plural: "schemas".to_string(),
1119 singular: "schema".to_string(),
1120 style: vec![],
1121 }),
1122 documentation: None,
1123 },
1124 );
1125
1126 messages.insert(
1128 "Table".to_string(),
1129 MessageInfo {
1130 name: "Table".to_string(),
1131 fields: vec![],
1132 resource_descriptor: Some(ResourceDescriptor {
1133 r#type: "example.io/Table".to_string(),
1134 pattern: vec!["tables/{table}".to_string()],
1135 name_field: "full_name".to_string(),
1136 history: 0,
1137 plural: "tables".to_string(),
1138 singular: "table".to_string(),
1139 style: vec![],
1140 }),
1141 documentation: None,
1142 },
1143 );
1144
1145 messages.insert(
1147 "ListCatalogsRequest".to_string(),
1148 MessageInfo {
1149 name: "ListCatalogsRequest".to_string(),
1150 fields: vec![],
1151 resource_descriptor: None,
1152 documentation: None,
1153 },
1154 );
1155
1156 messages.insert(
1158 "ListSchemasRequest".to_string(),
1159 MessageInfo {
1160 name: "ListSchemasRequest".to_string(),
1161 fields: vec![MessageField {
1162 name: "catalog_name".to_string(),
1163 json_name: None,
1164 unified_type: UnifiedType {
1165 base_type: BaseType::String,
1166 is_optional: false,
1167 is_repeated: false,
1168 },
1169 documentation: None,
1170 oneof_variants: None,
1171 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1172 is_sensitive: false,
1173 resource_reference: Some(ResourceReference {
1174 r#type: String::new(),
1175 child_type: "example.io/Schema".to_string(),
1176 }),
1177 }],
1178 resource_descriptor: None,
1179 documentation: None,
1180 },
1181 );
1182
1183 messages.insert(
1185 "ListTablesRequest".to_string(),
1186 MessageInfo {
1187 name: "ListTablesRequest".to_string(),
1188 fields: vec![
1189 MessageField {
1190 name: "catalog_name".to_string(),
1191 json_name: None,
1192 unified_type: UnifiedType {
1193 base_type: BaseType::String,
1194 is_optional: false,
1195 is_repeated: false,
1196 },
1197 documentation: None,
1198 oneof_variants: None,
1199 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1200 is_sensitive: false,
1201 resource_reference: Some(ResourceReference {
1202 r#type: String::new(),
1203 child_type: "example.io/Table".to_string(),
1204 }),
1205 },
1206 MessageField {
1207 name: "schema_name".to_string(),
1208 json_name: None,
1209 unified_type: UnifiedType {
1210 base_type: BaseType::String,
1211 is_optional: false,
1212 is_repeated: false,
1213 },
1214 documentation: None,
1215 oneof_variants: None,
1216 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1217 is_sensitive: false,
1218 resource_reference: Some(ResourceReference {
1219 r#type: String::new(),
1220 child_type: "example.io/Table".to_string(),
1221 }),
1222 },
1223 ],
1224 resource_descriptor: None,
1225 documentation: None,
1226 },
1227 );
1228
1229 let metadata = CodeGenMetadata {
1230 messages,
1231 ..Default::default()
1232 };
1233
1234 let catalog_svc = ServiceInfo {
1235 name: "CatalogService".to_string(),
1236 package: "example.v1".to_string(),
1237 documentation: None,
1238 methods: vec![
1239 MethodMetadata {
1240 service_name: "CatalogService".to_string(),
1241 method_name: "ListCatalogs".to_string(),
1242 input_type: "ListCatalogsRequest".to_string(),
1243 output_type: "ListCatalogsResponse".to_string(),
1244 operation: None,
1245 http_rule: HttpRule {
1246 selector: "".to_string(),
1247 pattern: Some(Pattern::Get("/catalogs".to_string())),
1248 body: "".to_string(),
1249 response_body: "".to_string(),
1250 additional_bindings: vec![],
1251 },
1252 http_pattern: HttpPattern::parse("/catalogs"),
1253 documentation: None,
1254 },
1255 MethodMetadata {
1256 service_name: "CatalogService".to_string(),
1257 method_name: "GetCatalog".to_string(),
1258 input_type: "GetCatalogRequest".to_string(),
1259 output_type: "Catalog".to_string(),
1260 operation: None,
1261 http_rule: HttpRule {
1262 selector: "".to_string(),
1263 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
1264 body: "".to_string(),
1265 response_body: "".to_string(),
1266 additional_bindings: vec![],
1267 },
1268 http_pattern: HttpPattern::parse("/catalogs/{name}"),
1269 documentation: None,
1270 },
1271 ],
1272 };
1273
1274 let schema_svc = ServiceInfo {
1275 name: "SchemaService".to_string(),
1276 package: "example.v1".to_string(),
1277 documentation: None,
1278 methods: vec![
1279 MethodMetadata {
1280 service_name: "SchemaService".to_string(),
1281 method_name: "ListSchemas".to_string(),
1282 input_type: "ListSchemasRequest".to_string(),
1283 output_type: "ListSchemasResponse".to_string(),
1284 operation: None,
1285 http_rule: HttpRule {
1286 selector: "".to_string(),
1287 pattern: Some(Pattern::Get("/schemas".to_string())),
1288 body: "".to_string(),
1289 response_body: "".to_string(),
1290 additional_bindings: vec![],
1291 },
1292 http_pattern: HttpPattern::parse("/schemas"),
1293 documentation: None,
1294 },
1295 MethodMetadata {
1296 service_name: "SchemaService".to_string(),
1297 method_name: "GetSchema".to_string(),
1298 input_type: "GetSchemaRequest".to_string(),
1299 output_type: "Schema".to_string(),
1300 operation: None,
1301 http_rule: HttpRule {
1302 selector: "".to_string(),
1303 pattern: Some(Pattern::Get("/schemas/{full_name}".to_string())),
1304 body: "".to_string(),
1305 response_body: "".to_string(),
1306 additional_bindings: vec![],
1307 },
1308 http_pattern: HttpPattern::parse("/schemas/{full_name}"),
1309 documentation: None,
1310 },
1311 ],
1312 };
1313
1314 let table_svc = ServiceInfo {
1315 name: "TableService".to_string(),
1316 package: "example.v1".to_string(),
1317 documentation: None,
1318 methods: vec![
1319 MethodMetadata {
1320 service_name: "TableService".to_string(),
1321 method_name: "ListTables".to_string(),
1322 input_type: "ListTablesRequest".to_string(),
1323 output_type: "ListTablesResponse".to_string(),
1324 operation: None,
1325 http_rule: HttpRule {
1326 selector: "".to_string(),
1327 pattern: Some(Pattern::Get("/tables".to_string())),
1328 body: "".to_string(),
1329 response_body: "".to_string(),
1330 additional_bindings: vec![],
1331 },
1332 http_pattern: HttpPattern::parse("/tables"),
1333 documentation: None,
1334 },
1335 MethodMetadata {
1336 service_name: "TableService".to_string(),
1337 method_name: "GetTable".to_string(),
1338 input_type: "GetTableRequest".to_string(),
1339 output_type: "Table".to_string(),
1340 operation: None,
1341 http_rule: HttpRule {
1342 selector: "".to_string(),
1343 pattern: Some(Pattern::Get("/tables/{full_name}".to_string())),
1344 body: "".to_string(),
1345 response_body: "".to_string(),
1346 additional_bindings: vec![],
1347 },
1348 http_pattern: HttpPattern::parse("/tables/{full_name}"),
1349 documentation: None,
1350 },
1351 ],
1352 };
1353
1354 (metadata, catalog_svc, schema_svc, table_svc)
1355 }
1356
1357 fn make_plans_from_fixture() -> (Vec<ServicePlan>, CodeGenMetadata) {
1359 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1360 let mut plans = Vec::new();
1361 for svc in &[&catalog_svc, &schema_svc, &table_svc] {
1362 let (plan, _) = analyze_service(&metadata, svc).unwrap();
1363 plans.push(plan);
1364 }
1365 (plans, metadata)
1366 }
1367
1368 #[test]
1369 fn test_build_global_parent_map() {
1370 let (plans, metadata) = make_plans_from_fixture();
1371 let map = build_global_parent_map(&plans, &metadata);
1372
1373 assert_eq!(
1375 map.get(&(
1376 "example.io/Catalog".to_string(),
1377 "example.io/Schema".to_string()
1378 )),
1379 Some(&"catalog_name".to_string()),
1380 "Catalog→Schema mapping missing"
1381 );
1382 assert_eq!(
1384 map.get(&(
1385 "example.io/Schema".to_string(),
1386 "example.io/Table".to_string()
1387 )),
1388 Some(&"schema_name".to_string()),
1389 "Schema→Table mapping missing"
1390 );
1391 assert_eq!(
1393 map.get(&(
1394 "example.io/Catalog".to_string(),
1395 "example.io/Table".to_string()
1396 )),
1397 Some(&"catalog_name".to_string()),
1398 "Catalog→Table flat-API mapping missing"
1399 );
1400 }
1401
1402 #[test]
1403 fn test_derive_ordered_hierarchy_three_levels() {
1404 let (plans, metadata) = make_plans_from_fixture();
1405 let map = build_global_parent_map(&plans, &metadata);
1406 let table_plan = plans
1407 .iter()
1408 .find(|p| p.service_name == "TableService")
1409 .unwrap();
1410 let hierarchy = derive_ordered_hierarchy(table_plan, &map, &metadata)
1411 .expect("no cycles in test fixture");
1412
1413 assert_eq!(hierarchy.len(), 2, "expected 2 ancestors for Table");
1414 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1416 assert_eq!(hierarchy[1].parent_field_name, "schema_name");
1417 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1418 assert_eq!(hierarchy[1].parent_resource_type, "example.io/Schema");
1419 assert_eq!(hierarchy[0].parent_singular, Some("catalog".to_string()));
1420 assert_eq!(hierarchy[1].parent_singular, Some("schema".to_string()));
1421 assert!(
1423 hierarchy
1424 .iter()
1425 .all(|h| h.child_resource_type == "example.io/Table")
1426 );
1427 }
1428
1429 #[test]
1430 fn test_derive_ordered_hierarchy_two_levels() {
1431 let (plans, metadata) = make_plans_from_fixture();
1432 let map = build_global_parent_map(&plans, &metadata);
1433 let schema_plan = plans
1434 .iter()
1435 .find(|p| p.service_name == "SchemaService")
1436 .unwrap();
1437 let hierarchy = derive_ordered_hierarchy(schema_plan, &map, &metadata)
1438 .expect("no cycles in test fixture");
1439
1440 assert_eq!(hierarchy.len(), 1);
1441 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1442 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1443 }
1444
1445 #[test]
1446 fn test_derive_ordered_hierarchy_root_resource() {
1447 let (plans, metadata) = make_plans_from_fixture();
1448 let map = build_global_parent_map(&plans, &metadata);
1449 let catalog_plan = plans
1450 .iter()
1451 .find(|p| p.service_name == "CatalogService")
1452 .unwrap();
1453 let hierarchy = derive_ordered_hierarchy(catalog_plan, &map, &metadata)
1454 .expect("no cycles in test fixture");
1455
1456 assert!(
1457 hierarchy.is_empty(),
1458 "root resource should have empty hierarchy"
1459 );
1460 }
1461
1462 #[test]
1463 fn test_compute_depth_cycle_guard() {
1464 let mut map: GlobalParentMap = HashMap::new();
1466 map.insert(("A".to_string(), "B".to_string()), "a_name".to_string());
1467 map.insert(("B".to_string(), "A".to_string()), "b_name".to_string());
1468
1469 assert!(compute_depth("A", &map, &mut HashSet::new()).is_err());
1470 assert!(compute_depth("B", &map, &mut HashSet::new()).is_err());
1471 }
1472
1473 #[test]
1474 fn test_full_analyze_metadata_hierarchy_ordering() {
1475 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1476 let mut services_map = HashMap::new();
1477 services_map.insert("CatalogService".to_string(), catalog_svc);
1478 services_map.insert("SchemaService".to_string(), schema_svc);
1479 services_map.insert("TableService".to_string(), table_svc);
1480 let full_metadata = CodeGenMetadata {
1481 messages: metadata.messages,
1482 services: services_map,
1483 ..Default::default()
1484 };
1485
1486 let plan = analyze_metadata(&full_metadata).unwrap();
1487 let table_svc_plan = plan
1488 .services
1489 .iter()
1490 .find(|s| s.service_name == "TableService")
1491 .expect("TableService plan not found");
1492
1493 assert_eq!(table_svc_plan.hierarchy.len(), 2);
1494 assert_eq!(
1495 table_svc_plan.hierarchy[0].parent_field_name,
1496 "catalog_name"
1497 );
1498 assert_eq!(table_svc_plan.hierarchy[1].parent_field_name, "schema_name");
1499 }
1500
1501 #[test]
1502 fn resource_accessor_params_nested_and_flat() {
1503 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1504 let mut services_map = HashMap::new();
1505 services_map.insert("CatalogService".to_string(), catalog_svc);
1506 services_map.insert("SchemaService".to_string(), schema_svc);
1507 services_map.insert("TableService".to_string(), table_svc);
1508 let full_metadata = CodeGenMetadata {
1509 messages: metadata.messages,
1510 services: services_map,
1511 ..Default::default()
1512 };
1513
1514 let plan = analyze_metadata(&full_metadata).unwrap();
1515 let params = |name: &str| {
1516 plan.services
1517 .iter()
1518 .find(|s| s.service_name == name)
1519 .unwrap()
1520 .resource_accessor_params
1521 .clone()
1522 };
1523
1524 assert_eq!(
1526 params("TableService"),
1527 Some(vec![
1528 "catalog_name".to_string(),
1529 "schema_name".to_string(),
1530 "table_name".to_string(),
1531 ])
1532 );
1533 assert_eq!(
1535 params("SchemaService"),
1536 Some(vec!["catalog_name".to_string(), "schema_name".to_string()])
1537 );
1538 assert_eq!(
1540 params("CatalogService"),
1541 Some(vec!["catalog_name".to_string()])
1542 );
1543 }
1544
1545 #[test]
1546 fn direct_children_via_accessor_param_prefix() {
1547 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1548 let mut services_map = HashMap::new();
1549 services_map.insert("CatalogService".to_string(), catalog_svc);
1550 services_map.insert("SchemaService".to_string(), schema_svc);
1551 services_map.insert("TableService".to_string(), table_svc);
1552 let full_metadata = CodeGenMetadata {
1553 messages: metadata.messages,
1554 services: services_map,
1555 ..Default::default()
1556 };
1557
1558 let plan = analyze_metadata(&full_metadata).unwrap();
1559 let children = |name: &str| -> Vec<String> {
1560 plan.services
1561 .iter()
1562 .find(|s| s.service_name == name)
1563 .unwrap()
1564 .direct_children
1565 .iter()
1566 .map(|c| c.child_singular.clone())
1567 .collect()
1568 };
1569
1570 assert_eq!(children("CatalogService"), vec!["schema".to_string()]);
1573 assert_eq!(children("SchemaService"), vec!["table".to_string()]);
1575 assert!(children("TableService").is_empty());
1577
1578 let catalog = plan
1580 .services
1581 .iter()
1582 .find(|s| s.service_name == "CatalogService")
1583 .unwrap();
1584 let schema_link = &catalog.direct_children[0];
1585 let schema_base_path = plan
1587 .services
1588 .iter()
1589 .find(|s| s.service_name == "SchemaService")
1590 .unwrap()
1591 .base_path
1592 .clone();
1593 assert_eq!(schema_link.child_base_path, schema_base_path);
1594 assert_eq!(
1595 schema_link.child_accessor_params,
1596 vec!["catalog_name".to_string(), "schema_name".to_string()]
1597 );
1598 }
1599
1600 #[test]
1606 fn standard_list_fields_recognized() {
1607 for f in [
1608 "page_token",
1609 "page_size",
1610 "max_results",
1611 "order_by",
1612 "filter",
1613 ] {
1614 assert!(
1615 is_standard_list_field(f),
1616 "{f} should be a standard list field"
1617 );
1618 }
1619 for f in ["name", "catalog_name", "schema_name"] {
1620 assert!(
1621 !is_standard_list_field(f),
1622 "{f} should NOT be a standard list field"
1623 );
1624 }
1625 }
1626}