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}