diff --git a/src/catcher.rs b/src/catcher.rs index f94898d..e7cc05c 100644 --- a/src/catcher.rs +++ b/src/catcher.rs @@ -11,13 +11,13 @@ use pyo3::prelude::*; /// handler (callable): The handler function that will be called when this status occurs. /// /// Example: -/// ```python -/// from oxapy import catcher, Status -/// -/// @catcher(Status.NOT_FOUND) -/// def handle_not_found(request, response): -/// return Response("

Custom 404 Page

", content_type="text/html") -/// ``` +/// ```python +/// from oxapy import catcher, Status +/// +/// @catcher(Status.NOT_FOUND) +/// def handle_not_found(request, response): +/// return Response("

Custom 404 Page

", content_type="text/html") +/// ``` #[pyclass] pub struct Catcher { pub status: Status, @@ -61,16 +61,16 @@ impl CatcherBuilder { /// CatcherBuilder: A builder that creates a Catcher when called with a handler function. /// /// Example: -/// ```python -/// from oxapy import catcher, Status, Response -/// -/// @catcher(Status.NOT_FOUND) -/// def handle_404(request, response): -/// return Response("

Page Not Found

", content_type="text/html") -/// -/// # Add the catcher to your server -/// app.catchers([handle_404]) -/// ``` +/// ```python +/// from oxapy import catcher, Status, Response +/// +/// @catcher(Status.NOT_FOUND) +/// def handle_404(request, response): +/// return Response("

Page Not Found

", content_type="text/html") +/// +/// # Add the catcher to your server +/// app.catchers([handle_404]) +/// ``` #[pyfunction] pub fn catcher(status: Status) -> CatcherBuilder { CatcherBuilder { status } diff --git a/src/cors.rs b/src/cors.rs index 3a166ad..db33175 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -13,19 +13,19 @@ use pyo3::prelude::*; /// Cors: A new CORS configuration with default settings. /// /// Example: -/// ```python -/// from oxapy import HttpServer, Cors -/// -/// app = HttpServer(("127.0.0.1", 8000)) -/// -/// # Set up CORS with custom configuration -/// cors = Cors() -/// cors.origins = ["https://example.com", "https://app.example.com"] -/// cors.methods = ["GET", "POST", "OPTIONS"] -/// cors.headers = ["Content-Type", "Authorization"] -/// -/// app.cors(cors) -/// ``` +/// ```python +/// from oxapy import HttpServer, Cors +/// +/// app = HttpServer(("127.0.0.1", 8000)) +/// +/// # Set up CORS with custom configuration +/// cors = Cors() +/// cors.origins = ["https://example.com", "https://app.example.com"] +/// cors.methods = ["GET", "POST", "OPTIONS"] +/// cors.headers = ["Content-Type", "Authorization"] +/// +/// app.cors(cors) +/// ``` #[derive(Clone, Debug)] #[pyclass] pub struct Cors { @@ -66,14 +66,14 @@ impl Cors { /// Cors: A new CORS configuration with default values. /// /// Example: - /// ```python - /// # Create CORS with default configuration (allows all origins) - /// cors = Cors() - /// - /// # Customize CORS settings - /// cors.origins = ["https://example.com"] - /// cors.allow_credentials = False - /// ``` + /// ```python + /// # Create CORS with default configuration (allows all origins) + /// cors = Cors() + /// + /// # Customize CORS settings + /// cors.origins = ["https://example.com"] + /// cors.allow_credentials = False + /// ``` #[new] fn new() -> Self { Self::default() diff --git a/src/handling/response_handler.rs b/src/handling/response_handler.rs index f9a06e4..280d35c 100644 --- a/src/handling/response_handler.rs +++ b/src/handling/response_handler.rs @@ -99,14 +99,13 @@ fn process_response( let route = route.value; let kwargs = prepare_route_params(params, py)?; - - kwargs.set_item("request", request.clone())?; + let request = request.clone(); let result = if !router.middlewares.is_empty() { let chain = MiddlewareChain::new(router.middlewares.clone()); - chain.execute(py, &route.handler.clone(), kwargs.clone())? + chain.execute(py, &route.handler.clone(), (request,), kwargs.clone())? } else { - route.handler.call(py, (), Some(&kwargs))? + route.handler.call(py, (request,), Some(&kwargs))? }; convert_to_response(result, py) } else { diff --git a/src/lib.rs b/src/lib.rs index 689a618..812a335 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,35 +85,35 @@ struct RequestContext { /// HttpServer: A new server instance. /// /// Example: -/// ```python -/// from oxapy import HttpServer, Router +/// ```python +/// from oxapy import HttpServer, Router /// -/// # Create a server on localhost port 8000 -/// app = HttpServer(("127.0.0.1", 8000)) +/// # Create a server on localhost port 8000 +/// app = HttpServer(("127.0.0.1", 8000)) /// -/// # Create a router -/// router = Router() +/// # Create a router +/// router = Router() /// -/// # Define route handlers -/// @router.get("/") -/// def home(request): -/// return "Hello, World!" +/// # Define route handlers +/// @router.get("/") +/// def home(request): +/// return "Hello, World!" /// -/// @router.get("/users/{user_id}") -/// def get_user(request, user_id: int): -/// return {"user_id": user_id, "name": f"User {user_id}"} +/// @router.get("/users/{user_id}") +/// def get_user(request, user_id: int): +/// return {"user_id": user_id, "name": f"User {user_id}"} /// -/// @router.post("/api/data") -/// def create_data(request): -/// # Access JSON data from the request -/// data = request.json() -/// return {"status": "success", "received": data} +/// @router.post("/api/data") +/// def create_data(request): +/// # Access JSON data from the request +/// data = request.json() +/// return {"status": "success", "received": data} /// -/// # Attach the router to the server -/// app.attach(router) +/// # Attach the router to the server +/// app.attach(router) /// -/// # Run the server -/// app.run() +/// # Run the server +/// app.run() /// ``` #[derive(Clone)] #[pyclass] @@ -140,9 +140,9 @@ impl HttpServer { /// HttpServer: A new server instance ready to be configured. /// /// Example: - /// ```python - /// server = HttpServer(("127.0.0.1", 5555)) - /// ``` + /// ```python + /// server = HttpServer(("127.0.0.1", 5555)) + /// ``` #[new] fn new(addr: (String, u16)) -> PyResult { let (ip, port) = addr; @@ -171,23 +171,23 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// class AppState: - /// def __init__(self): - /// self.counter = 0 - /// # You can store database connection pools here - /// self.db_pool = create_database_pool() - /// - /// app = HttpServer(("127.0.0.1", 5555)) - /// app.app_data(AppState()) - /// - /// # Example of a handler that increments the counter - /// @router.get("/counter") - /// def increment_counter(request): - /// state = request.app_data - /// state.counter += 1 - /// return {"count": state.counter} - /// ``` + /// ```python + /// class AppState: + /// def __init__(self): + /// self.counter = 0 + /// # You can store database connection pools here + /// self.db_pool = create_database_pool() + /// + /// app = HttpServer(("127.0.0.1", 5555)) + /// app.app_data(AppState()) + /// + /// # Example of a handler that increments the counter + /// @router.get("/counter") + /// def increment_counter(request): + /// state = request.app_data + /// state.counter += 1 + /// return {"count": state.counter} + /// ``` fn app_data(&mut self, app_data: Py) { self.app_data = Some(Arc::new(app_data)) } @@ -201,27 +201,27 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// router = Router() - /// - /// # Define a simple hello world handler - /// @router.get("/") - /// def hello(request): - /// return "Hello, World!" - /// - /// # Handler with path parameters - /// @router.get("/users/{user_id}") - /// def get_user(request, user_id: int): - /// return f"User ID: {user_id}" - /// - /// # Handler that returns JSON - /// @router.get("/api/data") - /// def get_data(request): - /// return {"message": "Success", "data": [1, 2, 3]} - /// - /// # Attach the router to the server - /// server.attach(router) - /// ``` + /// ```python + /// router = Router() + /// + /// # Define a simple hello world handler + /// @router.get("/") + /// def hello(request): + /// return "Hello, World!" + /// + /// # Handler with path parameters + /// @router.get("/users/{user_id}") + /// def get_user(request, user_id: int): + /// return f"User ID: {user_id}" + /// + /// # Handler that returns JSON + /// @router.get("/api/data") + /// def get_data(request): + /// return {"message": "Success", "data": [1, 2, 3]} + /// + /// # Attach the router to the server + /// server.attach(router) + /// ``` fn attach(&mut self, router: Router) { self.routers.push(Arc::new(router)); } @@ -237,9 +237,9 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// server.session_store(SessionStore()) - /// ``` + /// ```python + /// server.session_store(SessionStore()) + /// ``` fn session_store(&mut self, session_store: SessionStore) { self.session_store = Some(Arc::new(session_store)); } @@ -253,11 +253,11 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// from oxapy import templating + /// ```python + /// from oxapy import templating /// - /// server.template(templating.Template()) - /// ``` + /// server.template(templating.Template()) + /// ``` fn template(&mut self, template: Template) { self.template = Some(Arc::new(template)) } @@ -271,11 +271,11 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// cors = Cors() - /// cors.origins = ["https://example.com"] - /// server.cors(cors) - /// ``` + /// ```python + /// cors = Cors() + /// cors.origins = ["https://example.com"] + /// server.cors(cors) + /// ``` fn cors(&mut self, cors: Cors) { self.cors = Some(Arc::new(cors)); } @@ -289,9 +289,9 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// server.max_connections(1000) - /// ``` + /// ```python + /// server.max_connections(1000) + /// ``` fn max_connections(&mut self, max_connections: usize) { self.max_connections = Arc::new(Semaphore::new(max_connections)); } @@ -308,9 +308,9 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// server.channel_capacity(200) - /// ``` + /// ```python + /// server.channel_capacity(200) + /// ``` fn channel_capacity(&mut self, channel_capacity: usize) { self.channel_capacity = channel_capacity; } @@ -324,13 +324,13 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// @catcher(Status.NOT_FOUND) - /// def not_found(request, response): - /// return Response("

Page Not Found

", content_type="text/html") + /// ```python + /// @catcher(Status.NOT_FOUND) + /// def not_found(request, response): + /// return Response("

Page Not Found

", content_type="text/html") /// - /// server.catchers([not_found]) - /// ``` + /// server.catchers([not_found]) + /// ``` fn catchers(&mut self, catchers: Vec>, py: Python<'_>) { let mut map = HashMap::default(); @@ -353,15 +353,15 @@ impl HttpServer { /// None /// /// Example: - /// ```python - /// # Run with default number of workers - /// server.run() - /// - /// # Or specify number of workers based on CPU count - /// import multiprocessing - /// workers = multiprocessing.cpu_count() - /// server.run(workers) - /// ``` + /// ```python + /// # Run with default number of workers + /// server.run() + /// + /// # Or specify number of workers based on CPU count + /// import multiprocessing + /// workers = multiprocessing.cpu_count() + /// server.run(workers) + /// ``` #[pyo3(signature=(workers=None))] fn run(&self, workers: Option, py: Python<'_>) -> PyResult<()> { let mut runtime = tokio::runtime::Builder::new_multi_thread(); diff --git a/src/middleware.rs b/src/middleware.rs index 6ebc0a5..1a1037b 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,6 +1,11 @@ use std::sync::Arc; -use pyo3::{ffi::c_str, prelude::*, types::PyDict, Py, PyAny, PyResult, Python}; +use pyo3::{ + ffi::c_str, + prelude::*, + types::{PyDict, PyTuple}, + Py, PyAny, PyResult, Python, +}; #[derive(Clone, Debug)] pub struct Middleware { @@ -24,14 +29,18 @@ impl MiddlewareChain { Self { middlewares } } - pub fn execute<'py>( + pub fn execute<'py, A>( &self, py: Python<'py>, route_handler: &Py, + args: A, kwargs: Bound<'py, PyDict>, - ) -> PyResult> { + ) -> PyResult> + where + A: IntoPyObject<'py, Target = PyTuple>, + { let handler = self.build_middleware_chain(py, route_handler, 0)?; - handler.call(py, (), Some(&kwargs)) + handler.call(py, args, Some(&kwargs)) } fn build_middleware_chain( diff --git a/src/multipart.rs b/src/multipart.rs index c1e8c27..28b0bec 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -18,20 +18,20 @@ use crate::IntoPyException; /// File: A file object containing the uploaded data. /// /// Example: -/// ```python -/// @router.post("/upload") -/// def upload_handler(request): -/// if request.files: -/// image = request.files.get("profile_image") -/// if image: -/// # Access file properties -/// filename = image.name -/// content_type = image.content_type -/// # Save the file -/// image.save(f"uploads/{filename}") -/// return {"status": "success", "filename": filename} -/// return {"status": "error", "message": "No file uploaded"} -/// ``` +/// ```python +/// @router.post("/upload") +/// def upload_handler(request): +/// if request.files: +/// image = request.files.get("profile_image") +/// if image: +/// # Access file properties +/// filename = image.name +/// content_type = image.content_type +/// # Save the file +/// image.save(f"uploads/{filename}") +/// return {"status": "success", "filename": filename} +/// return {"status": "error", "message": "No file uploaded"} +/// ``` #[derive(Clone, Debug)] #[pyclass] pub struct File { @@ -53,10 +53,10 @@ impl File { /// bytes: The file content as a Python bytes object. /// /// Example: - /// ```python - /// file_bytes = uploaded_file.content() - /// file_size = len(file_bytes) - /// ``` + /// ```python + /// file_bytes = uploaded_file.content() + /// file_size = len(file_bytes) + /// ``` fn content<'py>(&'py self, py: Python<'py>) -> Bound<'py, PyBytes> { let data = &self.data.to_vec()[..]; PyBytes::new(py, data) @@ -74,12 +74,12 @@ impl File { /// Exception: If the file cannot be written to disk. /// /// Example: - /// ```python - /// # Save the uploaded file - /// if "profile_image" in request.files: - /// image = request.files["profile_image"] - /// image.save(f"uploads/{image.name}") - /// ``` + /// ```python + /// # Save the uploaded file + /// if "profile_image" in request.files: + /// image = request.files["profile_image"] + /// image.save(f"uploads/{image.name}") + /// ``` fn save(&self, path: String) -> PyResult<()> { std::fs::write(path, &self.data)?; Ok(()) diff --git a/src/request.rs b/src/request.rs index cda2609..b9d0272 100644 --- a/src/request.rs +++ b/src/request.rs @@ -33,15 +33,15 @@ use crate::{ /// Request: A new request object /// /// Example: -/// ```python -/// # Request objects are typically created by the framework and -/// # passed to your handler functions: +/// ```python +/// # Request objects are typically created by the framework and +/// # passed to your handler functions: /// -/// @router.get("/hello") -/// def handler(request): -/// user_agent = request.headers.get("user-agent") -/// return f"Hello from {user_agent}" -/// ``` +/// @router.get("/hello") +/// def handler(request): +/// user_agent = request.headers.get("user-agent") +/// return f"Hello from {user_agent}" +/// ``` #[derive(Clone, Debug, Default)] #[pyclass] pub struct Request { @@ -106,13 +106,13 @@ impl Request { /// Exception: If the body is not present or cannot be parsed as JSON /// /// Example: - /// ```python - /// @router.post("/api/data") - /// def handle_data(request): - /// data = request.json() - /// value = data["key"] - /// return {"received": value} - /// ``` + /// ```python + /// @router.post("/api/data") + /// def handle_data(request): + /// data = request.json() + /// value = data["key"] + /// return {"received": value} + /// ``` pub fn json(&self) -> PyResult> { let data = self .body @@ -130,13 +130,13 @@ impl Request { /// any: The application data object, or None if no app_data was set /// /// Example: - /// ```python - /// @router.get("/counter") - /// def get_counter(request): - /// app_state = request.app_data - /// app_state.counter += 1 - /// return {"count": app_state.counter} - /// ``` + /// ```python + /// @router.get("/counter") + /// def get_counter(request): + /// app_state = request.app_data + /// app_state.counter += 1 + /// return {"count": app_state.counter} + /// ``` #[getter] fn app_data(&self, py: Python<'_>) -> Option> { self.app_data.as_ref().map(|d| d.clone_ref(py)) @@ -154,15 +154,15 @@ impl Request { /// Exception: If the URI cannot be parsed /// /// Example: - /// ```python - /// # For a request to /api?name=John&age=30 - /// @router.get("/api") - /// def api_handler(request): - /// query = request.query() - /// name = query.get("name") - /// age = query.get("age") - /// return {"name": name, "age": age} - /// ``` + /// ```python + /// # For a request to /api?name=John&age=30 + /// @router.get("/api") + /// def api_handler(request): + /// query = request.query() + /// name = query.get("name") + /// age = query.get("age") + /// return {"name": name, "age": age} + /// ``` fn query(&self) -> PyResult>> { let uri: Uri = self.uri.parse().into_py_exception()?; if let Some(query_string) = uri.query() { @@ -188,14 +188,14 @@ impl Request { /// AttributeError: If session store is not configured on the server /// /// Example: - /// ```python - /// @router.get("/login") - /// def login(request): - /// session = request.session() - /// session["user_id"] = 123 - /// session["is_authenticated"] = True - /// return "Logged in successfully" - /// ``` + /// ```python + /// @router.get("/login") + /// def login(request): + /// session = request.session() + /// session["user_id"] = 123 + /// session["is_authenticated"] = True + /// return "Logged in successfully" + /// ``` pub fn session(&self) -> PyResult { let message = "Session not available. Make sure you've configured SessionStore."; let session = self diff --git a/src/response.rs b/src/response.rs index 255dbeb..6e5b500 100644 --- a/src/response.rs +++ b/src/response.rs @@ -22,16 +22,16 @@ use crate::{ /// Response: A new HTTP response. /// /// Example: -/// ```python -/// # JSON response -/// response = Response({"message": "Success"}) -/// -/// # Plain text response -/// response = Response("Hello, World!", content_type="text/plain") -/// -/// # HTML response with custom status -/// response = Response("

Not Found

", Status.NOT_FOUND, "text/html") -/// ``` +/// ```python +/// # JSON response +/// response = Response({"message": "Success"}) +/// +/// # Plain text response +/// response = Response("Hello, World!", content_type="text/plain") +/// +/// # HTML response with custom status +/// response = Response("

Not Found

", Status.NOT_FOUND, "text/html") +/// ``` #[derive(Clone)] #[pyclass(subclass)] pub struct Response { @@ -55,16 +55,16 @@ impl Response { /// Response: A new response object. /// /// Example: - /// ```python - /// # Return JSON - /// response = Response({"message": "Hello"}) - /// - /// # Return plain text - /// response = Response("Hello", content_type="text/plain") - /// - /// # Return error - /// response = Response("Not authorized", status=Status.UNAUTHORIZED) - /// ``` + /// ```python + /// # Return JSON + /// response = Response({"message": "Hello"}) + /// + /// # Return plain text + /// response = Response("Hello", content_type="text/plain") + /// + /// # Return error + /// response = Response("Not authorized", status=Status.UNAUTHORIZED) + /// ``` #[new] #[pyo3(signature=(body, status = Status::OK , content_type="application/json"))] pub fn new( @@ -110,10 +110,10 @@ impl Response { /// Response: The response instance (for method chaining). /// /// Example: - /// ```python - /// response = Response("Hello") - /// response.insert_header("Cache-Control", "no-cache") - /// ``` + /// ```python + /// response = Response("Hello") + /// response.insert_header("Cache-Control", "no-cache") + /// ``` pub fn insert_header(&mut self, key: &str, value: String) -> Self { self.headers.insert(key.to_string(), value); self.clone() @@ -143,13 +143,13 @@ impl Response { /// Redirect: A redirect response. /// /// Example: -/// ```python -/// # Redirect to the home page -/// return Redirect("/home") -/// -/// # Redirect to an external site -/// return Redirect("https://example.com") -/// ``` +/// ```python +/// # Redirect to the home page +/// return Redirect("/home") +/// +/// # Redirect to an external site +/// return Redirect("https://example.com") +/// ``` #[pyclass(subclass, extends=Response)] pub struct Redirect; @@ -164,13 +164,13 @@ impl Redirect { /// Redirect: A redirect response with status 301 (Moved Permanently). /// /// Example: - /// ```python - /// # Redirect user after form submission - /// @router.post("/submit") - /// def submit_form(request): - /// # Process form... - /// return Redirect("/thank-you") - /// ``` + /// ```python + /// # Redirect user after form submission + /// @router.post("/submit") + /// def submit_form(request): + /// # Process form... + /// return Redirect("/thank-you") + /// ``` #[new] fn new(location: String) -> (Self, Response) { ( diff --git a/src/routing.rs b/src/routing.rs index 21d2f9d..b48e415 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -20,15 +20,15 @@ pub type MatchRoute<'l> = matchit::Match<'l, 'l, &'l Route>; /// Route: A route object that can be registered with a router. /// /// Example: -/// ```python -/// from oxapy import Route +/// ```python +/// from oxapy import Route /// -/// def handler(request): -/// return "Hello, World!" +/// def handler(request): +/// return "Hello, World!" /// -/// route = Route("/hello", "GET") -/// route = route(handler) # Attach the handler -/// ``` +/// route = Route("/hello", "GET") +/// route = route(handler) # Attach the handler +/// ``` #[derive(Clone, Debug)] #[pyclass] pub struct Route { @@ -72,8 +72,14 @@ impl Route { } macro_rules! method_decorator { - ($($method:ident),*) => { + ( $( + $(#[$docs:meta])* + $method:ident; + )* + ) => { + $( + $(#[$docs])* #[pyfunction] #[pyo3(signature = (path, handler = None))] pub fn $method(path: String, handler: Option>, py: Python<'_>) -> Route { @@ -87,7 +93,112 @@ macro_rules! method_decorator { }; } -method_decorator!(get, post, put, patch, delete, head, options); +method_decorator!( + /// Registers an HTTP GET route. + /// + /// Parameters: + /// path (str): The route path, which may include parameters (e.g. `/items/{id}`). + /// handler (callable | None): Optional Python function that handles the request. + /// + /// Returns: + /// Route: A GET Route instance. + /// + /// Example: + /// ```python + /// get("/hello/{name}", lambda req, name: f"Hello, {name}!") + /// ``` + get; + + /// Registers an HTTP POST route. + /// + /// Parameters: + /// path (str): The POST route path. + /// handler (callable | None): Optional Python function that handles the request. + /// + /// Returns: + /// Route: A POST Route instance. + /// + /// Example: + /// ```python + /// post("/users", lambda req: {"id": 1, "name": req.json()["name"]}) + /// ``` + post; + + /// Registers an HTTP DELETE route. + /// + /// Parameters: + /// path (str): The DELETE route path. + /// handler (callable | None): Optional Python function that handles the request. + /// + /// Returns: + /// Route: A DELETE Route instance. + /// + /// Example: + /// ```python + /// delete("/items/{id}", lambda req, id: f"Deleted {id}") + /// ``` + delete; + + /// Registers an HTTP PATCH route. + /// + /// Parameters: + /// path (str): The PATCH route path. + /// handler (callable | None): Optional Python function for partial updates. + /// + /// Returns: + /// Route: A PATCH Route instance. + /// + /// Example: + /// ```python + /// patch("/users/{id}", lambda req, id: req.json()) + /// ``` + patch; + + /// Registers an HTTP PUT route. + /// + /// Parameters: + /// path (str): The PUT route path. + /// handler (callable | None): Optional Python function for full replacement. + /// + /// Returns: + /// Route: A PUT Route instance. + /// + /// Example: + /// ```python + /// put("/users/{id}", lambda req, id: req.json()) + /// ``` + put; + + /// Registers an HTTP HEAD route. + /// + /// Parameters: + /// path (str): The HEAD route path. + /// handler (callable | None): Optional function for returning headers only. + /// + /// Returns: + /// Route: A HEAD Route instance. + /// + /// Example: + /// ```python + /// head("/status", lambda req: None) + /// ``` + head; + + /// Registers an HTTP OPTIONS route. + /// + /// Parameters: + /// path (str): The OPTIONS route path. + /// handler (callable | None): Optional handler that returns allowed methods. + /// + /// Returns: + /// Route: An OPTIONS Route instance. + /// + /// Example: + /// ```python + /// options("/users", lambda req: {"Allow": "GET, POST"}) + /// ``` + options; +); #[derive(Clone)] #[pyclass] @@ -121,15 +232,15 @@ impl RouteBuilder { /// Router: A new router instance. /// /// Example: -/// ```python -/// from oxapy import Router, get +/// ```python +/// from oxapy import Router, get /// -/// router = Router() +/// router = Router() /// -/// @router.get("/hello/{name}") -/// def hello(request, name): -/// return f"Hello, {name}!" -/// ``` +/// @router.get("/hello/{name}") +/// def hello(request, name): +/// return f"Hello, {name}!" +/// ``` #[derive(Default, Clone, Debug)] #[pyclass] pub struct Router { @@ -138,7 +249,12 @@ pub struct Router { } macro_rules! impl_router { - ($($method:ident),*) => { + ( + $( + $(#[$docs:meta])* + $method:ident; + )* + ) => { #[pymethods] impl Router { /// Create a new Router instance. @@ -147,9 +263,9 @@ macro_rules! impl_router { /// Router: A new router with no routes or middleware. /// /// Example: - /// ```python - /// router = Router() - /// ``` + /// ```python + /// router = Router() + /// ``` #[new] pub fn new() -> Self { Router::default() @@ -167,14 +283,14 @@ macro_rules! impl_router { /// None /// /// Example: - /// ```python - /// def auth_middleware(request, next, **kwargs): - /// if "authorization" not in request.headers: - /// return Status.UNAUTHORIZED - /// return next(request, **kwargs) + /// ```python + /// def auth_middleware(request, next, **kwargs): + /// if "authorization" not in request.headers: + /// return Status.UNAUTHORIZED + /// return next(request, **kwargs) /// - /// router.middleware(auth_middleware) - /// ``` + /// router.middleware(auth_middleware) + /// ``` fn middleware(&mut self, middleware: Py) { let middleware = Middleware::new(middleware); self.middlewares.push(middleware); @@ -192,15 +308,15 @@ macro_rules! impl_router { /// Exception: If the route cannot be added. /// /// Example: - /// ```python - /// from oxapy import get + /// ```python + /// from oxapy import get /// - /// def hello_handler(request): - /// return "Hello World!" + /// def hello_handler(request): + /// return "Hello World!" /// - /// route = get("/hello", hello_handler) - /// router.route(route) - /// ``` + /// route = get("/hello", hello_handler) + /// router.route(route) + /// ``` fn route(&mut self, route: &Route) -> PyResult<()> { let mut ptr_mr = self.routes.write().unwrap(); let method_router = ptr_mr.entry(route.method.clone()).or_default(); @@ -222,21 +338,21 @@ macro_rules! impl_router { /// Exception: If any route cannot be added. /// /// Example: - /// ```python - /// from oxapy import get, post + /// ```python + /// from oxapy import get, post /// - /// def hello_handler(request): - /// return "Hello World!" + /// def hello_handler(request): + /// return "Hello World!" /// - /// def submit_handler(request): - /// return "Form submitted!" + /// def submit_handler(request): + /// return "Form submitted!" /// - /// routes = [ - /// get("/hello", hello_handler), - /// post("/submit", submit_handler) - /// ] - /// router.routes(routes) - /// ``` + /// routes = [ + /// get("/hello", hello_handler), + /// post("/submit", submit_handler) + /// ] + /// router.routes(routes) + /// ``` fn routes(&mut self, routes: Vec) -> PyResult<()> { for ref route in routes { self.route(route)?; @@ -244,15 +360,16 @@ macro_rules! impl_router { Ok(()) } - $( - fn $method(&self, path: String) -> PyResult { - Ok(RouteBuilder { - method: stringify!($method).to_string().to_uppercase(), - router: self.clone(), - path, - }) - } - )+ + $( + $(#[$docs])* + fn $method(&self, path: String) -> PyResult { + Ok(RouteBuilder { + method: stringify!($method).to_string().to_uppercase(), + router: self.clone(), + path, + }) + } + )+ fn __repr__(&self) -> String { format!("{:#?}", self) @@ -261,7 +378,77 @@ macro_rules! impl_router { }; } -impl_router!(get, post, put, patch, delete, head, options); +impl_router!( + /// Register a GET route using the decorator `@router.get(path)`. + /// + /// Example: + /// ```python + /// @router.get("/hello") + /// def hello(request): + /// return "Hello, world!" + /// ``` + get; + + /// Register a POST route using the decorator `@router.post(path)`. + /// + /// Example: + /// ```python + /// @router.post("/submit") + /// def submit(request): + /// return "Submitted!" + /// ``` + post; + + /// Register a PUT route using the decorator `@router.put(path)`. + /// + /// Example: + /// ```python + /// @router.put("/items/{id}") + /// def update_item(request, id): + /// return f"Updated item {id}" + /// ``` + put; + + /// Register a PATCH route using the decorator `@router.patch(path)`. + /// + /// Example: + /// ```python + /// @router.patch("/items/{id}") + /// def patch_item(request, id): + /// return f"Patched item {id}" + /// ``` + patch; + + /// Register a DELETE route using the decorator `@router.delete(path)`. + /// + /// Example: + /// ```python + /// @router.delete("/items/{id}") + /// def delete_item(request, id): + /// return f"Deleted item {id}" + /// ``` + delete; + + /// Register a HEAD route using the decorator `@router.head(path)`. + /// + /// Example: + /// ```python + /// @router.head("/ping") + /// def head_ping(request): + /// return "" + /// ``` + head; + + /// Register an OPTIONS route using the decorator `@router.options(path)`. + /// + /// Example: + /// ```python + /// @router.options("/data") + /// def options_data(request): + /// return "OPTIONS OK" + /// ``` + options; +); impl Router { pub(crate) fn find<'l>(&'l self, method: &str, uri: &'l str) -> Option> { @@ -284,13 +471,13 @@ impl Router { /// Route: A route configured to serve static files. /// /// Example: -/// ```python -/// from oxapy import Router, static_file +/// ```python +/// from oxapy import Router, static_file /// -/// router = Router() -/// router.route(static_file("./static", "static")) -/// # This will serve files from ./static directory at /static URL path -/// ``` +/// router = Router() +/// router.route(static_file("./static", "static")) +/// # This will serve files from ./static directory at /static URL path +/// ``` #[pyfunction] pub fn static_file(directory: String, path: String, py: Python<'_>) -> PyResult { let pathlib = py.import("pathlib")?; diff --git a/src/serializer/fields.rs b/src/serializer/fields.rs index ecf9d3c..390fb63 100644 --- a/src/serializer/fields.rs +++ b/src/serializer/fields.rs @@ -79,19 +79,6 @@ impl Field { } } -static TYPE_STR: &str = "type"; -static FORMAT_STR: &str = "format"; -static MIN_LEN_STR: &str = "minLength"; -static MAX_LEN_STR: &str = "maxLength"; -static MIN_STR: &str = "minimum"; -static MAX_STR: &str = "maximum"; -static PATTERN_STR: &str = "pattern"; -static ENUM_STR: &str = "enum"; -static TITLE_STR: &str = "title"; -static DESC_STR: &str = "description"; -static ARRAY_STR: &str = "array"; -static ITEMS_STR: &str = "items"; - impl Field { pub fn to_json_schema_value(&self) -> Value { let capacity = 1 @@ -106,30 +93,30 @@ impl Field { + self.description.is_some() as usize; let mut schema = serde_json::Map::with_capacity(capacity); - schema.insert(TYPE_STR.to_string(), Value::String(self.ty.clone())); + schema.insert("type".to_string(), Value::String(self.ty.clone())); if let Some(fmt) = &self.format { - schema.insert(FORMAT_STR.to_string(), Value::String(fmt.clone())); + schema.insert("format".to_string(), Value::String(fmt.clone())); } if let Some(min_length) = self.min_length { - schema.insert(MIN_LEN_STR.to_string(), Value::Number(min_length.into())); + schema.insert("minLength".to_string(), Value::Number(min_length.into())); } if let Some(max_length) = self.max_length { - schema.insert(MAX_LEN_STR.to_string(), Value::Number(max_length.into())); + schema.insert("maxLength".to_string(), Value::Number(max_length.into())); } if let Some(minimum) = self.minimum { - schema.insert(MIN_STR.to_string(), serde_json::json!(minimum)); + schema.insert("minimum".to_string(), serde_json::json!(minimum)); } if let Some(maximum) = self.maximum { - schema.insert(MAX_STR.to_string(), serde_json::json!(maximum)); + schema.insert("maximum".to_string(), serde_json::json!(maximum)); } if let Some(pattern) = &self.pattern { - schema.insert(PATTERN_STR.to_string(), Value::String(pattern.clone())); + schema.insert("pattern".to_string(), Value::String(pattern.clone())); } if let Some(enum_values) = &self.enum_values { @@ -137,21 +124,24 @@ impl Field { .iter() .map(|v| Value::String(v.clone())) .collect(); - schema.insert(ENUM_STR.to_string(), Value::Array(enum_array)); + schema.insert("enum".to_string(), Value::Array(enum_array)); } if let Some(title) = &self.title { - schema.insert(TITLE_STR.to_string(), Value::String(title.clone())); + schema.insert("title".to_string(), Value::String(title.clone())); } if let Some(description) = &self.description { - schema.insert(DESC_STR.to_string(), Value::String(description.clone())); + schema.insert( + "description".to_string(), + Value::String(description.clone()), + ); } if self.many.unwrap_or(false) { let mut array_schema = serde_json::Map::with_capacity(2); - array_schema.insert(TYPE_STR.to_string(), Value::String(ARRAY_STR.to_string())); - array_schema.insert(ITEMS_STR.to_string(), Value::Object(schema)); + array_schema.insert("type".to_string(), Value::String("array".to_string())); + array_schema.insert("items".to_string(), Value::Object(schema)); return Value::Object(array_schema); } diff --git a/src/serializer/mod.rs b/src/serializer/mod.rs index 862a558..9344e03 100644 --- a/src/serializer/mod.rs +++ b/src/serializer/mod.rs @@ -204,16 +204,6 @@ impl Serializer { static CACHES_JSON_SCHEMA_VALUE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); -static TYPE_STR: &str = "type"; -static ARRAY_STR: &str = "array"; -static OBJECT_STR: &str = "object"; -static ITEMS_STR: &str = "items"; -static TITLE_STR: &str = "title"; -static DESC_STR: &str = "description"; -static PROPS_STR: &str = "properties"; -static ADD_PROPS_STR: &str = "additionalProperties"; -static REQUIRED_STR: &str = "required"; - impl Serializer { fn json_schema_value(cls: &Bound<'_, PyType>) -> PyResult { let mut properties = serde_json::Map::with_capacity(16); @@ -272,9 +262,8 @@ impl Serializer { if is_field_many { let mut array_schema = serde_json::Map::with_capacity(2); - array_schema - .insert(TYPE_STR.to_string(), Value::String(ARRAY_STR.to_string())); - array_schema.insert(ITEMS_STR.to_string(), nested_schema); + array_schema.insert("type".to_string(), Value::String("array".to_string())); + array_schema.insert("items".to_string(), nested_schema); properties.insert(attr_name, Value::Object(array_schema)); } else { properties.insert(attr_name, nested_schema); @@ -290,26 +279,26 @@ impl Serializer { } let mut schema = serde_json::Map::with_capacity(5); - schema.insert(TYPE_STR.to_string(), Value::String(OBJECT_STR.to_string())); - schema.insert(PROPS_STR.to_string(), Value::Object(properties)); - schema.insert(ADD_PROPS_STR.to_string(), Value::Bool(false)); + schema.insert("type".to_string(), Value::String("object".to_string())); + schema.insert("properties".to_string(), Value::Object(properties)); + schema.insert("additionalProperties".to_string(), Value::Bool(false)); if !required_fields.is_empty() { let reqs: Vec = required_fields.into_iter().map(Value::String).collect(); - schema.insert(REQUIRED_STR.to_string(), Value::Array(reqs)); + schema.insert("required".to_string(), Value::Array(reqs)); } if let Some(t) = title { - schema.insert(TITLE_STR.to_string(), Value::String(t)); + schema.insert("title".to_string(), Value::String(t)); } if let Some(d) = description { - schema.insert(DESC_STR.to_string(), Value::String(d)); + schema.insert("description".to_string(), Value::String(d)); } let final_schema = if is_many { let mut array_schema = serde_json::Map::with_capacity(2); - array_schema.insert(TYPE_STR.to_string(), Value::String(ARRAY_STR.to_string())); - array_schema.insert(ITEMS_STR.to_string(), Value::Object(schema)); + array_schema.insert("type".to_string(), Value::String("array".to_string())); + array_schema.insert("items".to_string(), Value::Object(schema)); Value::Object(array_schema) } else { Value::Object(schema) diff --git a/src/session.rs b/src/session.rs index a55da0b..a1312e5 100644 --- a/src/session.rs +++ b/src/session.rs @@ -31,14 +31,14 @@ pub fn generate_session_id() -> String { /// Session: A new session instance. /// /// Example: -/// ```python -/// # Sessions are typically accessed from the request object: -/// @router.get("/profile") -/// def profile(request): -/// session = request.session() -/// session["last_visit"] = "today" -/// return {"user_id": session.get("user_id")} -/// ``` +/// ```python +/// # Sessions are typically accessed from the request object: +/// @router.get("/profile") +/// def profile(request): +/// session = request.session() +/// session["last_visit"] = "today" +/// return {"user_id": session.get("user_id")} +/// ``` #[derive(Clone, Debug)] #[pyclass] pub struct Session { @@ -62,10 +62,10 @@ impl Session { /// Session: A new session instance. /// /// Example: - /// ```python - /// # Manual session creation (normally handled by the framework) - /// session = Session() - /// ``` + /// ```python + /// # Manual session creation (normally handled by the framework) + /// session = Session() + /// ``` #[new] fn new(id: Option) -> PyResult { let now = SystemTime::now() @@ -91,11 +91,11 @@ impl Session { /// any: The value associated with the key, or None if the key doesn't exist. /// /// Example: - /// ```python - /// user_id = session.get("user_id") - /// if user_id is not None: - /// # User is logged in - /// ``` + /// ```python + /// user_id = session.get("user_id") + /// if user_id is not None: + /// # User is logged in + /// ``` fn get(&self, key: &str, py: Python<'_>) -> PyResult { *self.last_accessed.lock().into_py_exception()? = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -122,11 +122,11 @@ impl Session { /// None /// /// Example: - /// ```python - /// # Store user information in the session - /// session.set("user_id", 123) - /// session.set("is_admin", False) - /// ``` + /// ```python + /// # Store user information in the session + /// session.set("user_id", 123) + /// session.set("is_admin", False) + /// ``` fn set(&self, key: &str, value: PyObject) -> PyResult<()> { let mut data = self.data.write().into_py_exception()?; data.insert(key.to_string(), value); @@ -143,11 +143,11 @@ impl Session { /// None /// /// Example: - /// ```python - /// # Log user out by removing their session data - /// session.remove("user_id") - /// session.remove("is_admin") - /// ``` + /// ```python + /// # Log user out by removing their session data + /// session.remove("user_id") + /// session.remove("is_admin") + /// ``` fn remove(&self, key: &str) -> PyResult<()> { let mut data = self.data.write().into_py_exception()?; if data.remove(key).is_some() { @@ -165,10 +165,10 @@ impl Session { /// None /// /// Example: - /// ```python - /// # Clear all session data (e.g., during logout) - /// session.clear() - /// ``` + /// ```python + /// # Clear all session data (e.g., during logout) + /// session.clear() + /// ``` fn clear(&self) -> PyResult<()> { let mut data = self.data.write().into_py_exception()?; if !data.is_empty() { @@ -187,11 +187,11 @@ impl Session { /// list: A list of all keys in the session. /// /// Example: - /// ```python - /// # Check what data is stored in the session - /// for key in session.keys(): - /// print(f"Session contains: {key}") - /// ``` + /// ```python + /// # Check what data is stored in the session + /// for key in session.keys(): + /// print(f"Session contains: {key}") + /// ``` fn keys(&self, py: Python<'_>) -> PyResult { let data = self.data.read().into_py_exception()?; let keys: Vec = data.keys().cloned().collect(); @@ -283,19 +283,19 @@ impl Session { /// SessionStore: A new session store instance. /// /// Example: -/// ```python -/// from oxapy import HttpServer, SessionStore -/// -/// app = HttpServer(("127.0.0.1", 8000)) -/// -/// # Configure sessions with custom settings -/// store = SessionStore( -/// cookie_name="my_app_session", -/// cookie_secure=True, -/// expiry_seconds=3600 # 1 hour -/// ) -/// app.session_store(store) -/// ``` +/// ```python +/// from oxapy import HttpServer, SessionStore +/// +/// app = HttpServer(("127.0.0.1", 8000)) +/// +/// # Configure sessions with custom settings +/// store = SessionStore( +/// cookie_name="my_app_session", +/// cookie_secure=True, +/// expiry_seconds=3600 # 1 hour +/// ) +/// app.session_store(store) +/// ``` #[derive(Clone, Debug)] #[pyclass] pub struct SessionStore { @@ -333,17 +333,17 @@ impl SessionStore { /// SessionStore: A new session store instance. /// /// Example: - /// ```python - /// # Create a session store with default settings - /// store = SessionStore() - /// - /// # Create a session store with custom settings - /// secure_store = SessionStore( - /// cookie_name="secure_session", - /// cookie_secure=True, - /// cookie_same_site="Strict" - /// ) - /// ``` + /// ```python + /// # Create a session store with default settings + /// store = SessionStore() + /// + /// # Create a session store with custom settings + /// secure_store = SessionStore( + /// cookie_name="secure_session", + /// cookie_secure=True, + /// cookie_same_site="Strict" + /// ) + /// ``` #[new] #[pyo3(signature = ( cookie_name = "session".to_string(), @@ -415,10 +415,10 @@ impl SessionStore { /// bool: True if the session was found and removed, False otherwise. /// /// Example: - /// ```python - /// # Clear a specific session - /// session_store.clear_session("abcd1234") - /// ``` + /// ```python + /// # Clear a specific session + /// session_store.clear_session("abcd1234") + /// ``` fn clear_session(&self, session_id: &str) -> PyResult { let mut sessions = self.sessions.write().into_py_exception()?; Ok(sessions.remove(session_id).is_some()) @@ -433,11 +433,11 @@ impl SessionStore { /// int: The number of active sessions in the store. /// /// Example: - /// ```python - /// # Check how many active sessions exist - /// count = session_store.session_count() - /// print(f"Active sessions: {count}") - /// ``` + /// ```python + /// # Check how many active sessions exist + /// count = session_store.session_count() + /// print(f"Active sessions: {count}") + /// ``` fn session_count(&self) -> PyResult { let sessions = self.sessions.read().into_py_exception()?; Ok(sessions.len()) diff --git a/src/status.rs b/src/status.rs index 59e5aff..014d04c 100644 --- a/src/status.rs +++ b/src/status.rs @@ -16,20 +16,20 @@ use crate::response::Response; /// - 5xx: Server error responses /// /// Example: -/// ```python -/// from oxapy import Status, Response -/// -/// # Create a not found response -/// response = Response("Not found", status=Status.NOT_FOUND) -/// -/// # Check status in a handler -/// @router.get("/resource/{id}") -/// def get_resource(request, id): -/// resource = find_resource(id) -/// if resource is None: -/// return Status.NOT_FOUND -/// return resource -/// ``` +/// ```python +/// from oxapy import Status, Response +/// +/// # Create a not found response +/// response = Response("Not found", status=Status.NOT_FOUND) +/// +/// # Check status in a handler +/// @router.get("/resource/{id}") +/// def get_resource(request, id): +/// resource = find_resource(id) +/// if resource is None: +/// return Status.NOT_FOUND +/// return resource +/// ``` #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[pyclass] #[allow(non_camel_case_types, clippy::upper_case_acronyms)] @@ -180,11 +180,11 @@ impl Status { /// bool: The result of the comparison. /// /// Example: - /// ```python - /// # Check if a status code is a success code (2xx) - /// if status >= Status.OK and status < Status.MULTIPLE_CHOICES: - /// print("Success!") - /// ``` + /// ```python + /// # Check if a status code is a success code (2xx) + /// if status >= Status.OK and status < Status.MULTIPLE_CHOICES: + /// print("Success!") + /// ``` fn __richcmp__( &self, other: PyRef, diff --git a/src/templating/mod.rs b/src/templating/mod.rs index 370d371..395623b 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -26,18 +26,18 @@ mod tera; /// PyException: If an invalid engine type is specified. /// /// Example: -/// ```python -/// from oxapy import HttpServer -/// from oxapy.templating import Template +/// ```python +/// from oxapy import HttpServer +/// from oxapy.templating import Template /// -/// app = HttpServer(("127.0.0.1", 8000)) +/// app = HttpServer(("127.0.0.1", 8000)) /// -/// # Configure templates with default settings (Jinja) -/// app.template(Template()) +/// # Configure templates with default settings (Jinja) +/// app.template(Template()) /// -/// # Or use Tera with custom template directory -/// app.template(Template("./views/**/*.html", "tera")) -/// ``` +/// # Or use Tera with custom template directory +/// app.template(Template("./views/**/*.html", "tera")) +/// ``` #[derive(Clone, Debug)] #[pyclass] pub enum Template { @@ -60,13 +60,13 @@ impl Template { /// PyException: If an invalid engine type is specified. /// /// Example: - /// ```python - /// # Use Jinja with default template directory - /// template = Template() + /// ```python + /// # Use Jinja with default template directory + /// template = Template() /// - /// # Use Tera with custom template directory - /// template = Template("./views/**/*.html", "tera") - /// ``` + /// # Use Tera with custom template directory + /// template = Template("./views/**/*.html", "tera") + /// ``` #[new] #[pyo3(signature=(dir="./templates/**/*.html", engine="jinja"))] fn new(dir: &str, engine: &str) -> PyResult