Crate fluffer

Source
Expand description

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

NameDescriptionDefault
interactiveEnable prompt for generating key/cert at runtime.Yes
anyhowEnable GemBytes for anyhow (not recommended outside of debugging)No
reqwestEnable GemBytes for reqwest::Result and reqwest::ResponseNo

Structs§

App
🖥️ A Fluffer App
Client
Information about a client’s request.

Enums§

AppErr
Fluff
🐰 A general-purpose implementation of GemBytes.
Status
Re-export from trotter. Enum for representing gemini status codes.

Traits§

GemBytes
💎 A trait implemented on types that can be returned as a gemini response.
GemCall

Attribute Macros§

async_trait
Procedural macro used when implementing GemBytes.