1use std::collections::{BTreeMap, 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 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 { services })
102}
103
104type GlobalParentMap = BTreeMap<(String, String), String>;
111
112fn build_global_parent_map(plans: &[ServicePlan], metadata: &CodeGenMetadata) -> GlobalParentMap {
121 let mut map = GlobalParentMap::new();
122
123 for plan in plans {
124 let managed_type = match plan.managed_resources.first() {
125 Some(r) => &r.descriptor.r#type,
126 None => continue,
127 };
128 if managed_type.is_empty() {
129 continue;
130 }
131
132 for method in &plan.methods {
133 if method.request_type != RequestType::List {
134 continue;
135 }
136 for param in method.query_parameters() {
137 let Some(ref rr) = param.resource_reference else {
138 continue;
139 };
140 if rr.child_type != *managed_type {
142 continue;
143 }
144 if let Some(parent_type) = resolve_parent_type_from_field(¶m.name, metadata) {
145 map.entry((parent_type, managed_type.clone()))
146 .or_insert_with(|| param.name.clone());
147 }
148 }
149 }
150 }
151
152 map
153}
154
155fn derive_ordered_hierarchy(
162 plan: &ServicePlan,
163 global_map: &GlobalParentMap,
164 metadata: &CodeGenMetadata,
165) -> Result<Vec<ResourceHierarchy>> {
166 let managed_type = match plan.managed_resources.first() {
167 Some(r) => r.descriptor.r#type.clone(),
168 None => return Ok(vec![]),
169 };
170
171 let mut ancestors: Vec<(usize, String, String)> = global_map
173 .iter()
174 .filter(|((_, child), _)| child == &managed_type)
175 .map(|((parent_type, _), field_name)| {
176 let depth = compute_depth(parent_type, global_map, &mut HashSet::new())?;
177 Ok((depth, parent_type.clone(), field_name.clone()))
178 })
179 .collect::<Result<Vec<_>>>()?;
180
181 if ancestors.is_empty() {
182 return Ok(vec![]);
183 }
184
185 ancestors.sort_by(|(da, ta, _), (db, tb, _)| da.cmp(db).then_with(|| ta.cmp(tb)));
187
188 Ok(ancestors
189 .into_iter()
190 .map(|(_, parent_type, field_name)| {
191 let parent_singular = field_name.strip_suffix("_name").and_then(|stem| {
192 metadata
193 .resource_from_singular(stem)
194 .map(|_| stem.to_string())
195 });
196 ResourceHierarchy {
197 child_resource_type: managed_type.clone(),
198 parent_resource_type: parent_type,
199 parent_field_name: field_name,
200 parent_singular,
201 }
202 })
203 .collect())
204}
205
206fn compute_depth(
215 resource_type: &str,
216 map: &GlobalParentMap,
217 path: &mut HashSet<String>,
218) -> Result<usize> {
219 if !path.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 mut depth = 0;
225 for (parent, _) in map.keys().filter(|(_, child)| child == resource_type) {
226 depth = depth.max(1 + compute_depth(parent, map, path)?);
227 }
228 path.remove(resource_type);
229 Ok(depth)
230}
231
232fn resolve_parent_type_from_field(field_name: &str, metadata: &CodeGenMetadata) -> Option<String> {
237 field_name
238 .strip_suffix("_name")
239 .and_then(|s| metadata.resource_from_singular(s))
240 .map(|rd| rd.r#type.clone())
241}
242
243pub(crate) fn is_standard_list_field(name: &str) -> bool {
250 matches!(
251 name,
252 "page_token" | "page_size" | "max_results" | "order_by" | "filter"
253 )
254}
255
256pub(crate) fn derive_accessor_parent_fields(plan: &ServicePlan) -> Vec<String> {
270 let Some(resource) = plan.managed_resources.first() else {
271 return vec![];
272 };
273
274 let resource_type = &resource.descriptor.r#type;
276 if !plan.hierarchy.is_empty() && !resource_type.is_empty() {
277 let annotation_parents: Vec<String> = plan
278 .hierarchy
279 .iter()
280 .filter(|h| &h.child_resource_type == resource_type)
281 .map(|h| h.parent_field_name.clone())
282 .collect();
283 if !annotation_parents.is_empty() {
284 return annotation_parents;
285 }
286 }
287
288 let has_explicit_name_field = !resource.descriptor.name_field.is_empty();
290
291 let get_path_param_name = plan
292 .methods
293 .iter()
294 .find(|m| m.request_type == RequestType::Get)
295 .and_then(|m| m.path_parameters().next().map(|p| p.name.clone()));
296
297 let parent_params: Vec<String> = plan
298 .methods
299 .iter()
300 .find(|m| m.request_type == RequestType::List)
301 .map(|m| {
302 m.parameters
303 .iter()
304 .filter(|p| !p.is_path_param() && !p.is_optional())
305 .filter(|p| matches!(p.field_type().base_type, BaseType::String))
306 .filter(|p| !is_standard_list_field(p.name()))
307 .map(|p| p.name().to_string())
308 .collect()
309 })
310 .unwrap_or_default();
311
312 let should_decompose = has_explicit_name_field
313 || (get_path_param_name.as_deref() == Some("name") && !parent_params.is_empty());
314
315 if should_decompose {
316 parent_params
317 } else {
318 vec![]
319 }
320}
321
322fn derive_resource_accessor_params(plan: &ServicePlan) -> Option<Vec<String>> {
329 let resource = plan.managed_resources.first()?;
330 let mut params = derive_accessor_parent_fields(plan);
331 params.push(format!("{}_name", resource.descriptor.singular));
332 Some(params)
333}
334
335fn assign_direct_children(services: &mut [ServicePlan]) {
341 let candidates: Vec<(Vec<String>, String, String)> = services
344 .iter()
345 .filter_map(|s| {
346 let params = s.resource_accessor_params.clone()?;
347 let singular = s.managed_resources.first()?.descriptor.singular.clone();
348 Some((params, s.base_path.clone(), singular))
349 })
350 .collect();
351
352 for plan in services.iter_mut() {
353 let Some(parent_params) = plan.resource_accessor_params.clone() else {
354 continue;
355 };
356 let mut children: Vec<ChildLink> = candidates
357 .iter()
358 .filter(|(child_params, child_base_path, _)| {
359 *child_base_path != plan.base_path
360 && child_params.len() == parent_params.len() + 1
361 && child_params[..parent_params.len()] == parent_params[..]
362 })
363 .map(
364 |(child_params, child_base_path, child_singular)| ChildLink {
365 child_singular: child_singular.clone(),
366 child_base_path: child_base_path.clone(),
367 child_accessor_params: child_params.clone(),
368 },
369 )
370 .collect();
371 children.sort_by(|a, b| a.child_singular.cmp(&b.child_singular));
372 plan.direct_children = children;
373 }
374}
375
376fn analyze_service(metadata: &CodeGenMetadata, info: &ServiceInfo) -> Result<ServicePlan> {
378 let handler_name = strings::service_to_handler_name(&info.name);
379 let base_path = strings::service_to_base_path(&info.name);
380
381 let mut method_plans = info
385 .methods
386 .iter()
387 .map(|method| analyze_method(metadata, method))
388 .collect::<Result<Vec<_>>>()?;
389
390 let managed_resources = types::extract_managed_resources(metadata, &method_plans);
391
392 let service_has_resource = !managed_resources.is_empty();
396 for plan in &mut method_plans {
397 plan.shape = types::method_shape(
398 &plan.request_type,
399 &plan.metadata.method_name,
400 service_has_resource,
401 );
402 }
403
404 Ok(ServicePlan {
405 service_name: info.name.clone(),
406 handler_name,
407 base_path,
408 package: info.package.clone(),
409 methods: method_plans,
410 managed_resources,
411 documentation: info.documentation.clone(),
412 hierarchy: vec![], resource_accessor_params: None, direct_children: vec![], })
416}
417
418pub(crate) fn analyze_method(
430 metadata: &CodeGenMetadata,
431 method: &MethodMetadata,
432) -> Result<MethodPlan> {
433 let Some(http_method) = method.http_method().map(|m| m.to_string()) else {
434 return analyze_routeless_method(metadata, method);
437 };
438
439 let planner = MethodPlanner::try_new(method, metadata)?;
440 let request_type = planner.request_type();
441 let has_response = planner.has_response();
442 let output_resource_type = planner.output_resource_type();
443 let http_pattern = planner.into_http_pattern();
444
445 let input_fields = metadata.get_message_fields(&method.input_type);
446 let (path_params, query_params, body_fields) = extract_request_fields(method, &input_fields)?;
447
448 let parameters = path_params
449 .into_iter()
450 .map(Into::into)
451 .chain(query_params.into_iter().map(Into::into))
452 .chain(body_fields.into_iter().map(Into::into))
453 .collect();
454
455 let has_request_body =
457 types::request_has_body(&request_type) || !method.http_rule.body.is_empty();
458 let needs_request_parts = types::request_needs_request_parts(&request_type);
459 let scoped_verb = types::scoped_verb(&request_type);
460 let (raw_request_body, raw_response_body) = raw_bodies(metadata, method, &input_fields)?;
461 let request_headers = request_headers(method, &input_fields)?;
462
463 Ok(MethodPlan {
464 metadata: method.clone(),
465 handler_function_name: strings::operation_to_method_name(&method.method_name),
466 http_method,
467 parameters,
468 has_response,
469 request_type,
470 output_resource_type,
471 http_pattern,
472 has_request_body,
473 needs_request_parts,
474 scoped_verb,
475 shape: types::MethodShape::Unbound,
478 has_http_route: true,
479 raw_request_body,
480 raw_response_body,
481 request_headers,
482 })
483}
484
485fn request_headers(
488 method: &MethodMetadata,
489 input_fields: &[MessageField],
490) -> Result<Vec<(String, String)>> {
491 input_fields
492 .iter()
493 .filter_map(|f| Some((f, f.request_header.clone()?)))
494 .map(|(f, header)| {
495 let fail = |message: &str| {
496 Err(crate::Error::Build(format!(
497 "method `{}`: the request header field `{}` {message}",
498 method.method_name, f.name
499 )))
500 };
501 if !matches!(f.unified_type.base_type, BaseType::String) || f.unified_type.is_repeated {
502 return fail("must be a `string`");
503 }
504 if matches!(method.http_rule.body.as_str(), "*") || method.http_rule.body == f.name {
505 return fail("cannot be in the body");
506 }
507 Ok((f.name.clone(), header))
508 })
509 .collect()
510}
511
512fn raw_bodies(
516 metadata: &CodeGenMetadata,
517 method: &MethodMetadata,
518 input_fields: &[MessageField],
519) -> Result<(Option<String>, Option<String>)> {
520 let fail = |message: String| {
521 Err(crate::Error::Build(format!(
522 "method `{}`: {message}",
523 method.method_name
524 )))
525 };
526 let request = match input_fields.iter().find(|f| f.raw_body) {
527 None => None,
528 Some(f) if !f.unified_type.is_singular_bytes() => {
529 return fail(format!("the raw body `{}` must be `bytes`", f.name));
530 }
531 Some(f) if method.http_rule.body != f.name => {
532 return fail(format!(
533 "the rule's `body` must name the raw body `{}`",
534 f.name
535 ));
536 }
537 Some(f) => Some(f.name.clone()),
538 };
539 let output_fields = metadata.get_message_fields(&method.output_type);
540 let response = match output_fields.iter().find(|f| f.raw_body) {
541 None => None,
542 Some(f) if !f.unified_type.is_singular_bytes() => {
543 return fail(format!("the raw body `{}` must be `bytes`", f.name));
544 }
545 Some(_) if method.server_streaming => {
546 return fail("a streamed output has no raw body".to_string());
547 }
548 Some(f)
549 if output_fields
550 .iter()
551 .any(|o| o.name != f.name && o.response_header.is_none()) =>
552 {
553 return fail(format!(
554 "the other fields of an output with the raw body `{}` must come from response headers",
555 f.name
556 ));
557 }
558 Some(f) => Some(f.name.clone()),
559 };
560 Ok((request, response))
561}
562
563fn analyze_routeless_method(
575 metadata: &CodeGenMetadata,
576 method: &MethodMetadata,
577) -> Result<MethodPlan> {
578 let input_fields = metadata.get_message_fields(&method.input_type);
579 let body_fields = routeless_body_fields(&input_fields);
580
581 let request_type =
586 RequestType::Custom(crate::google::api::http_rule::Pattern::Post(String::new()));
587 let has_response = !method.output_type.is_empty() && !method.output_type.ends_with("Empty");
588
589 Ok(MethodPlan {
590 metadata: method.clone(),
591 handler_function_name: strings::operation_to_method_name(&method.method_name),
592 http_method: String::new(),
593 parameters: body_fields.into_iter().map(Into::into).collect(),
594 has_response,
595 request_type,
596 output_resource_type: None,
597 http_pattern: HttpPattern::default(),
598 has_request_body: true,
600 needs_request_parts: false,
601 scoped_verb: None,
602 shape: types::MethodShape::Unbound,
603 has_http_route: false,
604 raw_request_body: None,
605 raw_response_body: None,
606 request_headers: Vec::new(),
607 })
608}
609
610fn routeless_body_fields(input_fields: &[MessageField]) -> Vec<BodyField> {
617 input_fields
618 .iter()
619 .filter(|field| !field.field_behavior.contains(&FieldBehavior::OutputOnly))
620 .map(body_field)
621 .collect()
622}
623
624fn body_field(field: &MessageField) -> BodyField {
632 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
633 return BodyField {
634 name: field.name.clone(),
635 field_type: field.unified_type.clone().optional(),
636 repeated: false,
637 required: false,
638 oneof_variants: field.oneof_variants.clone(),
639 documentation: field.documentation.clone(),
640 };
641 }
642
643 let required = field.field_behavior.contains(&FieldBehavior::Required);
644 let needs_optional_marker = !required
645 && !field.unified_type.is_repeated
646 && matches!(field.unified_type.base_type, BaseType::Message(_));
647 let field_type = if needs_optional_marker {
648 field.unified_type.clone().optional()
649 } else {
650 field.unified_type.clone()
651 };
652 BodyField {
653 name: field.name.clone(),
654 field_type,
655 repeated: field.unified_type.is_repeated,
656 required,
657 oneof_variants: None,
658 documentation: field.documentation.clone(),
659 }
660}
661
662fn extract_request_fields(
672 method: &MethodMetadata,
673 input_fields: &[MessageField],
674) -> Result<(Vec<PathParam>, Vec<QueryParam>, Vec<BodyField>)> {
675 let mut path_params = Vec::new();
676 let mut query_params = Vec::new();
677 let mut body_fields = Vec::new();
678
679 let path_param_names = method.http_pattern.parameter_names();
680 let body_spec = method.http_rule.body.as_str();
681
682 let fields_by_name: HashMap<&str, &MessageField> =
684 input_fields.iter().map(|f| (f.name.as_str(), f)).collect();
685
686 let mut processed_fields = HashSet::new();
687
688 for path_param_name in path_param_names {
692 if let Some(field) = fields_by_name.get(path_param_name.as_str()) {
693 path_params.push(PathParam {
696 name: field.name.clone(),
697 field_type: field.unified_type.clone(),
698 documentation: field.documentation.clone(),
699 });
700 processed_fields.insert(field.name.as_str());
701 } else {
702 warn!(
704 "Method {}.{}: URL path placeholder `{}` does not match any request field; \
705 the param will be omitted from the path",
706 method.service_name, method.method_name, path_param_name
707 );
708 }
709 }
710
711 for field in input_fields {
713 let field_name = field.name.as_str();
714
715 if processed_fields.contains(field_name) {
716 continue;
717 }
718
719 if field.field_behavior.contains(&FieldBehavior::OutputOnly) {
721 processed_fields.insert(field_name);
722 continue;
723 }
724
725 if matches!(field.unified_type.base_type, BaseType::OneOf(_)) {
727 body_fields.push(body_field(field));
728 processed_fields.insert(field_name);
729 continue;
730 }
731
732 let is_body = match body_spec {
733 "*" => true,
734 "" => false,
735 specific => specific == field_name,
736 };
737
738 if is_body {
739 body_fields.push(body_field(field));
740 } else {
741 query_params.push(QueryParam {
742 name: field.name.clone(),
743 json_name: field.json_name.clone(),
744 field_type: field.unified_type.clone(),
745 documentation: field.documentation.clone(),
746 resource_reference: field.resource_reference.clone(),
747 });
748 }
749 processed_fields.insert(field_name);
750 }
751
752 Ok((path_params, query_params, body_fields))
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use crate::google::api::{HttpRule, ResourceDescriptor, http_rule::Pattern};
759 use crate::parsing::types::UnifiedType;
760 use crate::parsing::{CodeGenMetadata, HttpPattern, MessageInfo, MethodMetadata, ServiceInfo};
761 use std::collections::HashMap;
762
763 fn make_metadata_with_catalog() -> CodeGenMetadata {
764 let catalog_resource = ResourceDescriptor {
765 r#type: "example.io/Catalog".to_string(),
766 pattern: vec!["catalogs/{catalog}".to_string()],
767 name_field: "name".to_string(),
768 history: 0,
769 plural: "catalogs".to_string(),
770 singular: "catalog".to_string(),
771 style: vec![],
772 };
773 let catalog_info = MessageInfo {
774 name: "Catalog".to_string(),
775 fields: vec![],
776 resource_descriptor: Some(catalog_resource),
777 documentation: None,
778 };
779 let mut messages = HashMap::new();
780 messages.insert("Catalog".to_string(), catalog_info);
781 CodeGenMetadata {
782 messages,
783 ..Default::default()
784 }
785 }
786
787 fn make_get_method() -> MethodMetadata {
788 MethodMetadata {
789 service_name: "CatalogService".to_string(),
790 method_name: "GetCatalog".to_string(),
791 input_type: "GetCatalogRequest".to_string(),
792 output_type: "Catalog".to_string(),
793 server_streaming: false,
794 operation: None,
795 http_rule: HttpRule {
796 selector: "".to_string(),
797 pattern: Some(Pattern::Get("/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 }
806
807 #[test]
808 fn test_managed_resources_extraction() {
809 let metadata = make_metadata_with_catalog();
810 let service_info = ServiceInfo {
811 name: "CatalogService".to_string(),
812 package: "example.catalogs.v1".to_string(),
813 documentation: None,
814 methods: vec![make_get_method()],
815 };
816 let service_plan = analyze_service(&metadata, &service_info).unwrap();
817
818 assert_eq!(service_plan.managed_resources.len(), 1);
819 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
820 assert_eq!(
821 service_plan.managed_resources[0].descriptor.r#type,
822 "example.io/Catalog"
823 );
824 assert_eq!(
825 service_plan.managed_resources[0].descriptor.singular,
826 "catalog"
827 );
828 assert_eq!(
829 service_plan.managed_resources[0].descriptor.plural,
830 "catalogs"
831 );
832 }
833
834 #[test]
835 fn test_no_duplicate_managed_resources() {
836 let metadata = make_metadata_with_catalog();
837 let update_method = MethodMetadata {
838 service_name: "CatalogService".to_string(),
839 method_name: "UpdateCatalog".to_string(),
840 input_type: "UpdateCatalogRequest".to_string(),
841 output_type: "Catalog".to_string(),
842 server_streaming: false,
843 operation: None,
844 http_rule: HttpRule {
845 selector: "".to_string(),
846 pattern: Some(Pattern::Patch("/catalogs/{name}".to_string())),
847 body: "*".to_string(),
848 response_body: "".to_string(),
849 additional_bindings: vec![],
850 },
851 http_pattern: HttpPattern::parse("/catalogs/{name}"),
852 documentation: None,
853 };
854 let service_info = ServiceInfo {
855 name: "CatalogService".to_string(),
856 package: "example.catalogs.v1".to_string(),
857 documentation: None,
858 methods: vec![make_get_method(), update_method],
859 };
860 let service_plan = analyze_service(&metadata, &service_info).unwrap();
861
862 assert_eq!(service_plan.managed_resources.len(), 1);
863 assert_eq!(service_plan.managed_resources[0].type_name, "Catalog");
864 }
865
866 #[test]
867 fn test_analyze_method_missing_http_pattern_is_routeless() {
868 let metadata = CodeGenMetadata::default();
869 let method = MethodMetadata {
870 service_name: "SomeService".to_string(),
871 method_name: "SomeMethod".to_string(),
872 input_type: "".to_string(),
873 output_type: "".to_string(),
874 server_streaming: false,
875 operation: None,
876 http_rule: HttpRule {
877 selector: "".to_string(),
878 pattern: None,
879 body: "".to_string(),
880 response_body: "".to_string(),
881 additional_bindings: vec![],
882 },
883 http_pattern: HttpPattern::parse(""),
884 documentation: None,
885 };
886 let plan = analyze_method(&metadata, &method).unwrap();
889 assert!(!plan.has_http_route);
890 assert!(plan.http_method.is_empty());
891 }
892
893 #[test]
894 fn test_routeless_method_partitions_body_by_field_behavior() {
895 use crate::parsing::MessageInfo;
896
897 let req = MessageInfo {
899 name: "DoThingRequest".to_string(),
900 fields: vec![
901 {
902 let mut f = make_string_field("name", false);
903 f.field_behavior = vec![FieldBehavior::Required];
904 f
905 },
906 make_string_field("comment", false),
907 ],
908 resource_descriptor: None,
909 documentation: None,
910 };
911 let mut messages = HashMap::new();
912 messages.insert("DoThingRequest".to_string(), req);
913 let metadata = CodeGenMetadata {
914 messages,
915 ..Default::default()
916 };
917
918 let method = MethodMetadata {
919 service_name: "ThingService".to_string(),
920 method_name: "DoThing".to_string(),
921 input_type: "DoThingRequest".to_string(),
922 output_type: "DoThingResponse".to_string(),
923 server_streaming: false,
924 operation: None,
925 http_rule: HttpRule {
926 selector: "".to_string(),
927 pattern: None, body: "".to_string(),
929 response_body: "".to_string(),
930 additional_bindings: vec![],
931 },
932 http_pattern: HttpPattern::default(),
933 documentation: None,
934 };
935
936 let plan = analyze_method(&metadata, &method).unwrap();
937 assert!(!plan.has_http_route);
938
939 assert_eq!(plan.path_parameters().count(), 0);
941 assert_eq!(plan.query_parameters().count(), 0);
942 let body: Vec<_> = plan.body_fields().collect();
943 assert_eq!(body.len(), 2);
944
945 let name = body.iter().find(|b| b.name == "name").unwrap();
947 let comment = body.iter().find(|b| b.name == "comment").unwrap();
948 assert!(name.required, "REQUIRED field should be a constructor arg");
949 assert!(
950 !comment.required,
951 "unmarked field should be an optional with_* setter"
952 );
953 assert_eq!(plan.handler_function_name, "do_thing");
954 assert!(plan.has_response);
955 }
956
957 #[test]
958 fn test_routeless_method_drops_output_only_fields() {
959 use crate::parsing::MessageInfo;
960
961 let req = MessageInfo {
962 name: "MakeReq".to_string(),
963 fields: vec![make_string_field("input", false), {
964 let mut f = make_string_field("server_set", false);
965 f.field_behavior = vec![FieldBehavior::OutputOnly];
966 f
967 }],
968 resource_descriptor: None,
969 documentation: None,
970 };
971 let mut messages = HashMap::new();
972 messages.insert("MakeReq".to_string(), req);
973 let metadata = CodeGenMetadata {
974 messages,
975 ..Default::default()
976 };
977
978 let method = MethodMetadata {
979 service_name: "S".to_string(),
980 method_name: "Make".to_string(),
981 input_type: "MakeReq".to_string(),
982 output_type: "MakeResp".to_string(),
983 server_streaming: false,
984 operation: None,
985 http_rule: HttpRule::default(),
986 http_pattern: HttpPattern::default(),
987 documentation: None,
988 };
989
990 let plan = analyze_method(&metadata, &method).unwrap();
991 let body: Vec<_> = plan.body_fields().collect();
992 assert_eq!(body.len(), 1, "OUTPUT_ONLY field must be dropped");
993 assert_eq!(body[0].name, "input");
994 }
995
996 fn make_string_field(name: &str, optional: bool) -> MessageField {
999 use crate::parsing::types::BaseType;
1000 MessageField {
1001 name: name.to_string(),
1002 json_name: None,
1003 response_header: None,
1004 request_header: None,
1005 emit_unpopulated: false,
1006 raw_body: false,
1007 unified_type: UnifiedType {
1008 base_type: BaseType::String,
1009 is_optional: optional,
1010 is_repeated: false,
1011 },
1012 documentation: None,
1013 oneof_variants: None,
1014 field_behavior: vec![],
1015 is_sensitive: false,
1016 resource_reference: None,
1017 }
1018 }
1019
1020 fn make_repeated_field(name: &str) -> MessageField {
1021 use crate::parsing::types::BaseType;
1022 MessageField {
1023 name: name.to_string(),
1024 json_name: None,
1025 response_header: None,
1026 request_header: None,
1027 emit_unpopulated: false,
1028 raw_body: false,
1029 unified_type: UnifiedType {
1030 base_type: BaseType::String,
1031 is_optional: false,
1032 is_repeated: true,
1033 },
1034 documentation: None,
1035 oneof_variants: None,
1036 field_behavior: vec![],
1037 is_sensitive: false,
1038 resource_reference: None,
1039 }
1040 }
1041
1042 fn make_method_with_pattern(pattern: Pattern, body: &str, path: &str) -> MethodMetadata {
1043 MethodMetadata {
1044 service_name: "Svc".to_string(),
1045 method_name: "Method".to_string(),
1046 input_type: "".to_string(),
1047 output_type: "".to_string(),
1048 server_streaming: false,
1049 operation: None,
1050 http_rule: HttpRule {
1051 selector: "".to_string(),
1052 pattern: Some(pattern),
1053 body: body.to_string(),
1054 response_body: "".to_string(),
1055 additional_bindings: vec![],
1056 },
1057 http_pattern: HttpPattern::parse(path),
1058 documentation: None,
1059 }
1060 }
1061
1062 #[test]
1063 fn test_extract_path_params_in_url_order() {
1064 let method =
1065 make_method_with_pattern(Pattern::Get("/a/{x}/b/{y}".to_string()), "", "/a/{x}/b/{y}");
1066 let fields = vec![make_string_field("y", false), make_string_field("x", false)];
1067 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1068 assert_eq!(path.len(), 2);
1070 assert_eq!(path[0].name, "x");
1071 assert_eq!(path[1].name, "y");
1072 assert!(query.is_empty());
1073 assert!(body.is_empty());
1074 }
1075
1076 #[test]
1077 fn test_extract_body_wildcard() {
1078 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
1079 let fields = vec![
1080 make_string_field("name", false),
1081 make_string_field("description", true),
1082 ];
1083 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1084 assert!(path.is_empty());
1085 assert!(query.is_empty());
1086 assert_eq!(body.len(), 2);
1087 }
1088
1089 #[test]
1090 fn test_extract_specific_body_field() {
1091 let method = make_method_with_pattern(
1092 Pattern::Patch("/items/{name}".to_string()),
1093 "payload",
1094 "/items/{name}",
1095 );
1096 let fields = vec![
1097 make_string_field("name", false), make_string_field("payload", false), make_string_field("extra", true), ];
1101 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1102 assert_eq!(path.len(), 1);
1103 assert_eq!(path[0].name, "name");
1104 assert_eq!(body.len(), 1);
1105 assert_eq!(body[0].name, "payload");
1106 assert_eq!(query.len(), 1);
1107 assert_eq!(query[0].name, "extra");
1108 }
1109
1110 #[test]
1111 fn test_extract_no_body_spec_all_query() {
1112 let method = make_method_with_pattern(Pattern::Get("/items".to_string()), "", "/items");
1113 let fields = vec![
1114 make_string_field("filter", true),
1115 make_string_field("page_size", true),
1116 ];
1117 let (path, query, body) = extract_request_fields(&method, &fields).unwrap();
1118 assert!(path.is_empty());
1119 assert_eq!(query.len(), 2);
1120 assert!(body.is_empty());
1121 }
1122
1123 #[test]
1124 fn test_extract_repeated_field_becomes_body_with_repeated_flag() {
1125 let method = make_method_with_pattern(Pattern::Post("/items".to_string()), "*", "/items");
1126 let fields = vec![make_repeated_field("tags")];
1127 let (_, _, body) = extract_request_fields(&method, &fields).unwrap();
1128 assert_eq!(body.len(), 1);
1129 assert!(body[0].repeated);
1130 }
1131
1132 fn make_three_level_metadata() -> (CodeGenMetadata, ServiceInfo, ServiceInfo, ServiceInfo) {
1141 use crate::google::api::ResourceReference;
1142 use crate::parsing::types::BaseType;
1143
1144 let mut messages = HashMap::new();
1145
1146 messages.insert(
1148 "Catalog".to_string(),
1149 MessageInfo {
1150 name: "Catalog".to_string(),
1151 fields: vec![],
1152 resource_descriptor: Some(ResourceDescriptor {
1153 r#type: "example.io/Catalog".to_string(),
1154 pattern: vec!["catalogs/{catalog}".to_string()],
1155 name_field: "name".to_string(),
1156 history: 0,
1157 plural: "catalogs".to_string(),
1158 singular: "catalog".to_string(),
1159 style: vec![],
1160 }),
1161 documentation: None,
1162 },
1163 );
1164
1165 messages.insert(
1167 "Schema".to_string(),
1168 MessageInfo {
1169 name: "Schema".to_string(),
1170 fields: vec![],
1171 resource_descriptor: Some(ResourceDescriptor {
1172 r#type: "example.io/Schema".to_string(),
1173 pattern: vec!["schemas/{schema}".to_string()],
1174 name_field: "name".to_string(),
1175 history: 0,
1176 plural: "schemas".to_string(),
1177 singular: "schema".to_string(),
1178 style: vec![],
1179 }),
1180 documentation: None,
1181 },
1182 );
1183
1184 messages.insert(
1186 "Table".to_string(),
1187 MessageInfo {
1188 name: "Table".to_string(),
1189 fields: vec![],
1190 resource_descriptor: Some(ResourceDescriptor {
1191 r#type: "example.io/Table".to_string(),
1192 pattern: vec!["tables/{table}".to_string()],
1193 name_field: "full_name".to_string(),
1194 history: 0,
1195 plural: "tables".to_string(),
1196 singular: "table".to_string(),
1197 style: vec![],
1198 }),
1199 documentation: None,
1200 },
1201 );
1202
1203 messages.insert(
1205 "ListCatalogsRequest".to_string(),
1206 MessageInfo {
1207 name: "ListCatalogsRequest".to_string(),
1208 fields: vec![],
1209 resource_descriptor: None,
1210 documentation: None,
1211 },
1212 );
1213
1214 messages.insert(
1216 "ListSchemasRequest".to_string(),
1217 MessageInfo {
1218 name: "ListSchemasRequest".to_string(),
1219 fields: vec![MessageField {
1220 name: "catalog_name".to_string(),
1221 json_name: None,
1222 response_header: None,
1223 request_header: None,
1224 emit_unpopulated: false,
1225 raw_body: false,
1226 unified_type: UnifiedType {
1227 base_type: BaseType::String,
1228 is_optional: false,
1229 is_repeated: false,
1230 },
1231 documentation: None,
1232 oneof_variants: None,
1233 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1234 is_sensitive: false,
1235 resource_reference: Some(ResourceReference {
1236 r#type: String::new(),
1237 child_type: "example.io/Schema".to_string(),
1238 }),
1239 }],
1240 resource_descriptor: None,
1241 documentation: None,
1242 },
1243 );
1244
1245 messages.insert(
1247 "ListTablesRequest".to_string(),
1248 MessageInfo {
1249 name: "ListTablesRequest".to_string(),
1250 fields: vec![
1251 MessageField {
1252 name: "catalog_name".to_string(),
1253 json_name: None,
1254 response_header: None,
1255 request_header: None,
1256 emit_unpopulated: false,
1257 raw_body: false,
1258 unified_type: UnifiedType {
1259 base_type: BaseType::String,
1260 is_optional: false,
1261 is_repeated: false,
1262 },
1263 documentation: None,
1264 oneof_variants: None,
1265 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1266 is_sensitive: false,
1267 resource_reference: Some(ResourceReference {
1268 r#type: String::new(),
1269 child_type: "example.io/Table".to_string(),
1270 }),
1271 },
1272 MessageField {
1273 name: "schema_name".to_string(),
1274 json_name: None,
1275 response_header: None,
1276 request_header: None,
1277 emit_unpopulated: false,
1278 raw_body: false,
1279 unified_type: UnifiedType {
1280 base_type: BaseType::String,
1281 is_optional: false,
1282 is_repeated: false,
1283 },
1284 documentation: None,
1285 oneof_variants: None,
1286 field_behavior: vec![crate::google::api::FieldBehavior::Required],
1287 is_sensitive: false,
1288 resource_reference: Some(ResourceReference {
1289 r#type: String::new(),
1290 child_type: "example.io/Table".to_string(),
1291 }),
1292 },
1293 ],
1294 resource_descriptor: None,
1295 documentation: None,
1296 },
1297 );
1298
1299 let metadata = CodeGenMetadata {
1300 messages,
1301 ..Default::default()
1302 };
1303
1304 let catalog_svc = ServiceInfo {
1305 name: "CatalogService".to_string(),
1306 package: "example.v1".to_string(),
1307 documentation: None,
1308 methods: vec![
1309 MethodMetadata {
1310 service_name: "CatalogService".to_string(),
1311 method_name: "ListCatalogs".to_string(),
1312 input_type: "ListCatalogsRequest".to_string(),
1313 output_type: "ListCatalogsResponse".to_string(),
1314 server_streaming: false,
1315 operation: None,
1316 http_rule: HttpRule {
1317 selector: "".to_string(),
1318 pattern: Some(Pattern::Get("/catalogs".to_string())),
1319 body: "".to_string(),
1320 response_body: "".to_string(),
1321 additional_bindings: vec![],
1322 },
1323 http_pattern: HttpPattern::parse("/catalogs"),
1324 documentation: None,
1325 },
1326 MethodMetadata {
1327 service_name: "CatalogService".to_string(),
1328 method_name: "GetCatalog".to_string(),
1329 input_type: "GetCatalogRequest".to_string(),
1330 output_type: "Catalog".to_string(),
1331 server_streaming: false,
1332 operation: None,
1333 http_rule: HttpRule {
1334 selector: "".to_string(),
1335 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
1336 body: "".to_string(),
1337 response_body: "".to_string(),
1338 additional_bindings: vec![],
1339 },
1340 http_pattern: HttpPattern::parse("/catalogs/{name}"),
1341 documentation: None,
1342 },
1343 ],
1344 };
1345
1346 let schema_svc = ServiceInfo {
1347 name: "SchemaService".to_string(),
1348 package: "example.v1".to_string(),
1349 documentation: None,
1350 methods: vec![
1351 MethodMetadata {
1352 service_name: "SchemaService".to_string(),
1353 method_name: "ListSchemas".to_string(),
1354 input_type: "ListSchemasRequest".to_string(),
1355 output_type: "ListSchemasResponse".to_string(),
1356 server_streaming: false,
1357 operation: None,
1358 http_rule: HttpRule {
1359 selector: "".to_string(),
1360 pattern: Some(Pattern::Get("/schemas".to_string())),
1361 body: "".to_string(),
1362 response_body: "".to_string(),
1363 additional_bindings: vec![],
1364 },
1365 http_pattern: HttpPattern::parse("/schemas"),
1366 documentation: None,
1367 },
1368 MethodMetadata {
1369 service_name: "SchemaService".to_string(),
1370 method_name: "GetSchema".to_string(),
1371 input_type: "GetSchemaRequest".to_string(),
1372 output_type: "Schema".to_string(),
1373 server_streaming: false,
1374 operation: None,
1375 http_rule: HttpRule {
1376 selector: "".to_string(),
1377 pattern: Some(Pattern::Get("/schemas/{full_name}".to_string())),
1378 body: "".to_string(),
1379 response_body: "".to_string(),
1380 additional_bindings: vec![],
1381 },
1382 http_pattern: HttpPattern::parse("/schemas/{full_name}"),
1383 documentation: None,
1384 },
1385 ],
1386 };
1387
1388 let table_svc = ServiceInfo {
1389 name: "TableService".to_string(),
1390 package: "example.v1".to_string(),
1391 documentation: None,
1392 methods: vec![
1393 MethodMetadata {
1394 service_name: "TableService".to_string(),
1395 method_name: "ListTables".to_string(),
1396 input_type: "ListTablesRequest".to_string(),
1397 output_type: "ListTablesResponse".to_string(),
1398 server_streaming: false,
1399 operation: None,
1400 http_rule: HttpRule {
1401 selector: "".to_string(),
1402 pattern: Some(Pattern::Get("/tables".to_string())),
1403 body: "".to_string(),
1404 response_body: "".to_string(),
1405 additional_bindings: vec![],
1406 },
1407 http_pattern: HttpPattern::parse("/tables"),
1408 documentation: None,
1409 },
1410 MethodMetadata {
1411 service_name: "TableService".to_string(),
1412 method_name: "GetTable".to_string(),
1413 input_type: "GetTableRequest".to_string(),
1414 output_type: "Table".to_string(),
1415 server_streaming: false,
1416 operation: None,
1417 http_rule: HttpRule {
1418 selector: "".to_string(),
1419 pattern: Some(Pattern::Get("/tables/{full_name}".to_string())),
1420 body: "".to_string(),
1421 response_body: "".to_string(),
1422 additional_bindings: vec![],
1423 },
1424 http_pattern: HttpPattern::parse("/tables/{full_name}"),
1425 documentation: None,
1426 },
1427 ],
1428 };
1429
1430 (metadata, catalog_svc, schema_svc, table_svc)
1431 }
1432
1433 fn make_plans_from_fixture() -> (Vec<ServicePlan>, CodeGenMetadata) {
1435 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1436 let mut plans = Vec::new();
1437 for svc in &[&catalog_svc, &schema_svc, &table_svc] {
1438 let plan = analyze_service(&metadata, svc).unwrap();
1439 plans.push(plan);
1440 }
1441 (plans, metadata)
1442 }
1443
1444 #[test]
1445 fn test_build_global_parent_map() {
1446 let (plans, metadata) = make_plans_from_fixture();
1447 let map = build_global_parent_map(&plans, &metadata);
1448
1449 assert_eq!(
1451 map.get(&(
1452 "example.io/Catalog".to_string(),
1453 "example.io/Schema".to_string()
1454 )),
1455 Some(&"catalog_name".to_string()),
1456 "Catalog→Schema mapping missing"
1457 );
1458 assert_eq!(
1460 map.get(&(
1461 "example.io/Schema".to_string(),
1462 "example.io/Table".to_string()
1463 )),
1464 Some(&"schema_name".to_string()),
1465 "Schema→Table mapping missing"
1466 );
1467 assert_eq!(
1469 map.get(&(
1470 "example.io/Catalog".to_string(),
1471 "example.io/Table".to_string()
1472 )),
1473 Some(&"catalog_name".to_string()),
1474 "Catalog→Table flat-API mapping missing"
1475 );
1476 }
1477
1478 #[test]
1479 fn test_derive_ordered_hierarchy_three_levels() {
1480 let (plans, metadata) = make_plans_from_fixture();
1481 let map = build_global_parent_map(&plans, &metadata);
1482 let table_plan = plans
1483 .iter()
1484 .find(|p| p.service_name == "TableService")
1485 .unwrap();
1486 let hierarchy = derive_ordered_hierarchy(table_plan, &map, &metadata)
1487 .expect("no cycles in test fixture");
1488
1489 assert_eq!(hierarchy.len(), 2, "expected 2 ancestors for Table");
1490 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1492 assert_eq!(hierarchy[1].parent_field_name, "schema_name");
1493 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1494 assert_eq!(hierarchy[1].parent_resource_type, "example.io/Schema");
1495 assert_eq!(hierarchy[0].parent_singular, Some("catalog".to_string()));
1496 assert_eq!(hierarchy[1].parent_singular, Some("schema".to_string()));
1497 assert!(
1499 hierarchy
1500 .iter()
1501 .all(|h| h.child_resource_type == "example.io/Table")
1502 );
1503 }
1504
1505 #[test]
1506 fn test_derive_ordered_hierarchy_two_levels() {
1507 let (plans, metadata) = make_plans_from_fixture();
1508 let map = build_global_parent_map(&plans, &metadata);
1509 let schema_plan = plans
1510 .iter()
1511 .find(|p| p.service_name == "SchemaService")
1512 .unwrap();
1513 let hierarchy = derive_ordered_hierarchy(schema_plan, &map, &metadata)
1514 .expect("no cycles in test fixture");
1515
1516 assert_eq!(hierarchy.len(), 1);
1517 assert_eq!(hierarchy[0].parent_field_name, "catalog_name");
1518 assert_eq!(hierarchy[0].parent_resource_type, "example.io/Catalog");
1519 }
1520
1521 #[test]
1522 fn test_derive_ordered_hierarchy_root_resource() {
1523 let (plans, metadata) = make_plans_from_fixture();
1524 let map = build_global_parent_map(&plans, &metadata);
1525 let catalog_plan = plans
1526 .iter()
1527 .find(|p| p.service_name == "CatalogService")
1528 .unwrap();
1529 let hierarchy = derive_ordered_hierarchy(catalog_plan, &map, &metadata)
1530 .expect("no cycles in test fixture");
1531
1532 assert!(
1533 hierarchy.is_empty(),
1534 "root resource should have empty hierarchy"
1535 );
1536 }
1537
1538 #[test]
1539 fn test_compute_depth_cycle_guard() {
1540 let mut map = GlobalParentMap::new();
1542 map.insert(("A".to_string(), "B".to_string()), "a_name".to_string());
1543 map.insert(("B".to_string(), "A".to_string()), "b_name".to_string());
1544
1545 assert!(compute_depth("A", &map, &mut HashSet::new()).is_err());
1546 assert!(compute_depth("B", &map, &mut HashSet::new()).is_err());
1547 }
1548
1549 #[test]
1550 fn test_compute_depth_takes_the_longest_parent_chain() {
1551 let mut map = GlobalParentMap::new();
1555 for (parent, child) in [
1556 ("Catalog", "Schema"),
1557 ("Catalog", "Table"),
1558 ("Schema", "Table"),
1559 ("Catalog", "Column"),
1560 ("Schema", "Column"),
1561 ("Table", "Column"),
1562 ] {
1563 map.insert((parent.to_string(), child.to_string()), String::new());
1564 }
1565 let depth = |t: &str| compute_depth(t, &map, &mut HashSet::new()).unwrap();
1566 assert_eq!(depth("Catalog"), 0);
1567 assert_eq!(depth("Schema"), 1);
1568 assert_eq!(depth("Table"), 2);
1569 assert_eq!(depth("Column"), 3);
1570 }
1571
1572 #[test]
1573 fn test_full_analyze_metadata_hierarchy_ordering() {
1574 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1575 let mut services_map = HashMap::new();
1576 services_map.insert("CatalogService".to_string(), catalog_svc);
1577 services_map.insert("SchemaService".to_string(), schema_svc);
1578 services_map.insert("TableService".to_string(), table_svc);
1579 let full_metadata = CodeGenMetadata {
1580 messages: metadata.messages,
1581 services: services_map,
1582 ..Default::default()
1583 };
1584
1585 let plan = analyze_metadata(&full_metadata).unwrap();
1586 let table_svc_plan = plan
1587 .services
1588 .iter()
1589 .find(|s| s.service_name == "TableService")
1590 .expect("TableService plan not found");
1591
1592 assert_eq!(table_svc_plan.hierarchy.len(), 2);
1593 assert_eq!(
1594 table_svc_plan.hierarchy[0].parent_field_name,
1595 "catalog_name"
1596 );
1597 assert_eq!(table_svc_plan.hierarchy[1].parent_field_name, "schema_name");
1598 }
1599
1600 #[test]
1601 fn resource_accessor_params_nested_and_flat() {
1602 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1603 let mut services_map = HashMap::new();
1604 services_map.insert("CatalogService".to_string(), catalog_svc);
1605 services_map.insert("SchemaService".to_string(), schema_svc);
1606 services_map.insert("TableService".to_string(), table_svc);
1607 let full_metadata = CodeGenMetadata {
1608 messages: metadata.messages,
1609 services: services_map,
1610 ..Default::default()
1611 };
1612
1613 let plan = analyze_metadata(&full_metadata).unwrap();
1614 let params = |name: &str| {
1615 plan.services
1616 .iter()
1617 .find(|s| s.service_name == name)
1618 .unwrap()
1619 .resource_accessor_params
1620 .clone()
1621 };
1622
1623 assert_eq!(
1625 params("TableService"),
1626 Some(vec![
1627 "catalog_name".to_string(),
1628 "schema_name".to_string(),
1629 "table_name".to_string(),
1630 ])
1631 );
1632 assert_eq!(
1634 params("SchemaService"),
1635 Some(vec!["catalog_name".to_string(), "schema_name".to_string()])
1636 );
1637 assert_eq!(
1639 params("CatalogService"),
1640 Some(vec!["catalog_name".to_string()])
1641 );
1642 }
1643
1644 #[test]
1645 fn direct_children_via_accessor_param_prefix() {
1646 let (metadata, catalog_svc, schema_svc, table_svc) = make_three_level_metadata();
1647 let mut services_map = HashMap::new();
1648 services_map.insert("CatalogService".to_string(), catalog_svc);
1649 services_map.insert("SchemaService".to_string(), schema_svc);
1650 services_map.insert("TableService".to_string(), table_svc);
1651 let full_metadata = CodeGenMetadata {
1652 messages: metadata.messages,
1653 services: services_map,
1654 ..Default::default()
1655 };
1656
1657 let plan = analyze_metadata(&full_metadata).unwrap();
1658 let children = |name: &str| -> Vec<String> {
1659 plan.services
1660 .iter()
1661 .find(|s| s.service_name == name)
1662 .unwrap()
1663 .direct_children
1664 .iter()
1665 .map(|c| c.child_singular.clone())
1666 .collect()
1667 };
1668
1669 assert_eq!(children("CatalogService"), vec!["schema".to_string()]);
1672 assert_eq!(children("SchemaService"), vec!["table".to_string()]);
1674 assert!(children("TableService").is_empty());
1676
1677 let catalog = plan
1679 .services
1680 .iter()
1681 .find(|s| s.service_name == "CatalogService")
1682 .unwrap();
1683 let schema_link = &catalog.direct_children[0];
1684 let schema_base_path = plan
1686 .services
1687 .iter()
1688 .find(|s| s.service_name == "SchemaService")
1689 .unwrap()
1690 .base_path
1691 .clone();
1692 assert_eq!(schema_link.child_base_path, schema_base_path);
1693 assert_eq!(
1694 schema_link.child_accessor_params,
1695 vec!["catalog_name".to_string(), "schema_name".to_string()]
1696 );
1697 }
1698
1699 #[test]
1705 fn standard_list_fields_recognized() {
1706 for f in [
1707 "page_token",
1708 "page_size",
1709 "max_results",
1710 "order_by",
1711 "filter",
1712 ] {
1713 assert!(
1714 is_standard_list_field(f),
1715 "{f} should be a standard list field"
1716 );
1717 }
1718 for f in ["name", "catalog_name", "schema_name"] {
1719 assert!(
1720 !is_standard_list_field(f),
1721 "{f} should NOT be a standard list field"
1722 );
1723 }
1724 }
1725}