kvapi_macros_internals/api/
builder.rs

1use super::{common::Separator, dict::Dict, headers::Headers};
2use convert_case::{Case, Casing};
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::{
6    parse::{Parse, ParseStream},
7    Expr, Ident, LitStr,
8};
9
10/// Input for the `api! { #input }` macro.
11///
12/// ```rust
13/// name:       PascalStructName
14/// dict:       { "endpoint": Type }
15/// headers:    { "Header Name": "Header Value" }
16/// query:      "?query_param"
17/// ```
18///
19/// The Director will generate the API with an ApiBuilder.
20pub struct ApiBuilder {
21    // required
22    pub name: Option<Ident>,
23    pub dict: Option<Dict>,
24
25    // optional
26    pub base: Option<TokenStream>,
27    pub headers: Option<Headers>,
28    pub query: Option<Expr>,
29}
30
31impl ApiBuilder {
32    pub fn build(self) -> TokenStream {
33        let mut fields: Vec<TokenStream> = vec![];
34        let mut nodes: Vec<TokenStream> = vec![];
35
36        let dict = self.dict.expect("dict entry is required").inner;
37        let api_name = self.name.expect("name entry is required");
38
39        // build all fields from nodes
40        for (name, node) in dict {
41            // (snake_name, PascalName) == (field_name, StructName)
42            let (snake, pascal) = (
43                format_ident!("{}", name.to_case(Case::Snake)),
44                format_ident!("{}{}", api_name, name.to_case(Case::Pascal)),
45            );
46
47            // api.fields
48            if node.is_root() {
49                fields.push(quote! { #snake: #pascal });
50            }
51
52            // api.nodes
53            let fields = node.children_fields(api_name.clone());
54            if node.is_http() {
55                let url = node.build_url(self.base.clone());
56                let http = node.build_http(url, self.headers.clone());
57
58                // http node
59                let node = quote! {
60                    pub struct #pascal {
61                        client: kvapi::Client,
62                        url: String,
63                        #( pub #fields, )*
64                    }
65                    impl #pascal {
66                        pub fn new() -> Self {
67                            Self {
68                                client: Self::build_client().unwrap(),
69                                url: Self::build_url(),
70                                #( #fields::new(), )*
71                            }
72                        }
73                        #http
74                    }
75                };
76
77                nodes.push(node)
78            } else {
79                // non-http node
80                let node = quote! {
81                    pub struct #pascal {
82                        #( pub #fields, )*
83                    }
84
85                    impl #pascal {
86                        pub fn new() -> Self {
87                            Self {
88                                #( #fields::new(), )*
89                            }
90                        }
91                    }
92                };
93                nodes.push(node);
94            }
95        }
96
97        // return the final TokenStream
98        quote! {
99            pub struct #api_name {
100                #( pub #fields, )*
101            }
102            impl #api_name {
103                pub fn new() -> Self {
104                    Self {
105                        #( #fields::new(), )*
106                    }
107                }
108            }
109            #( #nodes )*
110        }
111    }
112}
113
114impl Parse for ApiBuilder {
115    fn parse(input: ParseStream) -> syn::Result<Self> {
116        let mut api = Self {
117            // required
118            name: None,
119            dict: None,
120
121            // optional
122            base: None,
123            headers: None,
124            query: None,
125        };
126
127        while !input.is_empty() {
128            let ident: Ident = input.parse()?;
129            input.parse::<Separator>()?;
130            match ident.to_string().as_str() {
131                "name" | "N" => {
132                    let name: Ident = input.parse()?;
133                    api.name = Some(name);
134                }
135                "base" | "B" => {
136                    let base: LitStr = input.parse()?;
137                    let base = base.value();
138                    api.base = Some(quote!(#base));
139                }
140                "dict" | "D" => {
141                    let dict: Dict = input.parse()?;
142                    api.dict = Some(dict);
143                }
144                "headers" | "head" | "hdrs" | "H" => {
145                    let headers: Headers = input.parse()?;
146                    api.headers = Some(headers);
147                }
148                "query" | "Q" => {
149                    let query: Expr = input.parse()?;
150                    api.query = Some(query);
151                }
152                _ => return Err(syn::Error::new(ident.span(), "unknown input to `api!`")),
153            }
154        }
155
156        // guarantee `name` & `dict`
157        if api.name.is_none() {
158            panic!("`name` is required; the name of the struct identity")
159        }
160
161        if api.dict.is_none() {
162            panic!("`dict` is required; a list of endpoints and output types")
163        }
164
165        Ok(api)
166    }
167}