# 🦊 Fluffer Fluffer is a fun and experimental gemini server framework. ## πŸ“” Overview Routes are generic functions that return anything implementing the [`GemBytes`] trait. There are some helpful implementations out of the box. Please consult [`GemBytes`] and [`Fluff`] while you experiment. Also check out the [examples](https://codeberg.org/catboomer/fluffer/src/branch/main/examples). ``` rust use fluffer::{App, Fluff}; #[tokio::main] async fn main() { App::default() .route("/", |_| async { "# Welcome\n=> /u32 Should show a number\n=> /pic 🦊 Here's a cool picture!" }) .route("/u32", |_| async { 777 }) .route("/pic", |_| async { Fluff::File("picture.png".to_string()) }) .run() .await; } ``` ## πŸ’Ž GemBytes The [`GemBytes`] trait has one method for returning a gemini byte response: ``` text \r\n ``` Remember you must include the `` characterβ€”even if `` is blank. To implement [`GemBytes`] on a type is to decide the response appropriate for it. For example: you may represent a mime-ambiguous type as formatted gemtext. ``` rust use fluffer::{GemBytes, async_trait}; struct Profile { name: String, bio: String, } #[async_trait] impl GemBytes for Profile { async fn gem_bytes(&self) -> Vec { format!("20 text/gemini\r\n# {},\n\n## Bio\n\n{}", self.name, self.bio).into_bytes() } } ``` ## πŸ™ƒ Identity Gemini uses certificates to identify clients. The [`Client`] struct implements common functionality. ## πŸ”— Input, queries, and parameters ### Input Calling [`Client::input`] returns the request's query line percent-decoded. ``` rust App::default() .route("/" |c| async { c.input().unwrap_or("no input πŸ˜₯".to_string()) }) .run() .await .unwrap() ``` ### Queries For routes where you aren't also accounting for a user's input, queries are suitable for tracking UI state across requests. For example, you can add warning or error messages to a gemtext document by redirecting to a path with special query names. (E.g. `/home?err=bad%20thingg%20happened`), The Fluff variant [`Fluff::RedirectQueries`] helps by redirecting to a route with a vector of key-value queries. Use [`Client::query`] to inspect query values. ### Parameters Parameters are derived from patterns you define in a route's path. Define a parameter in your route string, and access it by calling [`Client::parameter`]. ``` rust App::default() .route("/page=:number" |c| async { format!("{}", c.parameter("number").unwrap_or("0")) }) .run() .await .unwrap() ``` If you're unfamiliar with [`matchit`], here are a few examples: - `"/owo/:A/:B"` defines `A` and `B`. (`/owo/this_is_A/this_is_B`) - `"/page=:N/filter=:F` defines `N` and `F`. (`/page=20/filter=date`) Keep in mind: some clients cache pages based on their url. You may want to avoid using parameters in routes that update frequently. ## πŸƒ State Fluffer allows you to choose one data object to attach as a generic to [`Client`]. ``` rust use fluffer::App; use std::sync::{Arc, Mutex}; // Alias for Client type Client = fluffer::Client>>; #[derive(Default)] struct State { visitors: u32, } async fn index(c: Client) -> String { let mut state = c.state.lock().unwrap(); state.visitors += 1; format!("Visitors: {}", state.visitors) } #[tokio::main] async fn main() { let state = Arc::new(Mutex::new(State::default())); App::default() .state(state) // <- Must be called first. .route("/", index) .run() .await .unwrap() } ``` ## πŸŒ• Titan [Titan](https://communitywiki.org/wiki/Titan) is a sister protocol for uploading files. You can enable titan on a route by calling [`App::titan`] instead of [`App::route`]. On a titan-enabled route, the `titan` property in [`Client`] may yield a resource. ``` rust use fluffer::{App, Client}; async fn index(c: Client) -> String { if let Some(titan) = c.titan { return format!( "Size: {}\nMime: {}\nContent: {}\nToken: {}", titan.size, titan.mime, std::str::from_utf8(&titan.content).unwrap_or("[not utf8]"), titan.token.unwrap_or(String::from("[no token]")), ); } format!( "Hello, I'm expecting a text/plain gemini request.\n=> titan://{} Click me", c.url.domain().unwrap_or("") ) } #[tokio::main] async fn main() { App::default() .titan("/", index, 20_000_000) // < limits content size to 20mb .run() .await .unwrap() } ``` ## ✨ Features | Name | Description | Default | |---------------|-------------------------------------------------------------------------|---------| | `interactive` | Enable prompt for generating key/cert at runtime. | Yes | | `anyhow` | Enable [`GemBytes`] for `anyhow` (not recommended outside of debugging) | No | | `reqwest` | Enable [`GemBytes`] for `reqwest::Result` and `reqwest::Response` | No |