Crates.io | fluffer |
lib.rs | fluffer |
version | 4.0.0 |
source | src |
created_at | 2023-10-24 02:38:57.763745 |
updated_at | 2024-02-29 22:47:12.013103 |
description | Fluffer 🦊 is an experimental crate that aims to make writing Gemini apps fun and easy. |
homepage | https://codeberg.org/catboomer/fluffer |
repository | https://codeberg.org/catboomer/fluffer |
max_upload_size | |
id | 1012036 |
size | 134,093 |
Fluffer is an experimental crate that aims to make writing Gemini apps fun and easy.
Other helpful gemini projects:
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 for you to check out. Including a dice roller app.
Here is a basic example of a Fluffer app.
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;
}
The [GemBytes
] trait returns a Gemini byte response, which
is formatted like this:
<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.
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()
}
}
Gemini uses client certificates to facilitate identities in geminispace.
[Client
] provides methods that correspond to common
identity practices in Gemini.
Function | Descripion |
---|---|
[Client::certificate ] |
Get the client cert in the PEM format. |
[Client::fingerprint ] |
Get the client cert's fingerprint (SHA-256). |
[Client::name ] |
Get the client cert's subject_name field. Useful for providing temporary usernames, or just saying hello. |
[Client::verify ] |
Verify that this client's cert is one you're expecting. |
When a gemini client is prompted for input, that input consumes the entire query line.
As such, you should not attach query information to a route that also prompts the user.
Keep this in mind when considering the following options.
To get a user's input to a route, call [Client::input
].
This returns the whole query line percent-decoded.
App::default()
.route("/" |c| async {
c.input().unwrap_or("no input 😥".to_string())
})
.run()
.await
.unwrap()
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 you to do
this by redirecting to a route with a vector of key-value
queries.
You can persist multiple queries by including them in a gemtext link to other pages (or the same page).
Use the method [Client::query
] to look for query values
which correspond to a key.
Parameters are derived from patterns you define on a route's path.
To use a parameter, define it in your route's path string,
and call [Client::parameter
] in the route method.
App::default()
.route("/page=:number" |c| async {
format!("{}", c.parameter("number").unwrap_or("0"))
})
.run()
.await
.unwrap()
Fluffer uses [matchit
]. If you're unfamiliar, 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:
Titan is a sister-protocol to gemini. It allows clients to send data to the server (à la http post). (read more)
This allows titan clients to upload images, videos, and large amounts of text.
To use titan, you must selectively decide which routes will
accept titan data (and how much). You do this by calling
[App::titan
] instead of [App::route
].
Once you've done that, the titan
property on [Client
]
will yield a titan resource if the request was
made with titan.
Here's an example of all that in action.
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()
}
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.
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()
}
Name | Description | Default |
---|---|---|
interactive |
Enables interactive prompt for generating key/cert at runtime. | Yes |
anyhow |
Enables the [GemBytes ] impl for [anyhow ] (not recommended outside of debugging) |
No |
reqwest |
Enables the [GemBytes ] impl for [reqwest::Result ] and [reqwest::Response ] |
No |