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