fluffer

Crates.iofluffer
lib.rsfluffer
version4.0.1
sourcesrc
created_at2023-10-24 02:38:57.763745
updated_at2024-07-04 21:03:18.48082
description🦊 Fluffer is a fun and experimental gemini server framework.
homepagehttps://codeberg.org/catboomer/fluffer
repositoryhttps://codeberg.org/catboomer/fluffer
max_upload_size
id1012036
size127,272
(catb00mer)

documentation

README

🦊 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
Commit count: 0

cargo fmt