kanamaru_build/
utils.rs

1#![allow(dead_code)]
2
3use proc_macro2::{Delimiter, Group, Literal, Punct, Spacing, Span, TokenStream};
4use quote::TokenStreamExt;
5use syn::Ident;
6#[cfg(feature = "prost")]
7pub mod service_generator_chain;
8
9/// Service generation trait.
10///
11/// This trait can be implemented and consumed
12/// by `client::generate` and `server::generate`
13/// to allow any codegen module to generate service
14/// abstractions.
15pub trait Service {
16    /// Comment type.
17    type Comment: AsRef<str>;
18
19    /// Method type.
20    type Method: Method;
21
22    /// Name of service.
23    fn name(&self) -> &str;
24    /// Package name of service.
25    fn package(&self) -> &str;
26    /// Identifier used to generate type name.
27    fn identifier(&self) -> &str;
28    /// Methods provided by service.
29    fn methods(&self) -> &[Self::Method];
30    /// Get comments about this item.
31    fn comment(&self) -> &[Self::Comment];
32}
33
34/// Method generation trait.
35///
36/// Each service contains a set of generic
37/// `Methods`'s that will be used by codegen
38/// to generate abstraction implementations for
39/// the provided methods.
40pub trait Method {
41    /// Comment type.
42    type Comment: AsRef<str>;
43
44    /// Name of method.
45    fn name(&self) -> &str;
46    /// Identifier used to generate type name.
47    fn identifier(&self) -> &str;
48    /// Method is streamed by client.
49    fn client_streaming(&self) -> bool;
50    /// Method is streamed by server.
51    fn server_streaming(&self) -> bool;
52    /// Get comments about this item.
53    fn comment(&self) -> &[Self::Comment];
54    /// Method is deprecated.
55    fn deprecated(&self) -> bool {
56        false
57    }
58    /// Type name of request and response.
59    fn request_response_name(
60        &self,
61        proto_path: &str,
62        compile_well_known_types: bool,
63    ) -> (TokenStream, TokenStream);
64}
65
66/// Attributes that will be added to `mod` and `struct` items.
67#[derive(Debug, Default, Clone)]
68pub struct Attributes {
69    /// `mod` attributes.
70    module: Vec<(String, String)>,
71    /// `struct` attributes.
72    structure: Vec<(String, String)>,
73}
74
75impl Attributes {
76    pub fn for_mod(&self, name: &str) -> Vec<syn::Attribute> {
77        generate_attributes(name, &self.module)
78    }
79
80    pub fn for_struct(&self, name: &str) -> Vec<syn::Attribute> {
81        generate_attributes(name, &self.structure)
82    }
83
84    /// Add an attribute that will be added to `mod` items matching the given pattern.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// # use kanamaru_build::utils::*;
90    /// let mut attributes = Attributes::default();
91    /// attributes.push_mod("my.proto.package", r#"#[cfg(feature = "server")]"#);
92    /// ```
93    pub fn push_mod(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
94        self.module.push((pattern.into(), attr.into()));
95    }
96
97    /// Add an attribute that will be added to `struct` items matching the given pattern.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// # use kanamaru_build::utils::*;
103    /// let mut attributes = Attributes::default();
104    /// attributes.push_struct("EchoService", "#[derive(PartialEq)]");
105    /// ```
106    pub fn push_struct(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
107        self.structure.push((pattern.into(), attr.into()));
108    }
109}
110
111pub(crate) fn format_service_name<T: Service>(service: &T, emit_package: bool) -> String {
112    let package = if emit_package { service.package() } else { "" };
113    format!(
114        "{}{}{}",
115        package,
116        if package.is_empty() { "" } else { "." },
117        service.identifier(),
118    )
119}
120
121pub(crate) fn format_method_path<T: Service>(
122    service: &T,
123    method: &T::Method,
124    emit_package: bool,
125) -> String {
126    format!(
127        "{}/{}",
128        format_service_name(service, emit_package),
129        method.identifier()
130    )
131}
132
133pub(crate) fn format_method_name<T: Service>(
134    service: &T,
135    method: &T::Method,
136    emit_package: bool,
137) -> String {
138    format!(
139        "{}.{}",
140        format_service_name(service, emit_package),
141        method.identifier()
142    )
143}
144
145// Generates attributes given a list of (`pattern`, `attribute`) pairs. If `pattern` matches `name`, `attribute` will be included.
146pub(crate) fn generate_attributes<'a>(
147    name: &str,
148    attrs: impl IntoIterator<Item = &'a (String, String)>,
149) -> Vec<syn::Attribute> {
150    attrs
151        .into_iter()
152        .filter(|(matcher, _)| match_name(matcher, name))
153        .flat_map(|(_, attr)| {
154            // attributes cannot be parsed directly, so we pretend they're on a struct
155            syn::parse_str::<syn::DeriveInput>(&format!("{}\nstruct fake;", attr))
156                .unwrap()
157                .attrs
158        })
159        .collect::<Vec<_>>()
160}
161
162pub(crate) fn generate_deprecated() -> TokenStream {
163    let mut deprecated_stream = TokenStream::new();
164    deprecated_stream.append(Ident::new("deprecated", Span::call_site()));
165
166    let group = Group::new(Delimiter::Bracket, deprecated_stream);
167
168    let mut stream = TokenStream::new();
169    stream.append(Punct::new('#', Spacing::Alone));
170    stream.append(group);
171
172    stream
173}
174
175// Generate a singular line of a doc comment
176pub(crate) fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
177    let comment = comment.as_ref();
178
179    let comment = if !comment.starts_with(' ') {
180        format!(" {}", comment)
181    } else {
182        comment.to_string()
183    };
184
185    let mut doc_stream = TokenStream::new();
186
187    doc_stream.append(Ident::new("doc", Span::call_site()));
188    doc_stream.append(Punct::new('=', Spacing::Alone));
189    doc_stream.append(Literal::string(comment.as_ref()));
190
191    let group = Group::new(Delimiter::Bracket, doc_stream);
192
193    let mut stream = TokenStream::new();
194    stream.append(Punct::new('#', Spacing::Alone));
195    stream.append(group);
196    stream
197}
198
199// Generate a larger doc comment composed of many lines of doc comments
200pub(crate) fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream {
201    let mut stream = TokenStream::new();
202
203    for comment in comments {
204        stream.extend(generate_doc_comment(comment));
205    }
206
207    stream
208}
209
210// Checks whether a path pattern matches a given path.
211pub(crate) fn match_name(pattern: &str, path: &str) -> bool {
212    if pattern.is_empty() {
213        false
214    } else if pattern == "." || pattern == path {
215        true
216    } else {
217        let pattern_segments = pattern.split('.').collect::<Vec<_>>();
218        let path_segments = path.split('.').collect::<Vec<_>>();
219
220        if &pattern[..1] == "." {
221            // prefix match
222            if pattern_segments.len() > path_segments.len() {
223                false
224            } else {
225                pattern_segments[..] == path_segments[..pattern_segments.len()]
226            }
227        // suffix match
228        } else if pattern_segments.len() > path_segments.len() {
229            false
230        } else {
231            pattern_segments[..] == path_segments[path_segments.len() - pattern_segments.len()..]
232        }
233    }
234}
235
236pub(crate) fn naive_snake_case(name: &str) -> String {
237    let mut s = String::new();
238    let mut it = name.chars().peekable();
239
240    while let Some(x) = it.next() {
241        s.push(x.to_ascii_lowercase());
242        if let Some(y) = it.peek() {
243            if y.is_uppercase() {
244                s.push('_');
245            }
246        }
247    }
248
249    s
250}