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