# 🦊 Fluffer
Fluffer is an *experimental* crate that aims to make writing
Gemini apps fun and easy.
I'm also working on [`trotter`], a crate for gemini clients.
## 🗼 Design
Similar to Axum, Fluffer routes are generic functions that
can return anything that implements the [`GemBytes`] trait.
There are some helpful implementations out of the box, so
please consult [`GemBytes`] and [`Fluff`] while you
experiment.
Also, this crate has a lot of [examples](https://github.com/catb00mer/fluffer/tree/main/examples)
for you to check out. Including a dice roller app.
Here is a basic example of a Fluffer app.
``` 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 returns a Gemini byte response, which
is formatted like this:
``` text
<STATUS><SPACE><META>\r\n<CONTENT>
```
*Note: you must include the `<SPACE>` character, even if
`<META>` is blank.*
To implement [`GemBytes`] on a type is to decide which
Gemini response is appropriate for it.
For example: it is sensible to represent some mime-ambiguous
data as a successful Gemtext response so it can be read in a
client.
``` rust
use fluffer::{GemBytes, async_trait};
struct Profile {
name: String,
bio: String,
}
#[async_trait]
impl GemBytes for Profile {
async fn gem_bytes(&self) -> Vec<u8> {
format!("20 text/gemini\r\n# {},\n{}", self.name, self.bio).into_bytes()
}
}
```
## 📜 Certificates
### Server
Fluffer looks for two files at runtime (`./key.pem` and
`./cert.pem` by default). If they can't be located, a prompt
appears to generate them interactively.
### Client identity
Gemini uses client certificates to facilitate identities.
[`Client`] exposes functions with the `ident_` prefix,
which correspond to common identity practices in Gemini.
* [`Client::ident_get`] gets the client's certificate.
* [`Client::ident_verify`] returns true if the current
client's certificate matches one you pass.
* [`Client::ident_name`] returns the first entry in the
certificate's `subject_name` field. This can be used to
provide temporary usernames, or just to say hello.
* [`Client::ident_expired`] returns true if there's no
certificate, or if the client's certificate is
invalid/expired.
## 🥴 Parameters and Input
Queries in Gemini aren't one-to-one with HTTP.
Gemini clients tend to consider the entire query line to be
a user's input. As such, they discard any queries you may
have included in a link.
In other words, `/?p=20` often becomes `/?user%20input`.
This is a problem for apps like search engines, which may
want to include filters and pagination in each request
alongside a user's search query.
To simplify the problem, Fluffer encourages you to use the
whole query as input, and [`matchit`]'s route parameters for
everything else.
#### Input
To get a user's input to a route, call [`Client::input`].
This returns the whole query line percent-decoded.
``` rust
App::default()
.route("/" |c| async {
c.input().unwrap_or("no input 😥".to_string())
})
.run()
.await
.unwrap()
```
#### Parameters
To access a parameter, *you **must** declare it first* in
the path string. Referencing an undefined parameter causes
the connection's thread to panic.
``` rust
App::default()
.route("/page=:number" |c| async {
format!("{}", c.parameter("number"))
})
.run()
.await
.unwrap()
```
If you're unfamiliar with matchit patterns, here's a couple
of examples:
- `"/owo/:a/:b"` defines parameters `a` and `b`, e.g: `/owo/thisisa/thisisb`
- `"/page=:n/filter=:f` defines the parameter `n`, and `f`
following a prefix, e.g: `/page=20/filter=date`.
***Things to keep in mind:***
- Every parameter **must** be included in your url for the
route to be found.
- Be careful where you define your parameters. It's possible
to consume requests intended for a different route.
- It's more flexible to represent complex expressions as a
single parameter, which you parse manually inside the
route function.
## 🏃 App State
Currently, Fluffer allows you to add one piece of state that
gets attached as a generic to [`Client`].
This means you'll need to reflect the app's state in every
reference of [`Client`], so I recommend using a type alias.
``` rust
use fluffer::App;
use std::sync::{Arc, Mutex};
// Type alias for Client<State> **highly recommended**
type Client = fluffer::Client<Arc<Mutex<State>>>;
#[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()
}
```
## 📚 Helpful Resources
* [Gemini spec](https://gemini.circumlunar.space/docs/specification.gmi)
* [Examples](https://github.com/catb00mer/fluffer/tree/main/examples)
## 📋 Todo
* [X] Async for route functions
* [X] Switch to openssl
* [X] Add peer certificate to client
* [X] Spawn threads
* [X] App data
* [ ] Titan support