Skip to main content

openapi_to_rust/
bindings.rs

1//! Versioned metadata captured from the AST used to emit Rust bindings.
2//!
3//! Paths are relative to the generated module mount point. Rust token strings
4//! preserve the emitted syntax, including generics, bounds, and Option/Box
5//! nesting. Attributes describe conditional compilation and serde behavior.
6
7use crate::{GenerationResult, GeneratorError, Result};
8use quote::ToTokens;
9use serde::{Deserialize, Serialize};
10use syn::{Item, Visibility};
11
12/// An additive library result; existing `GenerationResult` literals remain valid.
13#[derive(Debug, Clone)]
14pub struct GenerationWithBindings {
15    pub generation: GenerationResult,
16    pub bindings: BindingsMetadata,
17}
18
19/// Version 1 covers models and the HTTP client, including enabled builders and
20/// public helpers. Dedicated configured streaming, server, and registry output
21/// are rejected rather than silently omitted.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct BindingsMetadata {
24    pub schema_version: u32,
25    pub generator_version: String,
26    pub module_label: String,
27    pub coverage: Vec<String>,
28    pub modules: Vec<String>,
29    pub reexports: Vec<String>,
30    pub symbols: Vec<BindingSymbol>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct BindingSymbol {
35    pub path: String,
36    pub name: String,
37    pub kind: String,
38    pub attributes: Vec<String>,
39    pub generics: String,
40    pub impl_generics: Option<String>,
41    pub impl_trait: Option<String>,
42    pub rust_type: Option<String>,
43    pub fields: Vec<BindingField>,
44    pub variants: Vec<BindingVariant>,
45    pub signature: Option<BindingSignature>,
46    pub operation: Option<BindingOperation>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct BindingField {
51    pub name: Option<String>,
52    pub index: usize,
53    pub public: bool,
54    pub wire_name: Option<String>,
55    pub rust_type: String,
56    pub attributes: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct BindingVariant {
61    pub name: String,
62    pub wire_name: Option<String>,
63    pub attributes: Vec<String>,
64    pub fields: Vec<BindingField>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct BindingSignature {
69    /// Canonical token spelling of the signature shared with Rust emission.
70    pub rust: String,
71    pub asynchronous: bool,
72    pub receiver: Option<String>,
73    pub arguments: Vec<BindingArgument>,
74    pub return_type: String,
75    pub generics: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub struct BindingArgument {
80    pub name: String,
81    pub rust_type: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct BindingOperation {
86    pub operation_id: String,
87    pub source_json_pointer: String,
88    pub source_method: String,
89    pub source_path: String,
90    pub source_operation_id: Option<String>,
91    pub webhook: bool,
92    pub response_statuses: Vec<String>,
93    /// Exact statuses excluded from an OpenAPI status-class representation.
94    pub response_excluded_statuses: Vec<String>,
95    pub response_media_type: Option<String>,
96    pub response_kind: String,
97    pub consumption: String,
98}
99
100impl BindingsMetadata {
101    pub(crate) fn new(module_label: &str, http_client: bool) -> Self {
102        let mut coverage = vec!["models".to_string(), "public_helpers".to_string()];
103        if http_client {
104            coverage.extend(["http_client".to_string(), "enabled_builders".to_string()]);
105        }
106        Self {
107            schema_version: 1,
108            generator_version: crate::VERSION.to_string(),
109            module_label: module_label.to_string(),
110            coverage,
111            modules: Vec::new(),
112            reexports: Vec::new(),
113            symbols: Vec::new(),
114        }
115    }
116
117    /// Stable JSON with no timestamps or machine-specific input/output paths.
118    pub fn to_json(&self) -> Result<String> {
119        let mut output = serde_json::to_string_pretty(self).map_err(|error| {
120            GeneratorError::CodeGenError(format!("Failed to serialize binding metadata: {error}"))
121        })?;
122        output.push('\n');
123        Ok(output)
124    }
125
126    pub(crate) fn finish(&mut self) {
127        self.modules.sort();
128        self.modules.dedup();
129        self.reexports.sort();
130        self.reexports.dedup();
131        self.symbols.sort_by(|a, b| {
132            (&a.path, &a.kind, &a.impl_trait, &a.attributes).cmp(&(
133                &b.path,
134                &b.kind,
135                &b.impl_trait,
136                &b.attributes,
137            ))
138        });
139    }
140
141    /// Inspect the emitter's already parsed AST before prettyplease formats it.
142    pub(crate) fn collect(&mut self, module: &str, ast: &syn::File) {
143        if module == "sse" {
144            self.coverage.push("sse_runtime".to_string());
145        }
146        self.modules.push(module.to_string());
147        self.collect_items(module, &ast.items);
148    }
149
150    fn collect_items(&mut self, module: &str, items: &[Item]) {
151        let public_types: std::collections::BTreeSet<_> = items
152            .iter()
153            .filter_map(|item| match item {
154                Item::Struct(item) if public(&item.vis) => Some(item.ident.to_string()),
155                Item::Enum(item) if public(&item.vis) => Some(item.ident.to_string()),
156                Item::Type(item) if public(&item.vis) => Some(item.ident.to_string()),
157                _ => None,
158            })
159            .collect();
160        for item in items {
161            match item {
162                Item::Struct(item) if public(&item.vis) => {
163                    let mut symbol = symbol(
164                        module,
165                        &item.ident.to_string(),
166                        "struct",
167                        &item.attrs,
168                        &item.generics,
169                    );
170                    symbol.fields = fields(
171                        &item.fields,
172                        serde_derived(&item.attrs),
173                        serde_string(&item.attrs, "rename_all").as_deref(),
174                    );
175                    self.symbols.push(symbol);
176                }
177                Item::Enum(item) if public(&item.vis) => {
178                    let mut symbol = symbol(
179                        module,
180                        &item.ident.to_string(),
181                        "enum",
182                        &item.attrs,
183                        &item.generics,
184                    );
185                    symbol.variants = item
186                        .variants
187                        .iter()
188                        .map(|variant| BindingVariant {
189                            name: variant.ident.to_string(),
190                            wire_name: wire_name(
191                                &variant.attrs,
192                                serde_derived(&item.attrs)
193                                    .then(|| {
194                                        renamed(
195                                            &variant.ident.to_string(),
196                                            serde_string(&item.attrs, "rename_all").as_deref(),
197                                        )
198                                    })
199                                    .as_deref(),
200                            ),
201                            attributes: attributes(&variant.attrs),
202                            fields: fields(
203                                &variant.fields,
204                                serde_derived(&item.attrs),
205                                serde_string(&variant.attrs, "rename_all").as_deref(),
206                            )
207                            .into_iter()
208                            .map(|mut field| {
209                                field.public = true;
210                                field
211                            })
212                            .collect(),
213                        })
214                        .collect();
215                    self.symbols.push(symbol);
216                }
217                Item::Type(item) if public(&item.vis) => {
218                    let mut symbol = symbol(
219                        module,
220                        &item.ident.to_string(),
221                        "alias",
222                        &item.attrs,
223                        &item.generics,
224                    );
225                    symbol.rust_type = Some(tokens(&item.ty));
226                    self.symbols.push(symbol);
227                }
228                Item::Fn(item) if public(&item.vis) => {
229                    let mut symbol = symbol(
230                        module,
231                        &item.sig.ident.to_string(),
232                        "function",
233                        &item.attrs,
234                        &item.sig.generics,
235                    );
236                    symbol.signature = Some(signature(&item.sig));
237                    self.symbols.push(symbol);
238                }
239                Item::Impl(item) => {
240                    let syn::Type::Path(owner) = item.self_ty.as_ref() else {
241                        continue;
242                    };
243                    let Some(owner) = owner.path.segments.last() else {
244                        continue;
245                    };
246                    if !public_types.contains(&owner.ident.to_string()) {
247                        continue;
248                    }
249                    for method in &item.items {
250                        match method {
251                            syn::ImplItem::Fn(method) if public(&method.vis) => {
252                                let owner_path = format!("{module}::{}", owner.ident);
253                                let mut symbol = symbol(
254                                    &owner_path,
255                                    &method.sig.ident.to_string(),
256                                    "method",
257                                    &method.attrs,
258                                    &method.sig.generics,
259                                );
260                                symbol.attributes.extend(attributes(&item.attrs));
261                                symbol.impl_generics = Some(generics(&item.generics));
262                                symbol.impl_trait =
263                                    item.trait_.as_ref().map(|(_, path, _)| tokens(path));
264                                symbol.signature = Some(signature(&method.sig));
265                                self.symbols.push(symbol);
266                            }
267                            syn::ImplItem::Const(value) if public(&value.vis) => {
268                                let owner_path = format!("{module}::{}", owner.ident);
269                                let mut symbol = symbol(
270                                    &owner_path,
271                                    &value.ident.to_string(),
272                                    "constant",
273                                    &value.attrs,
274                                    &syn::Generics::default(),
275                                );
276                                symbol.rust_type = Some(tokens(&value.ty));
277                                symbol.impl_generics = Some(generics(&item.generics));
278                                self.symbols.push(symbol);
279                            }
280                            _ => {}
281                        }
282                    }
283                }
284                Item::Mod(item) if public(&item.vis) => {
285                    let path = format!("{module}::{}", item.ident);
286                    self.modules.push(path.clone());
287                    self.symbols.push(symbol(
288                        module,
289                        &item.ident.to_string(),
290                        "module",
291                        &item.attrs,
292                        &syn::Generics::default(),
293                    ));
294                    if let Some((_, items)) = &item.content {
295                        self.collect_items(&path, items);
296                    }
297                }
298                Item::Const(item) if public(&item.vis) => {
299                    let mut symbol = symbol(
300                        module,
301                        &item.ident.to_string(),
302                        "constant",
303                        &item.attrs,
304                        &syn::Generics::default(),
305                    );
306                    symbol.rust_type = Some(tokens(&item.ty));
307                    self.symbols.push(symbol);
308                }
309                Item::Use(item) if public(&item.vis) => {
310                    self.reexports
311                        .push(format!("{module}::{}", tokens(&item.tree)));
312                }
313                _ => {}
314            }
315        }
316    }
317}
318
319fn public(visibility: &Visibility) -> bool {
320    matches!(visibility, Visibility::Public(_))
321}
322fn tokens(value: &impl ToTokens) -> String {
323    value.to_token_stream().to_string()
324}
325fn attributes(attrs: &[syn::Attribute]) -> Vec<String> {
326    attrs
327        .iter()
328        .filter(|attribute| !attribute.path().is_ident("doc"))
329        .map(tokens)
330        .collect()
331}
332fn generics(value: &syn::Generics) -> String {
333    let params = tokens(value);
334    match &value.where_clause {
335        Some(clause) => format!("{params} {}", tokens(clause)).trim().to_string(),
336        None => params,
337    }
338}
339fn symbol(
340    module: &str,
341    name: &str,
342    kind: &str,
343    attrs: &[syn::Attribute],
344    params: &syn::Generics,
345) -> BindingSymbol {
346    BindingSymbol {
347        path: format!("{module}::{name}"),
348        name: name.to_string(),
349        kind: kind.to_string(),
350        attributes: attributes(attrs),
351        generics: generics(params),
352        impl_generics: None,
353        impl_trait: None,
354        rust_type: None,
355        fields: Vec::new(),
356        variants: Vec::new(),
357        signature: None,
358        operation: None,
359    }
360}
361fn fields(value: &syn::Fields, default_wire: bool, rename_all: Option<&str>) -> Vec<BindingField> {
362    value
363        .iter()
364        .enumerate()
365        .map(|(index, field)| {
366            let name = field.ident.as_ref().map(ToString::to_string);
367            let fallback = name
368                .as_deref()
369                .filter(|_| default_wire)
370                .map(|name| renamed(name, rename_all));
371            BindingField {
372                wire_name: wire_name(&field.attrs, fallback.as_deref()),
373                name,
374                index,
375                public: public(&field.vis),
376                rust_type: tokens(&field.ty),
377                attributes: attributes(&field.attrs),
378            }
379        })
380        .collect()
381}
382fn serde_derived(attrs: &[syn::Attribute]) -> bool {
383    attrs.iter().filter(|attribute| attribute.path().is_ident("derive")).any(|attribute| {
384        attribute.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated)
385            .is_ok_and(|paths| paths.iter().any(|path| path.segments.last().is_some_and(|segment| segment.ident == "Serialize" || segment.ident == "Deserialize")))
386    }) && !attrs.iter().filter(|attribute| attribute.path().is_ident("serde")).any(|attribute| {
387        attribute.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
388            .is_ok_and(|parts| parts.iter().any(|part| matches!(part, syn::Meta::Path(path) if path.is_ident("untagged"))))
389    })
390}
391fn serde_string(attrs: &[syn::Attribute], key: &str) -> Option<String> {
392    for attribute in attrs
393        .iter()
394        .filter(|attribute| attribute.path().is_ident("serde"))
395    {
396        if let Ok(parts) = attribute.parse_args_with(
397            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
398        ) {
399            for part in parts {
400                if let syn::Meta::NameValue(value) = part {
401                    if value.path.is_ident(key) {
402                        if let syn::Expr::Lit(syn::ExprLit {
403                            lit: syn::Lit::Str(value),
404                            ..
405                        }) = value.value
406                        {
407                            return Some(value.value());
408                        }
409                    }
410                }
411            }
412        }
413    }
414    None
415}
416fn renamed(name: &str, rule: Option<&str>) -> String {
417    use heck::{
418        ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase,
419        ToUpperCamelCase,
420    };
421    let name = name.strip_prefix("r#").unwrap_or(name);
422    match rule {
423        Some("lowercase") => name.to_lowercase(),
424        Some("UPPERCASE") => name.to_uppercase(),
425        Some("PascalCase") => name.to_upper_camel_case(),
426        Some("camelCase") => name.to_lower_camel_case(),
427        Some("snake_case") => name.to_snake_case(),
428        Some("SCREAMING_SNAKE_CASE") => name.to_shouty_snake_case(),
429        Some("kebab-case") => name.to_kebab_case(),
430        Some("SCREAMING-KEBAB-CASE") => name.to_shouty_kebab_case(),
431        _ => name.to_string(),
432    }
433}
434fn wire_name(attrs: &[syn::Attribute], fallback: Option<&str>) -> Option<String> {
435    for attribute in attrs
436        .iter()
437        .filter(|attribute| attribute.path().is_ident("serde"))
438    {
439        let parsed = attribute.parse_args_with(
440            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
441        );
442        if let Ok(parts) = parsed {
443            for part in parts {
444                match part {
445                    syn::Meta::Path(path) if path.is_ident("flatten") || path.is_ident("skip") => {
446                        return None;
447                    }
448                    syn::Meta::NameValue(value) if value.path.is_ident("rename") => {
449                        if let syn::Expr::Lit(syn::ExprLit {
450                            lit: syn::Lit::Str(value),
451                            ..
452                        }) = value.value
453                        {
454                            return Some(value.value());
455                        }
456                    }
457                    _ => {}
458                }
459            }
460        }
461    }
462    fallback.map(|name| name.strip_prefix("r#").unwrap_or(name).to_string())
463}
464fn signature(value: &syn::Signature) -> BindingSignature {
465    // Optional trailing punctuation changes with prettyplease line wrapping;
466    // canonical signatures retain the same AST semantics on either side.
467    let mut canonical = value.clone();
468    if canonical.inputs.trailing_punct() {
469        canonical.inputs.pop_punct();
470    }
471    if canonical.generics.params.trailing_punct() {
472        canonical.generics.params.pop_punct();
473    }
474    let mut receiver = None;
475    let mut arguments = Vec::new();
476    for argument in &value.inputs {
477        match argument {
478            syn::FnArg::Receiver(value) => receiver = Some(tokens(value)),
479            syn::FnArg::Typed(value) => arguments.push(BindingArgument {
480                name: tokens(&value.pat),
481                rust_type: tokens(&value.ty),
482            }),
483        }
484    }
485    let return_type = match &value.output {
486        syn::ReturnType::Default => "()".to_string(),
487        syn::ReturnType::Type(_, ty) => tokens(ty),
488    };
489    BindingSignature {
490        rust: tokens(&canonical),
491        asynchronous: value.asyncness.is_some(),
492        receiver,
493        arguments,
494        return_type,
495        generics: generics(&value.generics),
496    }
497}