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,no_run
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 std::{
39 fs,
40 path::{Path, PathBuf},
41};
42use tonic_build::{manual, Service};
43
44struct ServiceGenerator {
45 // builder: Builder,
46 definitions: TokenStream,
47}
48
49impl ServiceGenerator {
50 fn generate(&mut self, service: &manual::Service, rewrite_crate: &str) {
51 let definition = CodeGenBuilder::new()
52 .emit_package(true)
53 .compile_well_known_types(false)
54 .generate_server_definition(service, rewrite_crate, "");
55
56 self.definitions.extend(definition);
57 }
58
59 fn finalize(&mut self, buf: &mut String) {
60 if !self.definitions.is_empty() {
61 let definitions = &self.definitions;
62
63 let server_definitions = quote::quote! {
64 #definitions
65 };
66
67 let ast: syn::File =
68 syn::parse2(server_definitions).expect("not a valid tokenstream");
69 let code = prettyplease::unparse(&ast);
70 buf.push_str(&code);
71
72 self.definitions = TokenStream::default();
73 }
74 }
75}
76
77/// Service generator builder.
78#[derive(Debug, Default)]
79pub struct Builder {
80 rewrite_crate_name: Option<String>,
81 out_dir: Option<PathBuf>,
82}
83
84impl Builder {
85 /// Create a new Builder
86 pub fn new() -> Self {
87 Self::default()
88 }
89
90 /// Rewrite `crate::` references to provided crate
91 ///
92 pub fn rewrite_crate(mut self, crate_name: &str) -> Self {
93 self.rewrite_crate_name = Some(crate_name.to_string());
94 self
95 }
96
97 /// Set the output directory to generate code to.
98 ///
99 /// Defaults to the `OUT_DIR` environment variable.
100 pub fn out_dir(mut self, out_dir: impl AsRef<Path>) -> Self {
101 self.out_dir = Some(out_dir.as_ref().to_path_buf());
102 self
103 }
104
105 /// Performs code generation for the provided services.
106 ///
107 /// Generated services will be output into the directory specified by `out_dir`
108 /// with files named `<package_name>.<service_name>.sdk.rs`.
109 pub fn compile(self, services: &[&manual::Service]) {
110 let out_dir = if let Some(out_dir) = self.out_dir.as_ref() {
111 fs::create_dir_all(out_dir)
112 .expect(&format!("failed to create out dir: {}", out_dir.display()));
113 out_dir.clone()
114 } else {
115 PathBuf::from(std::env::var("OUT_DIR").unwrap())
116 };
117
118 let rewrite_crate_name = if let Some(name) = self.rewrite_crate_name.as_ref() {
119 name
120 } else {
121 "crate"
122 };
123
124 let mut generator = ServiceGenerator {
125 // builder: self,
126 definitions: TokenStream::default(),
127 };
128
129 for service in services {
130 let mut output = String::new();
131 generator.generate(service, rewrite_crate_name);
132 generator.finalize(&mut output);
133
134 let out_file =
135 out_dir.join(format!("{}.{}.sdk.rs", service.package(), service.name()));
136 fs::write(&out_file, output)
137 .expect(&format!("failed to write: {}", out_file.display()));
138 }
139 }
140}