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            PathBuf::from(std::env::var("OUT_DIR").unwrap())
123        } else if let Some(out_dir) = self.out_dir.as_ref() {
124            // Normal builds: use the specified directory
125            fs::create_dir_all(out_dir).unwrap_or_else(|_| {
126                panic!("failed to create out dir: {}", out_dir.display())
127            });
128            out_dir.clone()
129        } else {
130            PathBuf::from(std::env::var("OUT_DIR").unwrap())
131        };
132        // If provided, rewrites `crate::mod::Type` references to `{name}::mod::Type`
133        let rewrite_crate_name = if let Some(name) = self.rewrite_crate_name.as_ref() {
134            name
135        } else {
136            "crate"
137        };
138
139        let mut generator = ServiceGenerator {
140            // builder: self,
141            definitions: TokenStream::default(),
142        };
143
144        for service in services {
145            let mut output = String::new();
146            generator.generate(service, rewrite_crate_name);
147            generator.finalize(&mut output);
148
149            let out_file = out_dir.join(out_file(service));
150            fs::write(&out_file, output)
151                .unwrap_or_else(|_| panic!("failed to write: {}", out_file.display()));
152        }
153
154        if self.emit_composite_package {
155            let out_file = out_dir.join("packages.sdk.rs");
156            let output = generate_composite_package(services);
157            let ast = syn::parse2(output).unwrap();
158            let code = prettyplease::unparse(&ast);
159
160            fs::write(&out_file, code)
161                .unwrap_or_else(|_| panic!("failed to write: {}", out_file.display()));
162        }
163    }
164}
165
166fn out_file(service: &manual::Service) -> String {
167    format!("{}.{}.sdk.rs", service.package(), service.name())
168}
169
170fn generate_composite_package(services: &[&manual::Service]) -> TokenStream {
171    let mut includes = TokenStream::new();
172    let mut rpc_calls = TokenStream::new();
173
174    for service in services {
175        let service_name = syn::Lit::Str(syn::LitStr::new(
176            &out_file(service),
177            proc_macro2::Span::call_site(),
178        ));
179        includes.extend(quote! {
180            include!(#service_name);
181        });
182
183        let call = crate::server::server_fn_ident(service.name());
184        rpc_calls.extend(quote! { #call(), });
185    }
186
187    let rpcs = quote! {
188        vec![#rpc_calls]
189    };
190
191    quote! {
192        #includes
193
194        pub fn definitions() -> Vec<schema_builder::code_gen_types::SdkGeneratorStruct> {
195            #rpcs
196        }
197    }
198}