architect_api_schema_builder/
manual.rs

1//! This module provides utilities for generating `tonic` service definitions for use by our client
2//! sdk code generators.
3//!
4//! [2024-12-30] dkasten: fork of tonic-build/src/manual.rs at
5//! https://github.com/hyperium/tonic/commit/1c5150aaf62d6e72ce6c07966a9f19ceedb52702
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let greeter_service = tonic_build::manual::Service::builder()
12//!         .name("Greeter")
13//!         .package("helloworld")
14//!         .method(
15//!             tonic_build::manual::Method::builder()
16//!                 .name("say_hello")
17//!                 .route_name("SayHello")
18//!                 // Provide the path to the Request type
19//!                 .input_type("crate::HelloRequest")
20//!                 // Provide the path to the Response type
21//!                 .output_type("super::HelloResponse")
22//!                 // Provide the path to the Codec to use
23//!                 .codec_path("crate::JsonCodec")
24//!                 .build(),
25//!         )
26//!         .build();
27//!
28//!     // note we run first with a borrowed reference since tonic takes ownership
29//!     sdk_build::manual::Builder::new().compile(&[&greeter_service]);
30//!     tonic_build::manual::Builder::new().compile(&[greeter_service]);
31//!     Ok(())
32//! }
33//! ```
34// This module forked from https://github.com/hyperium/tonic/commit/1c5150aaf62d6e72ce6c07966a9f19ceedb52702
35
36use crate::code_gen::CodeGenBuilder;
37use proc_macro2::TokenStream;
38use quote::quote;
39use std::{
40    fs,
41    path::{Path, PathBuf},
42};
43use tonic_build::{manual, Service};
44
45struct ServiceGenerator {
46    // builder: Builder,
47    definitions: TokenStream,
48}
49
50impl ServiceGenerator {
51    fn generate(&mut self, service: &manual::Service, rewrite_crate: &str) {
52        let definition = CodeGenBuilder::new()
53            .emit_package(true)
54            .compile_well_known_types(false)
55            .generate_server_definition(service, rewrite_crate, "");
56
57        self.definitions.extend(definition);
58    }
59
60    fn finalize(&mut self, buf: &mut String) {
61        if !self.definitions.is_empty() {
62            let definitions = &self.definitions;
63
64            let server_definitions = quote::quote! {
65                #definitions
66            };
67
68            let ast: syn::File =
69                syn::parse2(server_definitions).expect("not a valid tokenstream");
70            let code = prettyplease::unparse(&ast);
71            buf.push_str(&code);
72
73            self.definitions = TokenStream::default();
74        }
75    }
76}
77
78/// Service generator builder.
79#[derive(Debug, Default)]
80pub struct Builder {
81    rewrite_crate_name: Option<String>,
82    out_dir: Option<PathBuf>,
83    emit_composite_package: bool,
84}
85
86impl Builder {
87    /// Create a new Builder
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Rewrite `crate::` references to provided crate
93    ///
94    pub fn rewrite_crate(mut self, crate_name: &str) -> Self {
95        self.rewrite_crate_name = Some(crate_name.to_string());
96        self
97    }
98
99    /// Set the output directory to generate code to.
100    ///
101    /// Defaults to the `OUT_DIR` environment variable.
102    pub fn out_dir(mut self, out_dir: impl AsRef<Path>) -> Self {
103        self.out_dir = Some(out_dir.as_ref().to_path_buf());
104        self
105    }
106
107    /// Set whether to emit a composite package.
108    ///
109    /// Defaults to false
110    pub fn emit_composite_package(mut self, emit_composite_package: bool) -> Self {
111        self.emit_composite_package = emit_composite_package;
112        self
113    }
114
115    /// Performs code generation for the provided services.
116    ///
117    /// Generated services will be output into the directory specified by `out_dir`
118    /// with files named `<package_name>.<service_name>.sdk.rs`.
119    pub fn compile(self, services: &[&manual::Service]) {
120        let out_dir = if std::env::var("DOCS_RS").is_ok() {
121            // On docs.rs: always use OUT_DIR (writable)
122            println!("cargo:warning=Using OUT_DIR for codegen because we're building on docs.rs");
123            PathBuf::from(std::env::var("OUT_DIR").unwrap())
124        } else if let Some(out_dir) = self.out_dir.as_ref() {
125            // Normal builds: use the specified directory
126            fs::create_dir_all(out_dir).unwrap_or_else(|_| {
127                panic!("failed to create out dir: {}", out_dir.display())
128            });
129            out_dir.clone()
130        } else {
131            PathBuf::from(std::env::var("OUT_DIR").unwrap())
132        };
133        // If provided, rewrites `crate::mod::Type` references to `{name}::mod::Type`
134        let rewrite_crate_name = if let Some(name) = self.rewrite_crate_name.as_ref() {
135            name
136        } else {
137            "crate"
138        };
139
140        let mut generator = ServiceGenerator {
141            // builder: self,
142            definitions: TokenStream::default(),
143        };
144
145        for service in services {
146            let mut output = String::new();
147            generator.generate(service, rewrite_crate_name);
148            generator.finalize(&mut output);
149
150            let out_file = out_dir.join(out_file(service));
151            fs::write(&out_file, output)
152                .unwrap_or_else(|_| panic!("failed to write: {}", out_file.display()));
153        }
154
155        if self.emit_composite_package {
156            let out_file = out_dir.join("packages.sdk.rs");
157            let output = generate_composite_package(services);
158            let ast = syn::parse2(output).unwrap();
159            let code = prettyplease::unparse(&ast);
160
161            fs::write(&out_file, code)
162                .unwrap_or_else(|_| panic!("failed to write: {}", out_file.display()));
163        }
164    }
165}
166
167fn out_file(service: &manual::Service) -> String {
168    format!("{}.{}.sdk.rs", service.package(), service.name())
169}
170
171fn generate_composite_package(services: &[&manual::Service]) -> TokenStream {
172    let mut includes = TokenStream::new();
173    let mut rpc_calls = TokenStream::new();
174
175    for service in services {
176        let service_name = syn::Lit::Str(syn::LitStr::new(
177            &out_file(service),
178            proc_macro2::Span::call_site(),
179        ));
180        includes.extend(quote! {
181            include!(#service_name);
182        });
183
184        let call = crate::server::server_fn_ident(service.name());
185        rpc_calls.extend(quote! { #call(), });
186    }
187
188    let rpcs = quote! {
189        vec![#rpc_calls]
190    };
191
192    quote! {
193        #includes
194
195        pub fn definitions() -> Vec<schema_builder::code_gen_types::SdkGeneratorStruct> {
196            #rpcs
197        }
198    }
199}