1use std::collections::HashMap;
23use std::path::PathBuf;
24
25use convert_case::{Case, Casing};
26use itertools::Itertools;
27use proc_macro2::TokenStream;
28use quote::{format_ident, quote};
29use syn::{File, Ident};
30
31use crate::error::{Error, Result};
32
33use crate::analysis::{
34 BodyField, GenerationPlan, ManagedResource, MethodPlan, RequestParam, RequestType, ServicePlan,
35 analyze_metadata, split_body_fields,
36};
37use crate::google::api::http_rule::Pattern;
38use crate::output;
39use crate::parsing::types::{self, RenderContext, UnifiedType};
40use crate::parsing::{CodeGenMetadata, MessageField, MessageInfo};
41
42mod builder;
43mod client;
44mod handler;
45pub(crate) mod node;
46mod python;
47mod resources;
48mod server;
49
50impl MethodPlan {
51 pub(crate) fn resource_client_method(&self) -> Ident {
52 match self.request_type {
53 RequestType::Get => format_ident!("get"),
54 RequestType::Update => format_ident!("update"),
55 RequestType::Delete => format_ident!("delete"),
56 _ => format_ident!("{}", self.handler_function_name),
57 }
58 }
59
60 pub(crate) fn base_method_ident(&self) -> Ident {
61 format_ident!("{}", self.handler_function_name)
62 }
63}
64
65#[derive(Debug, Clone)]
71pub struct ModelsPath {
72 template: String,
73}
74
75impl ModelsPath {
76 pub fn new(template: &str) -> Result<Self> {
80 let test = template.replace("{service}", "test");
81 syn::parse_str::<syn::Path>(&test).map_err(|e| Error::InvalidModelsPathTemplate {
82 template: template.to_string(),
83 source: e,
84 })?;
85 Ok(Self {
86 template: template.to_string(),
87 })
88 }
89
90 pub fn resolve(&self, service: &str) -> syn::Path {
97 let path = self.template.replace("{service}", service);
98 syn::parse_str(&path)
99 .unwrap_or_else(|e| panic!("Invalid models path `{path}` after substitution: {e}"))
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct BindingsConfig {
106 pub aggregate_client_name: String,
108 pub client_crate_name: String,
115 pub py_error_type: String,
117 pub py_result_type: String,
119 pub napi_error_ext_trait: String,
121 pub typings_package_filter: Option<String>,
127 pub ts_error_base_class: String,
129 pub ts_error_code_prefix: String,
131}
132
133#[derive(Debug, Clone)]
137pub struct CodeGenConfig {
138 pub context_type_path: String,
142
143 pub result_type_path: String,
147
148 pub models_path_template: String,
153
154 pub models_path_crate_template: String,
159
160 pub output: CodeGenOutput,
162
163 pub generate_resource_enum: bool,
169
170 pub generate_store_integration: bool,
177
178 pub error_type_path: Option<String>,
182
183 pub generate_object_conversions: bool,
188
189 pub bindings: Option<BindingsConfig>,
192
193 pub models_gen_dir: Option<String>,
196
197 pub resource_store_crate_name: String,
201}
202
203impl CodeGenConfig {
204 pub fn validate(&self) -> Result<()> {
214 ModelsPath::new(&self.models_path_template)?;
215 ModelsPath::new(&self.models_path_crate_template)?;
216 if (self.output.python.is_some()
217 || self.output.node.is_some()
218 || self.output.node_ts.is_some())
219 && self.bindings.is_none()
220 {
221 return Err(Error::MissingBindingsConfig);
222 }
223 Ok(())
224 }
225}
226
227#[derive(Debug, Clone)]
233pub struct CodeGenOutput {
234 pub common: PathBuf,
236 pub models: Option<PathBuf>,
242 pub models_subdir: String,
246 pub server: Option<PathBuf>,
248 pub client: Option<PathBuf>,
250 pub python: Option<PathBuf>,
252 pub node: Option<PathBuf>,
254 pub node_ts: Option<PathBuf>,
256 pub python_typings_filename: String,
260}
261
262impl CodeGenOutput {
263 pub fn models_subdir_path(&self) -> Option<PathBuf> {
265 self.models.as_ref().map(|m| m.join(&self.models_subdir))
266 }
267}
268
269pub fn generate_code(metadata: &CodeGenMetadata, config: &CodeGenConfig) -> Result<()> {
299 ModelsPath::new(&config.models_path_template)?;
301 ModelsPath::new(&config.models_path_crate_template)?;
302
303 if (config.output.python.is_some()
305 || config.output.node.is_some()
306 || config.output.node_ts.is_some())
307 && config.bindings.is_none()
308 {
309 return Err(Error::MissingBindingsConfig);
310 }
311
312 let plan = analyze_metadata(metadata)?;
313
314 let common_code = generate_common_code(&plan, metadata, config)?;
315 output::write_generated_code(&common_code, &config.output.common)?;
316
317 if config.output.models.is_some() {
318 let subdir = config
319 .output
320 .models_subdir_path()
321 .expect("models is Some so subdir is Some");
322 std::fs::create_dir_all(&subdir).map_err(Error::Io)?;
323
324 if config.generate_resource_enum {
325 let resource_enum = resources::generate_resource_enum(
326 &plan,
327 metadata,
328 config,
329 config.error_type_path.as_deref(),
330 );
331 let mut models_files = GeneratedCode {
332 files: std::collections::HashMap::new(),
333 };
334 models_files
335 .files
336 .insert("labels.rs".to_string(), resource_enum);
337 output::write_generated_code(&models_files, &subdir)?;
338 }
339
340 let gen_dir = config.models_gen_dir.as_deref().unwrap_or("../gen");
341 let include_labels = config.generate_resource_enum;
342 let mod_content = generate_models_mod(&plan.services, gen_dir, include_labels, metadata);
343 let mod_path = subdir.join("mod.rs");
344 std::fs::write(&mod_path, mod_content).map_err(Error::Io)?;
345 }
346
347 if let Some(ref server_dir) = config.output.server {
348 let server_code = generate_server_code(&plan, metadata, config)?;
349 output::write_generated_code(&server_code, server_dir)?;
350 }
351
352 if let Some(ref client_dir) = config.output.client {
353 let client_code = generate_client_code(&plan, metadata, config)?;
354 output::write_generated_code(&client_code, client_dir)?;
355 }
356
357 if let Some(ref python_dir) = config.output.python {
358 let python_code = generate_python_code(&plan, metadata, config)?;
359 output::write_generated_code(&python_code, python_dir)?;
360 }
361
362 if let Some(ref node_dir) = config.output.node {
363 let node_code = generate_node_code(&plan, metadata, config)?;
364 output::write_generated_code(&node_code, node_dir)?;
365 }
366
367 if let Some(ref node_ts_dir) = config.output.node_ts {
368 let node_ts_code = generate_node_ts_code(&plan, metadata, config)?;
369 output::write_generated_code(&node_ts_code, node_ts_dir)?;
370 }
371
372 Ok(())
373}
374
375fn generate_common_code(
376 plan: &GenerationPlan,
377 metadata: &CodeGenMetadata,
378 config: &CodeGenConfig,
379) -> Result<GeneratedCode> {
380 let mut files = HashMap::new();
381
382 for service in &plan.services {
383 let handler = ServiceHandler {
384 plan: service,
385 metadata,
386 config,
387 };
388 let server_code = server::generate_common(&handler);
389 files.insert(format!("{}/server.rs", service.base_path), server_code);
390 let module_code = generate_common_module();
391 files.insert(format!("{}/mod.rs", service.base_path), module_code);
392 }
393
394 let module_code = main_module(&plan.services);
395 files.insert("mod.rs".to_string(), module_code);
396
397 Ok(GeneratedCode { files })
398}
399
400fn generate_server_code(
401 plan: &GenerationPlan,
402 metadata: &CodeGenMetadata,
403 config: &CodeGenConfig,
404) -> Result<GeneratedCode> {
405 let mut files = HashMap::new();
406
407 for service in &plan.services {
408 let handler = ServiceHandler {
409 plan: service,
410 metadata,
411 config,
412 };
413 let trait_code = handler::generate(&handler)?;
414 files.insert(format!("{}/handler.rs", service.base_path), trait_code);
415 let server_code = server::generate_server(&handler);
416 files.insert(format!("{}/server.rs", service.base_path), server_code);
417 let module_code = generate_server_module(service);
418 files.insert(format!("{}/mod.rs", service.base_path), module_code);
419 }
420
421 let module_code = main_module(&plan.services);
422 files.insert("mod.rs".to_string(), module_code);
423
424 Ok(GeneratedCode { files })
425}
426
427fn generate_python_code(
428 plan: &GenerationPlan,
429 metadata: &CodeGenMetadata,
430 config: &CodeGenConfig,
431) -> Result<GeneratedCode> {
432 let mut files = HashMap::new();
433
434 let handlers = plan
435 .services
436 .iter()
437 .map(|service| ServiceHandler {
438 plan: service,
439 metadata,
440 config,
441 })
442 .collect_vec();
443
444 for service in &handlers {
445 let python_code = python::generate(service);
446 files.insert(format!("{}.rs", service.plan.base_path), python_code);
447 }
448
449 let module_code = python::main_module(&handlers);
450 files.insert("mod.rs".to_string(), module_code);
451
452 let python_typings_code = python::generate_typings(&handlers);
453 files.insert(
454 config.output.python_typings_filename.clone(),
455 python_typings_code,
456 );
457
458 Ok(GeneratedCode { files })
459}
460
461fn generate_node_code(
462 plan: &GenerationPlan,
463 metadata: &CodeGenMetadata,
464 config: &CodeGenConfig,
465) -> Result<GeneratedCode> {
466 let mut files = HashMap::new();
467
468 let handlers = plan
469 .services
470 .iter()
471 .map(|service| ServiceHandler {
472 plan: service,
473 metadata,
474 config,
475 })
476 .collect_vec();
477
478 for service in &handlers {
479 let napi_code = node::generate(service);
480 files.insert(format!("{}.rs", service.plan.base_path), napi_code);
481 }
482
483 let module_code = node::main_module(&handlers);
484 files.insert("mod.rs".to_string(), module_code);
485
486 Ok(GeneratedCode { files })
487}
488
489fn generate_node_ts_code(
490 plan: &GenerationPlan,
491 metadata: &CodeGenMetadata,
492 config: &CodeGenConfig,
493) -> Result<GeneratedCode> {
494 let handlers = plan
495 .services
496 .iter()
497 .map(|service| ServiceHandler {
498 plan: service,
499 metadata,
500 config,
501 })
502 .collect_vec();
503
504 let ts_code = node::typescript::generate_client_ts(&handlers);
505 let mut files = HashMap::new();
506 files.insert("client.ts".to_string(), ts_code);
507
508 Ok(GeneratedCode { files })
509}
510
511fn generate_client_code(
512 plan: &GenerationPlan,
513 metadata: &CodeGenMetadata,
514 config: &CodeGenConfig,
515) -> Result<GeneratedCode> {
516 let mut files = HashMap::new();
517
518 for service in &plan.services {
519 let handler = ServiceHandler {
520 plan: service,
521 metadata,
522 config,
523 };
524 let client_code = client::generate(&handler)?;
525 files.insert(format!("{}/client.rs", service.base_path), client_code);
526 let builder_code = builder::generate(&handler)?;
527 files.insert(format!("{}/builders.rs", service.base_path), builder_code);
528 let module_code = generate_client_module();
529 files.insert(format!("{}/mod.rs", service.base_path), module_code);
530 }
531
532 let module_code = generate_client_main_module(&plan.services);
533 files.insert("mod.rs".to_string(), module_code);
534
535 Ok(GeneratedCode { files })
536}
537
538fn generate_common_module() -> String {
539 let tokens = quote! {
540 #[cfg(feature = "axum")]
541 pub mod server;
542 };
543 format_tokens(tokens)
544}
545
546fn generate_server_module(service: &ServicePlan) -> String {
547 let handler_ident = format_ident!("{}", service.handler_name);
548 let tokens = quote! {
549 pub use handler::#handler_ident;
550
551 mod handler;
552 #[cfg(feature = "axum")]
553 pub mod server;
554 };
555 format_tokens(tokens)
556}
557
558fn generate_client_module() -> String {
559 let tokens = quote! {
560 pub use client::*;
561 pub use builders::*;
562
563 pub mod client;
564 pub mod builders;
565 };
566 format_tokens(tokens)
567}
568
569pub fn main_module(services: &[ServicePlan]) -> String {
570 let service_modules: Vec<TokenStream> = services
571 .iter()
572 .map(|s| {
573 let module_name = format_ident!("{}", s.base_path);
574 quote! { pub mod #module_name; }
575 })
576 .collect();
577
578 let tokens = quote! {
579 #(#service_modules)*
580 };
581 format_tokens(tokens)
582}
583
584fn generate_client_main_module(services: &[ServicePlan]) -> String {
585 let service_modules: Vec<TokenStream> = services
586 .iter()
587 .map(|s| {
588 let module_name = format_ident!("{}", s.base_path);
589 quote! { pub mod #module_name; }
590 })
591 .collect();
592
593 let tokens = quote! {
594 #(#service_modules)*
595
596 use futures::Future;
597
598 pub(super) fn stream_paginated<F, Fut, S, T>(
599 state: S,
600 op: F,
601 ) -> impl futures::Stream<Item = crate::Result<T>>
602 where
603 F: Fn(S, Option<String>) -> Fut + Copy,
604 Fut: Future<Output = crate::Result<(T, S, Option<String>)>>,
605 {
606 enum PaginationState<T> {
607 Start(T),
608 HasMore(T, String),
609 Done,
610 }
611
612 futures::stream::unfold(
613 PaginationState::Start(state),
614 move |state| async move {
615 let (s, page_token) = match state {
616 PaginationState::Start(s) => (s, None),
617 PaginationState::HasMore(s, page_token) if !page_token.is_empty() => {
618 (s, Some(page_token))
619 }
620 _ => {
621 return None;
622 }
623 };
624
625 let (resp, s, continuation) = match op(s, page_token).await {
626 Ok(resp) => resp,
627 Err(e) => return Some((Err(e), PaginationState::Done)),
628 };
629
630 let next_state = match continuation {
631 Some(token) => PaginationState::HasMore(s, token),
632 None => PaginationState::Done,
633 };
634
635 Some((Ok(resp), next_state))
636 },
637 )
638 }
639 };
640 format_tokens(tokens)
641}
642
643pub(crate) fn doc_tokens(documentation: Option<&str>) -> TokenStream {
645 let Some(doc) = documentation else {
646 return quote! {};
647 };
648 let doc = doc.trim();
649 if doc.is_empty() {
650 return quote! {};
651 }
652 let attrs: Vec<TokenStream> = doc
653 .lines()
654 .map(|line| {
655 let line = line.trim();
656 if line.is_empty() {
657 quote! { #[doc = ""] }
658 } else {
659 let spaced = format!(" {}", line);
660 quote! { #[doc = #spaced] }
661 }
662 })
663 .collect();
664 quote! { #(#attrs)* }
665}
666
667pub fn generate_models_mod(
680 services: &[ServicePlan],
681 gen_dir: &str,
682 include_labels: bool,
683 metadata: &CodeGenMetadata,
684) -> String {
685 let mut sorted_services: Vec<&ServicePlan> = services.iter().collect();
686 sorted_services.sort_by_key(|s| &s.base_path);
687
688 let service_mods: Vec<TokenStream> = sorted_services
690 .iter()
691 .map(|svc| {
692 let parts: Vec<&str> = svc.package.split('.').collect();
695 let (svc_seg, ver_seg) = if parts.len() >= 2 {
698 (parts[parts.len() - 2], parts[parts.len() - 1])
699 } else {
700 (svc.base_path.as_str(), "v1")
701 };
702
703 let svc_mod = format_ident!("{}", svc_seg);
704 let ver_mod = format_ident!("{}", ver_seg);
705
706 let main_include = format!("./{}/{}.rs", gen_dir, svc.package);
707 let tonic_include = format!("./{}/{}.tonic.rs", gen_dir, svc.package);
708
709 quote! {
710 pub mod #svc_mod {
711 pub mod #ver_mod {
712 include!(#main_include);
713 #[cfg(feature = "grpc")]
714 include!(#tonic_include);
715 }
716 }
717 }
718 })
719 .collect();
720
721 let mut reexports: Vec<TokenStream> = Vec::new();
725 for svc in &sorted_services {
726 let package = &svc.package;
727 let fq_prefix = format!(".{}.", package);
728
729 let parts: Vec<&str> = svc.package.split('.').collect();
730 let (svc_seg, ver_seg) = if parts.len() >= 2 {
731 (parts[parts.len() - 2], parts[parts.len() - 1])
732 } else {
733 (svc.base_path.as_str(), "v1")
734 };
735 let svc_mod = format_ident!("{}", svc_seg);
736 let ver_mod = format_ident!("{}", ver_seg);
737
738 let mut type_names: std::collections::BTreeSet<String> = svc
740 .managed_resources
741 .iter()
742 .map(|r| r.type_name.clone())
743 .collect();
744
745 for (fq_name, msg_info) in &metadata.messages {
747 if msg_info.resource_descriptor.is_some()
748 && (fq_name.starts_with(&fq_prefix)
749 || fq_name.starts_with(&format!(".{}", package)))
750 {
751 let simple = fq_name
753 .rfind('.')
754 .map(|i| &fq_name[i + 1..])
755 .unwrap_or(fq_name.as_str());
756 type_names.insert(simple.to_string());
757 }
758 }
759
760 for type_name in &type_names {
761 let type_ident = format_ident!("{}", type_name);
762 reexports.push(quote! {
763 pub use #svc_mod::#ver_mod::#type_ident;
764 });
765 }
766 }
767
768 let labels_decl: TokenStream = if include_labels {
769 quote! {
770 pub mod labels;
771 pub use labels::{ObjectLabel, Resource};
772 }
773 } else {
774 quote! {}
775 };
776
777 let tokens = quote! {
778 use std::collections::HashMap;
779
780 #labels_decl
781
782 #(#reexports)*
783
784 pub type PropertyMap = HashMap<String, serde_json::Value>;
785
786 #(#service_mods)*
787 };
788
789 format_tokens(tokens)
790}
791
792pub(crate) fn format_tokens(tokens: TokenStream) -> String {
793 let tokens_string = tokens.to_string();
794 let syntax_tree = syn::parse2::<File>(tokens).unwrap_or_else(|_| {
795 syn::parse_str::<File>(&tokens_string).unwrap_or_else(|_| {
796 syn::parse_quote! {
797 }
799 })
800 });
801 prettyplease::unparse(&syntax_tree)
802}
803
804#[derive(Debug)]
806pub struct GeneratedCode {
807 pub files: HashMap<String, String>,
809}
810
811impl CodeGenMetadata {
812 fn get_message_meta(&self, message_name: &str) -> Option<MessageMeta<'_>> {
813 self.messages.get(message_name).map(|info| MessageMeta {
814 info,
815 metadata: self,
816 })
817 }
818}
819
820pub(crate) struct MessageMeta<'a> {
821 info: &'a MessageInfo,
822 #[allow(dead_code)]
823 metadata: &'a CodeGenMetadata,
824}
825
826pub(crate) struct ServiceHandler<'a> {
827 pub(crate) plan: &'a ServicePlan,
828 pub(crate) metadata: &'a CodeGenMetadata,
829 pub(crate) config: &'a CodeGenConfig,
830}
831
832impl ServiceHandler<'_> {
833 pub(crate) fn resource(&self) -> Option<&ManagedResource> {
834 self.plan.managed_resources.first()
835 }
836
837 pub(crate) fn methods(&self) -> impl Iterator<Item = MethodHandler<'_>> {
838 self.plan.methods.iter().map(|plan| MethodHandler {
839 plan,
840 metadata: self.metadata,
841 })
842 }
843
844 pub(crate) fn client_type(&self) -> Ident {
845 if let Some(resource) = self.resource() {
846 format_ident!(
847 "{}",
848 format!("{} client", resource.descriptor.singular).to_case(Case::Pascal)
849 )
850 } else {
851 format_ident!(
852 "{}Client",
853 self.plan
854 .service_name
855 .trim_end_matches("Service")
856 .trim_end_matches('s')
857 )
858 }
859 }
860
861 pub(crate) fn models_path(&self) -> syn::Path {
862 ModelsPath::new(&self.config.models_path_template)
865 .expect("models_path_template already validated by generate_code")
866 .resolve(&self.plan.base_path)
867 }
868
869 pub(crate) fn models_path_crate(&self) -> syn::Path {
870 ModelsPath::new(&self.config.models_path_crate_template)
871 .expect("models_path_crate_template already validated by generate_code")
872 .resolve(&self.plan.base_path)
873 }
874}
875
876pub(crate) struct MethodHandler<'a> {
877 plan: &'a MethodPlan,
878 metadata: &'a CodeGenMetadata,
879}
880
881impl MethodHandler<'_> {
882 pub(crate) fn is_collection_method(&self) -> bool {
883 matches!(
884 self.plan.request_type,
885 RequestType::List | RequestType::Create
886 ) || (matches!(self.plan.request_type, RequestType::Custom(Pattern::Get(_)))
887 && self.plan.metadata.method_name.starts_with("List"))
888 || (matches!(
889 self.plan.request_type,
890 RequestType::Custom(Pattern::Post(_))
891 ) && self.plan.metadata.method_name.starts_with("Generate"))
892 }
893
894 pub(crate) fn output_message(&self) -> Option<MessageMeta<'_>> {
895 if self.plan.metadata.output_type.ends_with("Empty") {
896 return None;
897 }
898 self.metadata
899 .get_message_meta(&self.plan.metadata.output_type)
900 }
901
902 pub(crate) fn output_type(&self) -> Option<Ident> {
903 self.output_message()
904 .map(|t| extract_type_ident(&t.info.name))
905 }
906
907 pub(crate) fn list_output_field(&self) -> Option<&MessageField> {
908 self.output_message()?
909 .info
910 .fields
911 .iter()
912 .find(|f| !f.name.contains("page_token"))
913 }
914
915 pub(crate) fn input_message(&self) -> Option<MessageMeta<'_>> {
916 if self.plan.metadata.input_type == "Empty" {
917 return None;
918 }
919 self.metadata
920 .get_message_meta(&self.plan.metadata.input_type)
921 }
922
923 pub(crate) fn input_type(&self) -> Option<Ident> {
924 self.input_message()
925 .map(|t| extract_type_ident(&t.info.name))
926 }
927
928 pub(crate) fn builder_type(&self) -> Ident {
929 format_ident!("{}Builder", self.plan.metadata.method_name)
930 }
931
932 pub(crate) fn field_type(&self, field_type: &UnifiedType, ctx: RenderContext) -> syn::Type {
936 let rust_type = types::unified_to_rust(field_type, ctx);
937 syn::parse_str(&rust_type).expect("proper field type")
938 }
939
940 pub(crate) fn field_assignment(
942 &self,
943 field_type: &UnifiedType,
944 field_ident: &proc_macro2::Ident,
945 ctx: &RenderContext,
946 ) -> TokenStream {
947 types::field_assignment(field_type, field_ident, ctx)
948 }
949
950 pub(crate) fn required_parameters(&self) -> impl Iterator<Item = &RequestParam> {
951 self.plan
952 .parameters
953 .iter()
954 .filter(|param| !param.is_optional())
955 }
956
957 pub(crate) fn optional_parameters(&self) -> impl Iterator<Item = &RequestParam> {
958 self.plan
959 .parameters
960 .iter()
961 .filter(|param| param.is_optional())
962 }
963
964 pub(crate) fn split_body_fields(&self) -> (Vec<&BodyField>, Vec<&BodyField>) {
966 split_body_fields(self.plan)
967 }
968}
969
970pub(crate) fn extract_type_ident(full_type: &str) -> Ident {
972 let type_name = full_type.split('.').next_back().unwrap_or(full_type);
973 format_ident!("{}", type_name)
974}