#gemini #routes #server-framework #traits #experimental #fun #gem-bytes

fluffer

🦊 Fluffer is a fun and experimental gemini server framework

18 releases (4 major breaking)

2023.10.27 Oct 27, 2023
4.0.1 Jul 4, 2024
4.0.0 Feb 29, 2024
3.0.0 Feb 28, 2024
0.9.1 Nov 23, 2023

#2 in #fun

Download history 3/week @ 2024-03-13 47/week @ 2024-03-27 95/week @ 2024-04-03

1,059 downloads per month
Used in dovetail

GPL-3.0-only

42KB
806 lines

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

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:

<STATUS><SPACE><META>\r\n<CONTENT>

Remember you must include the <SPACE> characterβ€”even if <META> 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.

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

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.

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.

use fluffer::App;
use std::sync::{Arc, Mutex};

// Alias for Client<State>
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()
}

πŸŒ• Titan

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.

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

Dependencies

~9–27MB
~398K SLoC