1pub use apidoc_macros::*;
13pub use linkme::distributed_slice;
14
15pub mod export;
17
18pub mod auth;
20
21#[cfg(feature = "mock")]
23pub mod mock;
24
25#[cfg(feature = "axum")]
27pub mod axum;
28
29#[cfg(feature = "actix")]
31pub mod actix;
32
33pub const UI_HTML: &str = concat!(
37 include_str!("ui.html"),
38 include_str!("ui.js"),
39 include_str!("ui.debug.js")
40);
41
42use serde::Serialize;
43
44#[distributed_slice]
46pub static DOC_FRAGMENTS: [DocFragmentEntry];
47
48pub struct DocFragmentEntry {
53 pub id: &'static str,
54 pub seq: u32,
55 pub frag: DocFragment,
56}
57
58pub enum DocFragment {
60 Title(&'static str),
61 Desc(&'static str),
62 Method(&'static str),
63 Url(&'static str),
64 Param(DocParam),
65 Query(DocParam),
66 Returned(DocParam),
67 Tag(&'static str),
70 Group(&'static str),
71 Author(&'static str),
72 Header(DocHeader),
73 RouteParam(DocParam),
74 ResponseStatus(&'static str),
75 Success(DocExample),
76 Error(DocExample),
77 NotDebug,
78 Md(&'static str),
79 Sort(i32),
80 Ref(&'static str),
81 App(&'static str),
83}
84
85#[derive(Clone, Serialize)]
87pub struct DocExample {
88 pub code: &'static str,
90 pub example: &'static str,
92}
93
94#[derive(Clone, Serialize)]
96pub struct DocParam {
97 pub name: &'static str,
98 #[serde(rename = "type")]
99 pub ty: &'static str,
100 pub required: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub default: Option<&'static str>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub desc: Option<&'static str>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub mock: Option<&'static str>,
107 #[serde(skip_serializing_if = "slice_is_empty")]
108 pub children: &'static [DocParam],
109}
110
111fn slice_is_empty<T>(s: &[T]) -> bool {
112 s.is_empty()
113}
114
115#[derive(Clone, Serialize)]
117pub struct DocEndpoint {
118 pub title: String,
119 pub desc: String,
120 pub url: String,
121 pub method: String,
122 #[serde(skip_serializing_if = "Vec::is_empty")]
123 pub headers: Vec<DocHeader>,
124 pub params: Vec<DocParam>,
125 pub querys: Vec<DocParam>,
126 pub returned: Vec<DocParam>,
127 #[serde(skip_serializing_if = "String::is_empty")]
131 pub group: String,
132 #[serde(skip_serializing_if = "Vec::is_empty")]
133 pub tags: Vec<String>,
134 #[serde(skip_serializing_if = "String::is_empty")]
135 pub author: String,
136 #[serde(skip_serializing_if = "Vec::is_empty")]
137 pub route_params: Vec<DocParam>,
138 #[serde(skip_serializing_if = "Vec::is_empty")]
139 pub response_status: Vec<String>,
140 #[serde(skip_serializing_if = "Vec::is_empty")]
141 pub success: Vec<DocExample>,
142 #[serde(skip_serializing_if = "Vec::is_empty")]
143 pub error: Vec<DocExample>,
144 #[serde(skip_serializing_if = "std::ops::Not::not")]
145 pub not_debug: bool,
146 #[serde(skip_serializing_if = "String::is_empty")]
147 pub md: String,
148 #[serde(skip_serializing_if = "is_zero")]
149 pub sort: i32,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub r#ref: Option<String>,
152 #[serde(skip)]
154 pub app_key: String,
155}
156
157fn is_zero(n: &i32) -> bool {
158 *n == 0
159}
160
161impl Default for DocEndpoint {
162 fn default() -> Self {
163 DocEndpoint {
164 title: String::new(),
165 desc: String::new(),
166 url: String::new(),
167 method: "GET".to_string(),
168 headers: Vec::new(),
169 params: Vec::new(),
170 querys: Vec::new(),
171 returned: Vec::new(),
172 group: String::new(),
173 tags: Vec::new(),
174 author: String::new(),
175 route_params: Vec::new(),
176 response_status: Vec::new(),
177 success: Vec::new(),
178 error: Vec::new(),
179 not_debug: false,
180 md: String::new(),
181 sort: 0,
182 r#ref: None,
183 app_key: String::new(),
184 }
185 }
186}
187
188#[derive(Clone, Serialize)]
190pub struct DocHeader {
191 pub name: &'static str,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub desc: Option<&'static str>,
194}
195
196#[derive(Clone)]
199pub struct AppConfig {
200 pub key: String,
201 pub title: String,
202 pub items: Vec<AppConfig>,
203 pub password: Option<String>,
204}
205
206pub fn find_app<'a>(apps: &'a [AppConfig], key: &str) -> Option<&'a AppConfig> {
208 for app in apps {
209 if app.key == key {
210 return Some(app);
211 }
212 if let Some(found) = find_app(&app.items, key) {
213 return Some(found);
214 }
215 }
216 None
217}
218
219#[derive(Serialize)]
221pub struct ApidocConfig {
222 pub title: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub description: Option<String>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub auth: Option<auth::AuthConfig>,
228 #[serde(skip)]
230 pub apps: Vec<AppConfig>,
231}
232
233#[derive(Serialize)]
235pub struct AppDoc {
236 pub key: String,
237 pub title: String,
238 #[serde(skip_serializing_if = "Vec::is_empty")]
239 pub items: Vec<AppDoc>,
240 #[serde(skip_serializing_if = "Vec::is_empty")]
241 pub endpoints: Vec<DocEndpoint>,
242}
243
244#[derive(Serialize)]
246pub struct ApiDoc {
247 pub config: ApidocConfig,
248 pub endpoints: Vec<DocEndpoint>,
249 #[serde(skip_serializing_if = "Vec::is_empty")]
251 pub apps: Vec<AppDoc>,
252}
253
254pub struct DocRegistry;
256
257impl DocRegistry {
258 pub fn collect() -> Vec<DocEndpoint> {
260 Self::collect_inner()
261 }
262
263 pub fn collect_doc(config: ApidocConfig) -> ApiDoc {
266 let endpoints = Self::collect_inner();
267 let apps = build_apps(&config.apps, &endpoints);
268 ApiDoc { config, endpoints, apps }
269 }
270
271 fn collect_inner() -> Vec<DocEndpoint> {
272 let mut entries: Vec<&DocFragmentEntry> = DOC_FRAGMENTS.iter().collect();
277 entries.sort_by_key(|e| e.seq);
278 let mut ids: Vec<&'static str> = Vec::new();
279 let mut endpoints: Vec<DocEndpoint> = Vec::new();
280 for entry in entries {
281 let idx = match ids.iter().position(|id| *id == entry.id) {
284 Some(i) => i,
285 None => {
286 ids.push(entry.id);
287 endpoints.push(DocEndpoint::default());
288 endpoints.len() - 1
289 }
290 };
291 let ep = &mut endpoints[idx];
292 match &entry.frag {
293 DocFragment::Title(t) => ep.title = t.to_string(),
294 DocFragment::Desc(d) => ep.desc = d.to_string(),
295 DocFragment::Method(m) => ep.method = m.to_string(),
296 DocFragment::Url(u) => ep.url = u.to_string(),
297 DocFragment::Param(p) => ep.params.push(p.clone()),
298 DocFragment::Query(q) => ep.querys.push(q.clone()),
299 DocFragment::Returned(r) => ep.returned.push(r.clone()),
300 DocFragment::Tag(t) => ep.tags.push(t.to_string()),
303 DocFragment::Group(g) => ep.group = g.to_string(),
304 DocFragment::Author(a) => ep.author = a.to_string(),
305 DocFragment::Header(h) => ep.headers.push(h.clone()),
306 DocFragment::RouteParam(p) => ep.route_params.push(p.clone()),
307 DocFragment::ResponseStatus(s) => {
308 if !ep.response_status.iter().any(|x| x == s) {
309 ep.response_status.push(s.to_string());
310 }
311 }
312 DocFragment::Success(e) => ep.success.push(e.clone()),
313 DocFragment::Error(e) => ep.error.push(e.clone()),
314 DocFragment::NotDebug => ep.not_debug = true,
315 DocFragment::Md(m) => ep.md = m.to_string(),
316 DocFragment::Sort(n) => ep.sort = *n,
317 DocFragment::Ref(r) => ep.r#ref = Some(r.to_string()),
318 DocFragment::App(a) => ep.app_key = a.to_string(),
319 }
320 }
321 for i in 0..endpoints.len() {
326 if endpoints[i].r#ref.is_some() {
327 resolve_ref(i, &mut Vec::new(), &ids, &mut endpoints);
328 }
329 }
330 endpoints
331 }
332}
333
334fn build_apps(config_apps: &[AppConfig], endpoints: &[DocEndpoint]) -> Vec<AppDoc> {
336 for ep in endpoints.iter().filter(|e| !e.app_key.is_empty()) {
337 if find_app(config_apps, &ep.app_key).is_none() {
338 eprintln!("apidoc: app `{}` not configured in ApidocConfig.apps, endpoints fall back to the default app", ep.app_key);
339 }
340 }
341 config_apps.iter().map(|c| build_app_doc(c, endpoints)).collect()
342}
343
344fn build_app_doc(cfg: &AppConfig, endpoints: &[DocEndpoint]) -> AppDoc {
345 let eps: Vec<DocEndpoint> = endpoints.iter().filter(|e| e.app_key == cfg.key).cloned().collect();
346 AppDoc {
347 key: cfg.key.clone(),
348 title: cfg.title.clone(),
349 items: cfg.items.iter().map(|c| build_app_doc(c, endpoints)).collect(),
350 endpoints: eps,
351 }
352}
353
354fn resolve_ref(
357 idx: usize,
358 visited: &mut Vec<usize>,
359 ids: &[&'static str],
360 endpoints: &mut [DocEndpoint],
361) -> bool {
362 if visited.contains(&idx) {
363 eprintln!("apidoc: ref cycle at `{}`, skipping", ids[idx]);
364 return false;
365 }
366 visited.push(idx);
367 let Some(target) = endpoints[idx].r#ref.as_deref() else {
368 return true;
369 };
370 let Some(j) = find_ref_target(target, ids) else {
371 eprintln!("apidoc: ref target `{target}` not found for `{}`", ids[idx]);
372 return false;
373 };
374 if !resolve_ref(j, visited, ids, endpoints) {
375 return false;
376 }
377 endpoints[idx].returned = endpoints[j].returned.clone();
378 true
379}
380
381fn find_ref_target(target: &str, ids: &[&'static str]) -> Option<usize> {
384 if let Some(j) = ids.iter().position(|id| *id == target) {
385 return Some(j);
386 }
387 let suffix = format!("::{target}");
388 let mut hits = ids.iter().enumerate().filter(|(_, id)| id.ends_with(&suffix));
389 let (j, _) = hits.next()?;
390 if hits.next().is_some() {
391 eprintln!("apidoc: ref `{target}` matches multiple endpoints, using `{}`", ids[j]);
392 }
393 Some(j)
394}