fluffer

Crates.iofluffer
lib.rsfluffer
version4.0.0
sourcesrc
created_at2023-10-24 02:38:57.763745
updated_at2024-02-29 22:47:12.013103
descriptionFluffer 🦊 is an experimental crate that aims to make writing Gemini apps fun and easy.
homepagehttps://codeberg.org/catboomer/fluffer
repositoryhttps://codeberg.org/catboomer/fluffer
max_upload_size
id1012036
size134,093
(catb00mer)

documentation

README

🦊 Fluffer

Fluffer is an experimental crate that aims to make writing Gemini apps fun and easy.

Other helpful gemini projects:

🗼 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 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;
}

💎 GemBytes

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()
    }
}

📜 Client

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.

🥴 Input, queries, and parameters

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.

Input

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()

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 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

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:

  • Some gemini clients cache pages based on their route. So, you may not want to use parameters for routes that update frequently.
  • 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.

🌕 Titan

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()
}

🏃 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.

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()
}

✨ Features

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

📚 Helpful Resources

📋 Todo

  • Async for route functions
  • Switch to openssl
  • Add peer certificate to client
  • Spawn threads
  • App data
  • Titan support
  • Add more options to certificate generation
  • Tests..?
Commit count: 0

cargo fmt