bytedocs_rs/lib.rs
1//! # Bytedocs
2//!
3//! Bytedocs is a Rust alternative to Swagger for API documentation generation.
4//! It automatically generates beautiful, interactive API documentation from your Rust web applications.
5//!
6//! ## Features
7//!
8//! - **Framework Support**: Works with Axum, Warp, Actix-web, and more
9//! - **Beautiful UI**: Clean, responsive interface with dark/light mode
10//! - **Authentication**: Built-in auth with session management and IP banning
11//! - **AI Integration**: Optional AI-powered chat for API exploration
12//! - **OpenAPI Compatible**: Generates standard OpenAPI 3.0.3 specifications
13//! - **Fast**: Built with Rust for maximum performance
14//!
15//! ## Quick Start
16//!
17//! ```rust
18//! use bytedocs_rs::{Bytedocs, Config};
19//!
20//! #[tokio::main]
21//! async fn main() {
22//! let config = Config::default();
23//! let mut docs = Bytedocs::new(Some(config));
24//!
25//! // Add your API routes
26//! docs.add_route("GET", "/users", None, Some("List all users"), None, None, None, None);
27//! docs.add_route("POST", "/users", None, Some("Create a new user"), None, None, None, None);
28//!
29//! // Generate documentation
30//! docs.generate().unwrap();
31//!
32//! println!("Documentation generated!");
33//! }
34//! ```
35
36pub mod core;
37pub mod ai;
38pub mod parser;
39
40pub use core::{
41 Config, AuthConfig, UIConfig, APIInfo, Documentation, APIDocs,
42 Endpoint, Parameter, RequestBody, Response, RouteInfo,
43 load_config_from_env, validate_config, ByteDocsError, Result,
44};
45
46pub use ai::{AIConfig, AIFeatures, ChatRequest, ChatResponse, Client as AIClient};
47
48pub use parser::{
49 AxumParser, WarpParser, ActixWebParser, FrameworkParser,
50 extract_path_params, generate_default_responses,
51};
52
53/// Main entry point for Bytedocs API documentation generation.
54///
55/// `Bytedocs` is the primary struct for creating and managing API documentation.
56/// It wraps the internal `APIDocs` implementation and provides a user-friendly API.
57pub struct Bytedocs {
58 inner: APIDocs,
59}
60
61impl Bytedocs {
62 /// Creates a new `Bytedocs` instance with the specified configuration.
63 ///
64 /// # Arguments
65 ///
66 /// * `config` - Optional configuration. If `None`, uses default configuration.
67 ///
68 /// # Example
69 ///
70 /// ```rust
71 /// use bytedocs_rs::Bytedocs;
72 ///
73 /// let docs = Bytedocs::new(None);
74 /// ```
75 pub fn new(config: Option<Config>) -> Self {
76 Self {
77 inner: APIDocs::new(config),
78 }
79 }
80
81 /// Creates a `Bytedocs` instance from environment variables.
82 ///
83 /// Reads configuration from environment variables and validates it.
84 ///
85 /// # Errors
86 ///
87 /// Returns an error if environment variables are invalid or configuration validation fails.
88 pub fn from_env() -> Result<Self> {
89 let config = load_config_from_env(None)
90 .map_err(|e| ByteDocsError::Environment(e.to_string()))?;
91 validate_config(&config)
92 .map_err(|e| ByteDocsError::Config(e.to_string()))?;
93 Ok(Self::new(Some(config)))
94 }
95
96 /// Creates a `Bytedocs` instance from a `.env` file.
97 ///
98 /// # Arguments
99 ///
100 /// * `env_file` - Path to the environment file to load
101 ///
102 /// # Errors
103 ///
104 /// Returns an error if the file path is empty, file cannot be read,
105 /// or configuration validation fails.
106 pub fn from_env_file(env_file: &str) -> Result<Self> {
107 if env_file.is_empty() {
108 return Err(ByteDocsError::InvalidInput("Environment file path cannot be empty".to_string()));
109 }
110
111 let config = load_config_from_env(Some(env_file))
112 .map_err(|e| ByteDocsError::Environment(e.to_string()))?;
113 validate_config(&config)
114 .map_err(|e| ByteDocsError::Config(e.to_string()))?;
115 Ok(Self::new(Some(config)))
116 }
117
118 /// Adds a new API route to the documentation.
119 ///
120 /// # Arguments
121 ///
122 /// * `method` - HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
123 /// * `path` - API endpoint path (must start with '/')
124 /// * `handler` - Optional handler function (for framework integration)
125 /// * `summary` - Optional short description of the endpoint
126 /// * `description` - Optional detailed description
127 /// * `parameters` - Optional list of parameters
128 /// * `request_body` - Optional request body specification
129 /// * `responses` - Optional response specifications
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if:
134 /// - Method is empty or invalid
135 /// - Path is empty or doesn't start with '/'
136 /// - Path format is invalid
137 #[allow(clippy::too_many_arguments)]
138 pub fn add_route(
139 &mut self,
140 method: &str,
141 path: &str,
142 handler: Option<Box<dyn std::any::Any + Send + Sync>>,
143 summary: Option<&str>,
144 description: Option<&str>,
145 parameters: Option<Vec<Parameter>>,
146 request_body: Option<RequestBody>,
147 responses: Option<std::collections::HashMap<String, Response>>,
148 ) -> Result<()> {
149 if method.is_empty() {
150 return Err(ByteDocsError::InvalidInput("HTTP method cannot be empty".to_string()));
151 }
152
153 if path.is_empty() {
154 return Err(ByteDocsError::InvalidInput("Path cannot be empty".to_string()));
155 }
156
157 if !path.starts_with('/') {
158 return Err(ByteDocsError::InvalidInput("Path must start with '/'".to_string()));
159 }
160
161 // Check for invalid paths like "//" or paths with only whitespace
162 let trimmed_path = path.trim();
163 if trimmed_path.is_empty() || trimmed_path == "//" {
164 return Err(ByteDocsError::InvalidInput("Invalid path format".to_string()));
165 }
166
167 let valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
168 if !valid_methods.contains(&method.to_uppercase().as_str()) {
169 return Err(ByteDocsError::InvalidInput(format!("Invalid HTTP method: {}", method)));
170 }
171
172 let sanitized_method = method.trim().to_uppercase();
173 let sanitized_path = path.trim().to_string();
174 let sanitized_summary = summary.map(|s| s.trim().to_string());
175 let sanitized_description = description.map(|s| s.trim().to_string());
176
177 self.inner.add_route(
178 &sanitized_method,
179 &sanitized_path,
180 handler.unwrap_or_else(|| Box::new(())),
181 sanitized_summary,
182 sanitized_description,
183 parameters,
184 request_body,
185 responses,
186 );
187
188 Ok(())
189 }
190
191 /// Adds multiple routes at once.
192 ///
193 /// # Arguments
194 ///
195 /// * `routes` - Vector of `RouteInfo` structs to add
196 pub fn add_routes(&mut self, routes: Vec<RouteInfo>) {
197 for route in routes {
198 self.inner.add_route_info(route);
199 }
200 }
201
202 /// Automatically detects and extracts Axum routes from source files.
203 ///
204 /// # Arguments
205 ///
206 /// * `source_paths` - Array of source file paths to scan
207 ///
208 /// # Errors
209 ///
210 /// Returns an error if source paths are empty or route detection fails.
211 pub fn auto_detect_axum_routes(&mut self, source_paths: &[&str]) -> Result<()> {
212 if source_paths.is_empty() {
213 return Err(ByteDocsError::InvalidInput("Source paths cannot be empty".to_string()));
214 }
215
216 let parser = parser::AxumParser::new();
217
218 for source_path in source_paths {
219 if source_path.is_empty() {
220 return Err(ByteDocsError::InvalidInput("Source path cannot be empty".to_string()));
221 }
222
223 let routes = parser.auto_detect_routes(source_path)
224 .map_err(|e| ByteDocsError::RouteDetection(e.to_string()))?;
225 self.add_routes(routes);
226 }
227
228 Ok(())
229 }
230
231 /// Convenience method to auto-detect routes from `src/main.rs`.
232 ///
233 /// # Errors
234 ///
235 /// Returns an error if route detection fails.
236 pub fn auto_detect_from_main(&mut self) -> Result<()> {
237 self.auto_detect_axum_routes(&["src/main.rs"])
238 }
239
240 /// Generates the API documentation.
241 ///
242 /// This must be called after adding all routes and before serving the documentation.
243 ///
244 /// # Errors
245 ///
246 /// Returns an error if documentation generation fails.
247 pub fn generate(&mut self) -> Result<()> {
248 self.inner.generate()
249 .map_err(|e| ByteDocsError::Server(e.to_string()))
250 }
251
252 /// Returns a reference to the generated documentation.
253 pub fn documentation(&self) -> &Documentation {
254 self.inner.get_documentation()
255 }
256
257 /// Returns a reference to the configuration.
258 pub fn config(&self) -> &Config {
259 self.inner.get_config()
260 }
261
262 /// Returns the OpenAPI specification as JSON.
263 ///
264 /// # Errors
265 ///
266 /// Returns an error if JSON generation fails.
267 pub async fn openapi_json(&mut self) -> Result<serde_json::Value> {
268 self.inner.get_openapi_json().await
269 .map_err(|e| ByteDocsError::Server(e.to_string()))
270 }
271
272 /// Returns the OpenAPI specification as YAML.
273 ///
274 /// # Errors
275 ///
276 /// Returns an error if YAML generation fails.
277 pub async fn openapi_yaml(&mut self) -> Result<String> {
278 self.inner.get_openapi_yaml().await
279 .map_err(|e| ByteDocsError::Server(e.to_string()))
280 }
281
282 /// Returns an Axum router with all documentation routes configured.
283 ///
284 /// This can be merged with your application's existing router.
285 pub fn router(&self) -> axum::Router {
286 self.inner.router()
287 }
288
289 /// Starts the documentation server on the specified address.
290 ///
291 /// # Arguments
292 ///
293 /// * `addr` - The address to bind to (e.g., "0.0.0.0:8813")
294 ///
295 /// # Errors
296 ///
297 /// Returns an error if the address is empty or the server fails to start.
298 pub async fn serve(&self, addr: &str) -> Result<()> {
299 if addr.is_empty() {
300 return Err(ByteDocsError::InvalidInput("Address cannot be empty".to_string()));
301 }
302
303 let app = self.router();
304 let listener = tokio::net::TcpListener::bind(addr)
305 .await
306 .map_err(|e| ByteDocsError::Server(format!("Failed to bind to {}: {}", addr, e)))?;
307
308
309 axum::serve(listener, app)
310 .await
311 .map_err(|e| ByteDocsError::Server(format!("Server error: {}", e)))?;
312 Ok(())
313 }
314}
315
316
317/// Creates a `Bytedocs` instance configured for Axum framework.
318///
319/// This is a convenience function equivalent to `Bytedocs::new()`.
320///
321/// # Arguments
322///
323/// * `config` - Optional configuration
324pub fn axum_docs(config: Option<Config>) -> Bytedocs {
325 Bytedocs::new(config)
326}
327
328/// Creates a `Bytedocs` instance configured for Warp framework.
329///
330/// This is a convenience function equivalent to `Bytedocs::new()`.
331///
332/// # Arguments
333///
334/// * `config` - Optional configuration
335pub fn warp_docs(config: Option<Config>) -> Bytedocs {
336 Bytedocs::new(config)
337}
338
339/// Creates a `Bytedocs` instance configured for Actix-web framework.
340///
341/// This is a convenience function equivalent to `Bytedocs::new()`.
342///
343/// # Arguments
344///
345/// * `config` - Optional configuration
346pub fn actix_docs(config: Option<Config>) -> Bytedocs {
347 Bytedocs::new(config)
348}
349
350/// Builder pattern for creating `Bytedocs` instances with custom configuration.
351///
352/// Provides a fluent API for configuring Bytedocs before instantiation.
353pub struct ByteDocsBuilder {
354 config: Config,
355}
356
357impl ByteDocsBuilder {
358 /// Creates a new builder with default configuration.
359 pub fn new() -> Self {
360 Self {
361 config: Config::default(),
362 }
363 }
364
365 /// Sets the API documentation title.
366 pub fn title(mut self, title: &str) -> Self {
367 self.config.title = title.to_string();
368 self
369 }
370
371 /// Sets the API version.
372 pub fn version(mut self, version: &str) -> Self {
373 self.config.version = version.to_string();
374 self
375 }
376
377 /// Sets the API description.
378 pub fn description(mut self, description: &str) -> Self {
379 self.config.description = description.to_string();
380 self
381 }
382
383 /// Sets the base URL for the API.
384 pub fn base_url(mut self, base_url: &str) -> Self {
385 self.config.base_url = base_url.to_string();
386 self
387 }
388
389 /// Sets the path where documentation will be served.
390 pub fn docs_path(mut self, docs_path: &str) -> Self {
391 self.config.docs_path = docs_path.to_string();
392 self
393 }
394
395 /// Enables authentication with the specified configuration.
396 pub fn with_auth(mut self, auth_config: AuthConfig) -> Self {
397 self.config.auth_config = Some(auth_config);
398 self
399 }
400
401 /// Enables AI features with the specified configuration.
402 pub fn with_ai(mut self, ai_config: AIConfig) -> Self {
403 self.config.ai_config = Some(ai_config);
404 self
405 }
406
407 /// Builds the `Bytedocs` instance with the configured settings.
408 pub fn build(self) -> Bytedocs {
409 Bytedocs::new(Some(self.config))
410 }
411}
412
413impl Default for ByteDocsBuilder {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_bytedocs_creation() {
425 let docs = Bytedocs::new(None);
426 assert_eq!(docs.config().title, "API Documentation");
427 }
428
429 #[test]
430 fn test_builder_pattern() {
431 let docs = ByteDocsBuilder::new()
432 .title("My API")
433 .version("1.0.0")
434 .description("Test API")
435 .build();
436
437 assert_eq!(docs.config().title, "My API");
438 assert_eq!(docs.config().version, "1.0.0");
439 assert_eq!(docs.config().description, "Test API");
440 }
441
442 #[tokio::test]
443 async fn test_route_addition() {
444 let mut docs = Bytedocs::new(None);
445 let _ = docs.add_route("GET", "/users", None, Some("List users"), None, None, None, None);
446
447 docs.generate().unwrap();
448 let documentation = docs.documentation();
449 assert!(!documentation.endpoints.is_empty());
450 }
451}