j-n-f / anodyne

An opinionated set of tools for building applications (mostly with `axum`)
MIT License
0 stars 0 forks source link

anodyne

An experimental framework for more easily building full-stack applications with axum.

Motivation

While building applications with axum + HTMX, the same code was being written over and over again. This framework is an attempt to reduce boilerplate by deriving it from structure definitions. This is different from something like loco where you would use a CLI scaffolding tool to generate both structures and code. With anodyne you start from data definitions and the code is generated for you.

Many frameworks take a highly-modular approach which requires writing a lot of glue code, and makes it more difficult to provide tight integration between concerns.

As an example: there are several excellent validation libraries for rust/axum, but this still requires you to manually run validation in every single handler.

For a simple backend API this is fine, but for building full-stack applications it becomes tedious and error-prone.

Objectives

Current Progress

Implementation is changing rapidly as details are worked out, but here's what's implemented so far:

As an example:

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/my-form", get(show_form).post(handle_form))
        // layer in anodyne middlewares (these will eventually be merged into a single service)
        // TODO: update this once middleware has been finalized
        ;

    let listener = tokio::net::TcpListener::bind("127.0.0.1:4000").await.unwrap();
    axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
        .await
        .unwrap();
}

#[derive(Deserialize, Serialize, Form, Default, Debug)]
pub struct MyForm {
    // HTML input name will be `exact_length`, but label will be as specified in attribute
    #[form(
        label="Enter eight characters"
        // Validation will ensure length of value is exactly 8 characters
        len=8,
        // Validation will ensure input matches this regex
        regex="^[A-Za-z0-9]+$",
        // Validation will show this error if regex doesn't match
        regex_description="must be only alphanumeric characters"
    )]
    exact_length: String
    #[form(
        // You can also use range specifiers for length validation
        len=8..=32
    )]
    password: String,
    #[form(
        // You can reference a field in this struct which this field must match
        matches=password
        // The label for this field will be "Confirm Password" (automatically converted from
        // snake-case)
    )]
    confirm_password: String,
}

async fn show_form() -> AnodyneResult<MyForm> {
    Ok(
        AnodyneResponse::from_data(
            // Here we specify that the form is initialized as empty, but you could provide some
            // pre-filled values if you wanted by passing a filled-in struct.
            MyForm::default()
        )
            .as_post() // Method for this form will be POST
            .with_route("/my-form") // Where data will be posted
    )
}

// This function could return AnodyneResult, or you can freely mix/match axum responses
async fn handle_form(FormData(form): FormData<MyForm>) -> impl IntoResponse {
    // You can access data in `form` and it's already valid by this point. If the user caused
    // a validation error they've already been redirected to the form to correct those errors.
}

Future Considerations