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    pub multipart_filenames: bool,
99}
100
101impl BindingsMetadata {
102    pub(crate) fn new(module_label: &str, http_client: bool) -> Self {
103        let mut coverage = vec!["models".to_string(), "public_helpers".to_string()];
104        if http_client {
105            coverage.extend(["http_client".to_string(), "enabled_builders".to_string()]);
106        }
107        Self {
108            schema_version: 1,
109            generator_version: crate::VERSION.to_string(),
110            module_label: module_label.to_string(),
111            coverage,
112            modules: Vec::new(),
113            reexports: Vec::new(),
114            symbols: Vec::new(),
115        }
116    }
117
118    /// Stable JSON with no timestamps or machine-specific input/output paths.
119    pub fn to_json(&self) -> Result<String> {
120        let mut output = serde_json::to_string_pretty(self).map_err(|error| {
121            GeneratorError::CodeGenError(format!("Failed to serialize binding metadata: {error}"))
122        })?;
123        output.push('\n');
124        Ok(output)
125    }
126
127    pub(crate) fn finish(&mut self) {
128        self.modules.sort();
129        self.modules.dedup();
130        self.reexports.sort();
131        self.reexports.dedup();
132        self.symbols.sort_by(|a, b| {
133            (&a.path, &a.kind, &a.impl_trait, &a.attributes).cmp(&(
134                &b.path,
135                &b.kind,
136                &b.impl_trait,
137                &b.attributes,
138            ))
139        });
140    }
141
142    /// Inspect the emitter's already parsed AST before prettyplease formats it.
143    pub(crate) fn collect(&mut self, module: &str, ast: &syn::File) {
144        if module == "sse" {
145            self.coverage.push("sse_runtime".to_string());
146        }
147        self.modules.push(module.to_string());
148        self.collect_items(module, &ast.items);
149    }
150
151    fn collect_items(&mut self, module: &str, items: &[Item]) {
152        let public_types: std::collections::BTreeSet<_> = items
153            .iter()
154            .filter_map(|item| match item {
155                Item::Struct(item) if public(&item.vis) => Some(item.ident.to_string()),
156                Item::Enum(item) if public(&item.vis) => Some(item.ident.to_string()),
157                Item::Type(item) if public(&item.vis) => Some(item.ident.to_string()),
158                _ => None,
159            })
160            .collect();
161        for item in items {
162            match item {
163                Item::Struct(item) if public(&item.vis) => {
164                    let mut symbol = symbol(
165                        module,
166                        &item.ident.to_string(),
167                        "struct",
168                        &item.attrs,
169                        &item.generics,
170                    );
171                    symbol.fields = fields(
172                        &item.fields,
173                        serde_derived(&item.attrs),
174                        serde_string(&item.attrs, "rename_all").as_deref(),
175                    );
176                    self.symbols.push(symbol);
177                }
178                Item::Enum(item) if public(&item.vis) => {
179                    let mut symbol = symbol(
180                        module,
181                        &item.ident.to_string(),
182                        "enum",
183                        &item.attrs,
184                        &item.generics,
185                    );
186                    symbol.variants = item
187                        .variants
188                        .iter()
189                        .map(|variant| BindingVariant {
190                            name: variant.ident.to_string(),
191                            wire_name: wire_name(
192                                &variant.attrs,
193                                serde_derived(&item.attrs)
194                                    .then(|| {
195                                        renamed(
196                                            &variant.ident.to_string(),
197                                            serde_string(&item.attrs, "rename_all").as_deref(),
198                                        )
199                                    })
200                                    .as_deref(),
201                            ),
202                            attributes: attributes(&variant.attrs),
203                            fields: fields(
204                                &variant.fields,
205                                serde_derived(&item.attrs),
206                                serde_string(&variant.attrs, "rename_all").as_deref(),
207                            )
208                            .into_iter()
209                            .map(|mut field| {
210                                field.public = true;
211                                field
212                            })
213                            .collect(),
214                        })
215                        .collect();
216                    self.symbols.push(symbol);
217                }
218                Item::Type(item) if public(&item.vis) => {
219                    let mut symbol = symbol(
220                        module,
221                        &item.ident.to_string(),
222                        "alias",
223                        &item.attrs,
224                        &item.generics,
225                    );
226                    symbol.rust_type = Some(tokens(&item.ty));
227                    self.symbols.push(symbol);
228                }
229                Item::Fn(item) if public(&item.vis) => {
230                    let mut symbol = symbol(
231                        module,
232                        &item.sig.ident.to_string(),
233                        "function",
234                        &item.attrs,
235                        &item.sig.generics,
236                    );
237                    symbol.signature = Some(signature(&item.sig));
238                    self.symbols.push(symbol);
239                }
240                Item::Impl(item) => {
241                    let syn::Type::Path(owner) = item.self_ty.as_ref() else {
242                        continue;
243                    };
244                    let Some(owner) = owner.path.segments.last() else {
245                        continue;
246                    };
247                    if !public_types.contains(&owner.ident.to_string()) {
248                        continue;
249                    }
250                    for method in &item.items {
251                        match method {
252                            syn::ImplItem::Fn(method) if public(&method.vis) => {
253                                let owner_path = format!("{module}::{}", owner.ident);
254                                let mut symbol = symbol(
255                                    &owner_path,
256                                    &method.sig.ident.to_string(),
257                                    "method",
258                                    &method.attrs,
259                                    &method.sig.generics,
260                                );
261                                symbol.attributes.extend(attributes(&item.attrs));
262                                symbol.impl_generics = Some(generics(&item.generics));
263                                symbol.impl_trait =
264                                    item.trait_.as_ref().map(|(_, path, _)| tokens(path));
265                                symbol.signature = Some(signature(&method.sig));
266                                self.symbols.push(symbol);
267                            }
268                            syn::ImplItem::Const(value) if public(&value.vis) => {
269                                let owner_path = format!("{module}::{}", owner.ident);
270                                let mut symbol = symbol(
271                                    &owner_path,
272                                    &value.ident.to_string(),
273                                    "constant",
274                                    &value.attrs,
275                                    &syn::Generics::default(),
276                                );
277                                symbol.rust_type = Some(tokens(&value.ty));
278                                symbol.impl_generics = Some(generics(&item.generics));
279                                self.symbols.push(symbol);
280                            }
281                            _ => {}
282                        }
283                    }
284                }
285                Item::Mod(item) if public(&item.vis) => {
286                    let path = format!("{module}::{}", item.ident);
287                    self.modules.push(path.clone());
288                    self.symbols.push(symbol(
289                        module,
290                        &item.ident.to_string(),
291                        "module",
292                        &item.attrs,
293                        &syn::Generics::default(),
294                    ));
295                    if let Some((_, items)) = &item.content {
296                        self.collect_items(&path, items);
297                    }
298                }
299                Item::Const(item) if public(&item.vis) => {
300                    let mut symbol = symbol(
301                        module,
302                        &item.ident.to_string(),
303                        "constant",
304                        &item.attrs,
305                        &syn::Generics::default(),
306                    );
307                    symbol.rust_type = Some(tokens(&item.ty));
308                    self.symbols.push(symbol);
309                }
310                Item::Use(item) if public(&item.vis) => {
311                    self.reexports
312                        .push(format!("{module}::{}", tokens(&item.tree)));
313                }
314                _ => {}
315            }
316        }
317    }
318}
319
320fn public(visibility: &Visibility) -> bool {
321    matches!(visibility, Visibility::Public(_))
322}
323fn tokens(value: &impl ToTokens) -> String {
324    value.to_token_stream().to_string()
325}
326fn attributes(attrs: &[syn::Attribute]) -> Vec<String> {
327    attrs
328        .iter()
329        .filter(|attribute| !attribute.path().is_ident("doc"))
330        .map(tokens)
331        .collect()
332}
333fn generics(value: &syn::Generics) -> String {
334    let params = tokens(value);
335    match &value.where_clause {
336        Some(clause) => format!("{params} {}", tokens(clause)).trim().to_string(),
337        None => params,
338    }
339}
340fn symbol(
341    module: &str,
342    name: &str,
343    kind: &str,
344    attrs: &[syn::Attribute],
345    params: &syn::Generics,
346) -> BindingSymbol {
347    BindingSymbol {
348        path: format!("{module}::{name}"),
349        name: name.to_string(),
350        kind: kind.to_string(),
351        attributes: attributes(attrs),
352        generics: generics(params),
353        impl_generics: None,
354        impl_trait: None,
355        rust_type: None,
356        fields: Vec::new(),
357        variants: Vec::new(),
358        signature: None,
359        operation: None,
360    }
361}
362fn fields(value: &syn::Fields, default_wire: bool, rename_all: Option<&str>) -> Vec<BindingField> {
363    value
364        .iter()
365        .enumerate()
366        .map(|(index, field)| {
367            let name = field.ident.as_ref().map(ToString::to_string);
368            let fallback = name
369                .as_deref()
370                .filter(|_| default_wire)
371                .map(|name| renamed(name, rename_all));
372            BindingField {
373                wire_name: wire_name(&field.attrs, fallback.as_deref()),
374                name,
375                index,
376                public: public(&field.vis),
377                rust_type: tokens(&field.ty),
378                attributes: attributes(&field.attrs),
379            }
380        })
381        .collect()
382}
383fn serde_derived(attrs: &[syn::Attribute]) -> bool {
384    attrs.iter().filter(|attribute| attribute.path().is_ident("derive")).any(|attribute| {
385        attribute.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated)
386            .is_ok_and(|paths| paths.iter().any(|path| path.segments.last().is_some_and(|segment| segment.ident == "Serialize" || segment.ident == "Deserialize")))
387    }) && !attrs.iter().filter(|attribute| attribute.path().is_ident("serde")).any(|attribute| {
388        attribute.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
389            .is_ok_and(|parts| parts.iter().any(|part| matches!(part, syn::Meta::Path(path) if path.is_ident("untagged"))))
390    })
391}
392fn serde_string(attrs: &[syn::Attribute], key: &str) -> Option<String> {
393    for attribute in attrs
394        .iter()
395        .filter(|attribute| attribute.path().is_ident("serde"))
396    {
397        if let Ok(parts) = attribute.parse_args_with(
398            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
399        ) {
400            for part in parts {
401                if let syn::Meta::NameValue(value) = part {
402                    if value.path.is_ident(key) {
403                        if let syn::Expr::Lit(syn::ExprLit {
404                            lit: syn::Lit::Str(value),
405                            ..
406                        }) = value.value
407                        {
408                            return Some(value.value());
409                        }
410                    }
411                }
412            }
413        }
414    }
415    None
416}
417fn renamed(name: &str, rule: Option<&str>) -> String {
418    use heck::{
419        ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase,
420        ToUpperCamelCase,
421    };
422    let name = name.strip_prefix("r#").unwrap_or(name);
423    match rule {
424        Some("lowercase") => name.to_lowercase(),
425        Some("UPPERCASE") => name.to_uppercase(),
426        Some("PascalCase") => name.to_upper_camel_case(),
427        Some("camelCase") => name.to_lower_camel_case(),
428        Some("snake_case") => name.to_snake_case(),
429        Some("SCREAMING_SNAKE_CASE") => name.to_shouty_snake_case(),
430        Some("kebab-case") => name.to_kebab_case(),
431        Some("SCREAMING-KEBAB-CASE") => name.to_shouty_kebab_case(),
432        _ => name.to_string(),
433    }
434}
435fn wire_name(attrs: &[syn::Attribute], fallback: Option<&str>) -> Option<String> {
436    for attribute in attrs
437        .iter()
438        .filter(|attribute| attribute.path().is_ident("serde"))
439    {
440        let parsed = attribute.parse_args_with(
441            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
442        );
443        if let Ok(parts) = parsed {
444            for part in parts {
445                match part {
446                    syn::Meta::Path(path) if path.is_ident("flatten") || path.is_ident("skip") => {
447                        return None;
448                    }
449                    syn::Meta::NameValue(value) if value.path.is_ident("rename") => {
450                        if let syn::Expr::Lit(syn::ExprLit {
451                            lit: syn::Lit::Str(value),
452                            ..
453                        }) = value.value
454                        {
455                            return Some(value.value());
456                        }
457                    }
458                    _ => {}
459                }
460            }
461        }
462    }
463    fallback.map(|name| name.strip_prefix("r#").unwrap_or(name).to_string())
464}
465fn signature(value: &syn::Signature) -> BindingSignature {
466    // Optional trailing punctuation changes with prettyplease line wrapping;
467    // canonical signatures retain the same AST semantics on either side.
468    let mut canonical = value.clone();
469    if canonical.inputs.trailing_punct() {
470        canonical.inputs.pop_punct();
471    }
472    if canonical.generics.params.trailing_punct() {
473        canonical.generics.params.pop_punct();
474    }
475    let mut receiver = None;
476    let mut arguments = Vec::new();
477    for argument in &value.inputs {
478        match argument {
479            syn::FnArg::Receiver(value) => receiver = Some(tokens(value)),
480            syn::FnArg::Typed(value) => arguments.push(BindingArgument {
481                name: tokens(&value.pat),
482                rust_type: tokens(&value.ty),
483            }),
484        }
485    }
486    let return_type = match &value.output {
487        syn::ReturnType::Default => "()".to_string(),
488        syn::ReturnType::Type(_, ty) => tokens(ty),
489    };
490    BindingSignature {
491        rust: tokens(&canonical),
492        asynchronous: value.asyncness.is_some(),
493        receiver,
494        arguments,
495        return_type,
496        generics: generics(&value.generics),
497    }
498}