doc: add readme
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# urus
|
||||
|
||||
A cowboy/bandit-style HTTP library for the smarm actor runtime.
|
||||
|
||||
## Overview
|
||||
|
||||
`urus` is a lightweight, actor-first HTTP/1.1 library designed to integrate seamlessly with the [smarm](https://github.com/Markk116/smarm) actor runtime. Instead of traditional shared mutable state and locking patterns, `urus` embraces the actor model: connection handling is fully concurrent, and request processing pipelines are message-passing all the way down.
|
||||
|
||||
**Key design principles:**
|
||||
- **No locks**: State is owned by actors; concurrency is via channels.
|
||||
- **Actor-native**: Built from the ground up for smarm; each connection is an actor.
|
||||
- **Pluggable pipelines**: Compose HTTP request handling logic with `Plug` and `Pipeline`.
|
||||
- **Fast HTTP/1.1 parsing**: Uses `httparse` for robust, battle-tested RFC 7230 compliance.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
urus = { path = "../urus" }
|
||||
smarm = { path = "../smarm" }
|
||||
```
|
||||
|
||||
### Minimal Example
|
||||
|
||||
```rust
|
||||
use urus::{Pipeline, Router, Conn, Next, serve};
|
||||
|
||||
let pipeline = Pipeline::new().plug(
|
||||
Router::new()
|
||||
.get("/", |c: Conn, _n: Next| {
|
||||
c.put_status(200).put_body("hello")
|
||||
})
|
||||
);
|
||||
|
||||
serve("0.0.0.0:8080", pipeline).unwrap();
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
cargo run --example hello
|
||||
curl http://localhost:8080/
|
||||
```
|
||||
|
||||
### Routing with Path Parameters
|
||||
|
||||
```rust
|
||||
Router::new()
|
||||
.get("/users/:id", |c: Conn, _n: Next| {
|
||||
let id = c.params.get("id").unwrap_or("").to_string();
|
||||
c.put_status(200).put_body(format!("user: {}", id))
|
||||
})
|
||||
```
|
||||
|
||||
Path parameters are extracted into `c.params: HashMap<String, String>`.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### `Conn` — The Connection Object
|
||||
|
||||
The `Conn` struct represents a single HTTP request/response pair:
|
||||
|
||||
```rust
|
||||
pub struct Conn {
|
||||
pub method: Method,
|
||||
pub path: String,
|
||||
pub params: HashMap<String, String>, // Path parameters (e.g., :id)
|
||||
pub assigns: Assigns, // Request-scoped state
|
||||
pub body: Body, // Request body
|
||||
pub headers: HeaderMap, // Request headers
|
||||
pub status: u16, // Response status (default 200)
|
||||
pub resp_headers: HeaderMap, // Response headers
|
||||
pub resp_body: RespBody, // Response body
|
||||
// ... (internal fields)
|
||||
}
|
||||
```
|
||||
|
||||
Builders make it ergonomic to modify response state:
|
||||
|
||||
```rust
|
||||
c.put_status(201)
|
||||
.put_header("content-type", "application/json")
|
||||
.put_body(r#"{"ok": true}"#)
|
||||
```
|
||||
|
||||
`c.assigns` is a request-scoped key-value store (similar to Plug's Assigns in Elixir/Phoenix) for passing data between plugs in a pipeline.
|
||||
|
||||
### `Pipeline` — Composable Handlers
|
||||
|
||||
A pipeline chains plugs (middleware/handlers) together. Each plug receives a `Conn`, can modify it, and passes it to the next plug via the `Next` callback:
|
||||
|
||||
```rust
|
||||
use urus::{Pipeline, Plug, Conn, Next};
|
||||
|
||||
struct LoggingPlug;
|
||||
|
||||
impl Plug for LoggingPlug {
|
||||
fn call(&self, mut c: Conn, n: Next) -> Conn {
|
||||
println!("{} {}", c.method, c.path);
|
||||
n.call(c)
|
||||
}
|
||||
}
|
||||
|
||||
let pipeline = Pipeline::new()
|
||||
.plug(LoggingPlug)
|
||||
.plug(Router::new().get("/", |c, _| c.put_body("ok")));
|
||||
```
|
||||
|
||||
### `Router` — Path Matching
|
||||
|
||||
The built-in router matches HTTP methods and paths:
|
||||
|
||||
```rust
|
||||
Router::new()
|
||||
.get("/", handler_fn)
|
||||
.post("/users", create_user)
|
||||
.get("/users/:id", get_user)
|
||||
.put("/users/:id", update_user)
|
||||
.delete("/users/:id", delete_user)
|
||||
```
|
||||
|
||||
Handlers are closures taking `(Conn, Next) -> Conn`. Call `Next::call(c)` to continue to the next plug; omitting it short-circuits the pipeline (e.g., for authentication failures).
|
||||
|
||||
### Server Configuration
|
||||
|
||||
`serve(addr, pipeline)` binds and listens on the given address. For more control, use `serve_with(config, pipeline)`:
|
||||
|
||||
```rust
|
||||
use urus::{serve_with, Config};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
let cfg = Config {
|
||||
listener_pool: 2, // Number of OS threads handling accept()
|
||||
scheduler_threads: Some(2), // Number of smarm worker threads
|
||||
..Config::new("127.0.0.1:8080".parse().unwrap())
|
||||
};
|
||||
|
||||
serve_with(cfg, pipeline).unwrap();
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### CRUD with Actor Ownership
|
||||
|
||||
See [`examples/crud.rs`](examples/crud.rs) for a complete example demonstrating:
|
||||
|
||||
- A background "store" actor that owns all data.
|
||||
- Handlers sending requests to the store via a channel.
|
||||
- No locks, no shared mutable state.
|
||||
- Automatic JSON serialization and file persistence.
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
cargo run --example crud
|
||||
curl -s http://localhost:8080/users
|
||||
curl -s -X POST -d '{"name":"alice","email":"a@x"}' http://localhost:8080/users
|
||||
curl -s http://localhost:8080/users/1
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Integration tests spawn the server on an ephemeral port and issue real TCP requests:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
Tests in `tests/integration.rs` cover:
|
||||
- Basic routing (GET, POST, PUT, DELETE)
|
||||
- Request body echoing
|
||||
- HTTP header parsing
|
||||
- Path parameter extraction
|
||||
- Status code responses
|
||||
|
||||
## Architecture
|
||||
|
||||
### Connection Actors
|
||||
|
||||
Each incoming TCP connection is handled by a dedicated actor (spawned in `conn_actor.rs`). The actor:
|
||||
|
||||
1. Parses the HTTP request line and headers.
|
||||
2. Reads the body (if present).
|
||||
3. Runs the request through the pipeline.
|
||||
4. Writes the HTTP response to the socket.
|
||||
5. Closes the connection (or handles pipelining if HTTP/1.1 Keep-Alive is enabled).
|
||||
|
||||
All of this happens concurrently with other connections—no thread pool juggling required. The smarm scheduler handles actor fairness.
|
||||
|
||||
### Parsing
|
||||
|
||||
HTTP request parsing uses the robust `httparse` crate, which handles:
|
||||
- Chunked transfer encoding (for request bodies).
|
||||
- Header validation.
|
||||
- Method and URI parsing.
|
||||
- HTTP version detection.
|
||||
|
||||
### No Async/Await
|
||||
|
||||
`urus` uses **synchronous code with blocking channels**. This is intentional:
|
||||
|
||||
- Simpler to reason about; no complex state machines.
|
||||
- Each actor runs in a smarm worker thread, blocked on I/O or channels.
|
||||
- The scheduler multiplexes many actors across a thread pool.
|
||||
|
||||
This avoids the complexity of async ecosystems while maintaining full concurrency.
|
||||
|
||||
## Roadmap
|
||||
|
||||
v1 covers HTTP/1.1, the plug pipeline, and a built-in router. Future versions may include:
|
||||
- Middleware ecosystem (auth, logging, compression, etc.).
|
||||
- WebSocket support.
|
||||
- Benchmarking suite (see `urus-bench-spec.md`).
|
||||
- Additional utilities and examples.
|
||||
|
||||
Refer to `urus-spec.md` and `urus-v1-build-notes.md` in the artifact persistence for the original design and implementation notes.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user