1mod args;
11
12use args::{HeaderArgs, ParamArgs, SuccessArgs};
13use proc_macro::TokenStream;
14use proc_macro2::Span;
15use quote::{format_ident, quote};
16use std::sync::atomic::{AtomicU32, Ordering};
17use syn::parse::ParseStream;
18use syn::punctuated::Punctuated;
19use syn::{parse_macro_input, Ident, Item, ItemFn, LitInt, LitStr, Token};
20
21const HTTP_METHODS: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
22
23static SEQ: AtomicU32 = AtomicU32::new(0);
27
28#[proc_macro_attribute]
29pub fn title(args: TokenStream, item: TokenStream) -> TokenStream {
30 simple_fragment("title", args, item)
31}
32
33#[proc_macro_attribute]
34pub fn desc(args: TokenStream, item: TokenStream) -> TokenStream {
35 simple_fragment("desc", args, item)
36}
37
38#[proc_macro_attribute]
39pub fn method(args: TokenStream, item: TokenStream) -> TokenStream {
40 simple_fragment("method", args, item)
41}
42
43#[proc_macro_attribute]
44pub fn url(args: TokenStream, item: TokenStream) -> TokenStream {
45 simple_fragment("url", args, item)
46}
47
48#[proc_macro_attribute]
49pub fn param(args: TokenStream, item: TokenStream) -> TokenStream {
50 param_fragment("param", args, item)
51}
52
53#[proc_macro_attribute]
54pub fn query(args: TokenStream, item: TokenStream) -> TokenStream {
55 param_fragment("query", args, item)
56}
57
58#[proc_macro_attribute]
59pub fn returned(args: TokenStream, item: TokenStream) -> TokenStream {
60 param_fragment("returned", args, item)
61}
62
63#[proc_macro_attribute]
66pub fn tag(args: TokenStream, item: TokenStream) -> TokenStream {
67 litstr_fragment("tag", args, item)
68}
69
70#[proc_macro_attribute]
71pub fn group(args: TokenStream, item: TokenStream) -> TokenStream {
72 simple_fragment("group", args, item)
73}
74
75#[proc_macro_attribute]
76pub fn author(args: TokenStream, item: TokenStream) -> TokenStream {
77 simple_fragment("author", args, item)
78}
79
80#[proc_macro_attribute]
81pub fn header(args: TokenStream, item: TokenStream) -> TokenStream {
82 let args = parse_macro_input!(args as HeaderArgs);
83 let item_fn = match parse_item_fn("header", item) {
84 Ok(f) => f,
85 Err(e) => return e.to_compile_error().into(),
86 };
87 if args.name.as_deref().is_none_or(str::is_empty) {
88 return syn::Error::new(
89 Span::call_site(),
90 "apidoc::header requires a non-empty `name`",
91 )
92 .to_compile_error()
93 .into();
94 }
95 let name = args.name.as_deref().unwrap();
96 let desc = opt_lit(&args.desc);
97 let frag = quote! { apidoc::DocFragment::Header(apidoc::DocHeader { name: #name, desc: #desc }) };
98 emit_many("header", item_fn, vec![frag])
99}
100
101#[proc_macro_attribute]
102pub fn route_param(args: TokenStream, item: TokenStream) -> TokenStream {
103 param_fragment("route_param", args, item)
104}
105
106#[proc_macro_attribute]
107pub fn response_status(args: TokenStream, item: TokenStream) -> TokenStream {
108 litstr_fragment("response_status", args, item)
109}
110
111#[proc_macro_attribute]
112pub fn success(args: TokenStream, item: TokenStream) -> TokenStream {
113 example_fragment("success", args, item)
114}
115
116#[proc_macro_attribute]
117pub fn error(args: TokenStream, item: TokenStream) -> TokenStream {
118 example_fragment("error", args, item)
119}
120
121#[proc_macro_attribute]
122pub fn not_debug(args: TokenStream, item: TokenStream) -> TokenStream {
123 if !args.is_empty() {
124 return syn::Error::new(
125 Span::call_site(),
126 "apidoc::not_debug takes no arguments",
127 )
128 .to_compile_error()
129 .into();
130 }
131 let item_fn = match parse_item_fn("not_debug", item) {
132 Ok(f) => f,
133 Err(e) => return e.to_compile_error().into(),
134 };
135 emit_many("not_debug", item_fn, vec![quote! { apidoc::DocFragment::NotDebug }])
136}
137
138#[proc_macro_attribute]
139pub fn md(args: TokenStream, item: TokenStream) -> TokenStream {
140 simple_fragment("md", args, item)
141}
142
143#[proc_macro_attribute]
144pub fn sort(args: TokenStream, item: TokenStream) -> TokenStream {
145 let parser = |input: ParseStream| -> syn::Result<i32> {
147 let neg = input.peek(Token![-]);
148 if neg {
149 input.parse::<Token![-]>()?;
150 }
151 let lit: LitInt = input.parse()?;
152 let n = lit.base10_parse::<i32>()?;
153 Ok(if neg { -n } else { n })
154 };
155 let n = match syn::parse::Parser::parse2(parser, args.into()) {
156 Ok(n) => n,
157 Err(e) => return e.to_compile_error().into(),
158 };
159 let item_fn = match parse_item_fn("sort", item) {
160 Ok(f) => f,
161 Err(e) => return e.to_compile_error().into(),
162 };
163 let frag = quote! { apidoc::DocFragment::Sort(#n) };
164 emit_many("sort", item_fn, vec![frag])
165}
166
167#[proc_macro_attribute]
168pub fn r#ref(args: TokenStream, item: TokenStream) -> TokenStream {
169 simple_fragment("ref", args, item)
170}
171
172#[proc_macro_attribute]
174pub fn app(args: TokenStream, item: TokenStream) -> TokenStream {
175 simple_fragment("app", args, item)
176}
177
178fn simple_fragment(kind: &str, args: TokenStream, item: TokenStream) -> TokenStream {
181 let lit = parse_macro_input!(args as LitStr);
182 let value = lit.value();
183 let err = match kind {
184 "url" if !value.starts_with('/') => {
185 Some(syn::Error::new(lit.span(), "apidoc::url must start with '/'"))
186 }
187 "method" if !HTTP_METHODS.contains(&value.as_str()) => Some(syn::Error::new(
188 lit.span(),
189 format!(
190 "apidoc::method must be one of {:?}, got `{}`",
191 HTTP_METHODS, value
192 ),
193 )),
194 "group" | "author" | "ref" | "app" if value.trim().is_empty() => Some(syn::Error::new(
195 lit.span(),
196 format!("apidoc::{kind} must not be empty"),
197 )),
198 _ => None,
199 };
200 if let Some(err) = err {
201 return err.to_compile_error().into();
202 }
203 let item_fn = match parse_item_fn(kind, item) {
204 Ok(f) => f,
205 Err(e) => return e.to_compile_error().into(),
206 };
207 let variant = variant_ident(kind);
208 let frag = quote! { apidoc::DocFragment::#variant(#lit) };
209 emit_many(kind, item_fn, vec![frag])
210}
211
212fn param_fragment(kind: &str, args: TokenStream, item: TokenStream) -> TokenStream {
216 let args = parse_macro_input!(args as ParamArgs);
217 let item_fn = match parse_item_fn(kind, item) {
218 Ok(f) => f,
219 Err(e) => return e.to_compile_error().into(),
220 };
221 if args.name.as_deref().is_none_or(str::is_empty) {
222 return syn::Error::new(
223 Span::call_site(),
224 format!("apidoc::{kind} requires a non-empty `name`"),
225 )
226 .to_compile_error()
227 .into();
228 }
229 let variant = variant_ident(kind);
230 let doc_param = doc_param_expr(&args);
231 let frag = quote! { apidoc::DocFragment::#variant(#doc_param) };
232 emit_many(kind, item_fn, vec![frag])
233}
234
235fn variant_ident(kind: &str) -> Ident {
238 let upper = match kind {
239 "response_status" => "ResponseStatus".to_string(),
240 "route_param" => "RouteParam".to_string(),
241 "not_debug" => "NotDebug".to_string(),
242 _ => {
243 let mut chars = kind.chars();
244 let first = chars.next().unwrap().to_ascii_uppercase();
245 format!("{first}{}", chars.as_str())
246 }
247 };
248 Ident::new(&upper, Span::call_site())
249}
250
251fn parse_item_fn(kind: &str, item: TokenStream) -> syn::Result<ItemFn> {
252 let item = syn::parse::<Item>(item)?;
253 match item {
254 Item::Fn(f) => Ok(f),
255 other => Err(syn::Error::new_spanned(
256 other,
257 format!("apidoc::{kind} can only be applied to a function"),
258 )),
259 }
260}
261
262fn doc_param_expr(args: &ParamArgs) -> proc_macro2::TokenStream {
263 let name = args.name.as_deref().unwrap_or("");
264 let ty = args.ty.as_deref().unwrap_or("string");
265 let required = args.required;
266 let default = opt_lit(&args.default);
267 let desc = opt_lit(&args.desc);
268 let mock = opt_lit(&args.mock);
269 let children = args.children.iter().map(doc_param_expr);
270 quote! {
271 apidoc::DocParam {
272 name: #name,
273 ty: #ty,
274 required: #required,
275 default: #default,
276 desc: #desc,
277 mock: #mock,
278 children: &[#(#children),*],
279 }
280 }
281}
282
283fn opt_lit(value: &Option<String>) -> proc_macro2::TokenStream {
284 match value {
285 Some(s) => quote! { Some(#s) },
286 None => quote! { None },
287 }
288}
289
290fn emit_many(kind: &str, item_fn: ItemFn, frags: Vec<proc_macro2::TokenStream>) -> TokenStream {
294 let fn_ident = item_fn.sig.ident.clone();
295 let kind_upper = kind.to_uppercase();
296 let mut out = quote! { #item_fn };
297 for frag in frags {
298 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
302 let static_ident = format_ident!("__APIDOC_{kind_upper}_{fn_ident}_{seq}");
303 out.extend(quote! {
304 #[apidoc::distributed_slice(apidoc::DOC_FRAGMENTS)]
305 static #static_ident: apidoc::DocFragmentEntry = apidoc::DocFragmentEntry {
306 id: concat!(module_path!(), "::", stringify!(#fn_ident)),
307 seq: #seq,
308 frag: #frag,
309 };
310 });
311 }
312 out.into()
313}
314
315fn litstr_fragment(kind: &str, args: TokenStream, item: TokenStream) -> TokenStream {
318 let lits = match litstr_list(args) {
319 Ok(l) => l,
320 Err(e) => return e.to_compile_error().into(),
321 };
322 let item_fn = match parse_item_fn(kind, item) {
323 Ok(f) => f,
324 Err(e) => return e.to_compile_error().into(),
325 };
326 for lit in &lits {
327 if let Some(err) = validate_lit(kind, lit) {
328 return err.to_compile_error().into();
329 }
330 }
331 let variant = variant_ident(kind);
332 let frags = lits.iter().map(|lit| quote! { apidoc::DocFragment::#variant(#lit) }).collect();
333 emit_many(kind, item_fn, frags)
334}
335
336fn litstr_list(args: TokenStream) -> syn::Result<Vec<LitStr>> {
338 let list = syn::parse::Parser::parse2(
339 Punctuated::<LitStr, Token![,]>::parse_terminated,
340 args.into(),
341 )?;
342 if list.is_empty() {
343 Err(syn::Error::new(
344 Span::call_site(),
345 "expected at least one string literal",
346 ))
347 } else {
348 Ok(list.into_iter().collect())
349 }
350}
351
352fn validate_lit(kind: &str, lit: &LitStr) -> Option<syn::Error> {
353 let v = lit.value();
354 match kind {
355 "response_status"
356 if v.parse::<u16>().map_or(true, |n| !(100..=599).contains(&n)) =>
357 {
358 Some(syn::Error::new(
359 lit.span(),
360 format!(
361 "apidoc::response_status must be a numeric HTTP status code 100-599, got `{v}`"
362 ),
363 ))
364 }
365 "tag" if v.trim().is_empty() => {
366 Some(syn::Error::new(lit.span(), "apidoc::tag must not be empty"))
367 }
368 _ => None,
369 }
370}
371
372fn example_fragment(kind: &str, args: TokenStream, item: TokenStream) -> TokenStream {
374 let args = parse_macro_input!(args as SuccessArgs);
375 let item_fn = match parse_item_fn(kind, item) {
376 Ok(f) => f,
377 Err(e) => return e.to_compile_error().into(),
378 };
379 let Some(code) = args.code.as_ref() else {
380 return syn::Error::new(
381 Span::call_site(),
382 format!("apidoc::{kind} requires `code`"),
383 )
384 .to_compile_error()
385 .into();
386 };
387 if code.value().parse::<u16>().map_or(true, |n| !(100..=599).contains(&n)) {
388 return syn::Error::new(
389 code.span(),
390 format!(
391 "apidoc::{kind} code must be a numeric HTTP status code 100-599, got `{}`",
392 code.value()
393 ),
394 )
395 .to_compile_error()
396 .into();
397 }
398 let Some(example) = args.example.as_ref() else {
399 return syn::Error::new(
400 Span::call_site(),
401 format!("apidoc::{kind} requires `example`"),
402 )
403 .to_compile_error()
404 .into();
405 };
406 let variant = variant_ident(kind);
407 let frag = quote! {
408 apidoc::DocFragment::#variant(apidoc::DocExample { code: #code, example: #example })
409 };
410 emit_many(kind, item_fn, vec![frag])
411}
412