openapi_gen/
lib.rs

1mod codegen;
2mod generator;
3mod parser;
4mod utils;
5
6use heck::ToPascalCase;
7use proc_macro::TokenStream;
8use proc_macro2::{Span, TokenStream as TokenStream2};
9use quote::{format_ident, quote};
10use syn::parse_macro_input;
11
12use generator::*;
13use parser::*;
14
15/// Generates an API client and structs from an OpenAPI specification
16///
17/// Supports loading OpenAPI specifications from both local files and remote URLs.
18/// The specification format (JSON/YAML) is auto-detected from the file extension
19/// or URL path.
20///
21/// Usage:
22/// ```rust,ignore
23/// use openapi_gen::openapi_client;
24///
25/// // From local file with auto-generated client name (derived from API title + "Api")
26/// openapi_client!("path/to/openapi.json");
27/// openapi_client!("path/to/openapi.yaml");
28///
29/// // From URL with auto-generated client name
30/// openapi_client!("https://api.example.com/openapi.json");
31/// openapi_client!("https://raw.githubusercontent.com/user/repo/main/openapi.yaml");
32///
33/// // With custom client name (works for both files and URLs)
34/// openapi_client!("path/to/openapi.json", "MyApiClient");
35/// openapi_client!("https://api.example.com/openapi.json", "MyApiClient");
36/// ```
37#[proc_macro]
38pub fn openapi_client(input: TokenStream) -> TokenStream {
39    let input = parse_macro_input!(input as OpenApiInput);
40
41    match generate_client(&input) {
42        Ok(tokens) => tokens.into(),
43        Err(e) => syn::Error::new(Span::call_site(), e)
44            .to_compile_error()
45            .into(),
46    }
47}
48
49fn generate_client(input: &OpenApiInput) -> Result<TokenStream2, String> {
50    // Load and parse the OpenAPI specification
51    let spec = load_openapi_spec(input)?;
52
53    let client_name = if let Some(name) = &input.client_name {
54        format_ident!("{}", name)
55    } else {
56        // Derive client name from API title
57        let title = spec.info.title.clone();
58        let sanitized_title = title
59            .chars()
60            .filter(|c| c.is_alphanumeric() || c.is_whitespace())
61            .collect::<String>()
62            .to_pascal_case();
63        format_ident!("{}Api", sanitized_title)
64    };
65
66    // Generate components
67    let structs = generate_structs(&spec)?;
68    let client_impl = generate_client_impl(&spec, &client_name)?;
69    let error_types = generate_error_types();
70
71    // Generate client documentation
72    let client_doc = generate_client_doc_comment(&spec, &client_name.to_string());
73
74    Ok(quote! {
75        use serde::{Deserialize, Serialize};
76        use std::collections::HashMap;
77
78        #error_types
79
80        #structs
81
82        #client_doc
83        #[derive(Clone)]
84        pub struct #client_name {
85            base_url: String,
86            client: reqwest::Client,
87        }
88
89        #client_impl
90    })
91}