Search code examples
rustactix-web

Rust Actix-web simple contact_form handler


Rust with Actix-web, development of a basic contact form with basic field tests and error strings to report back to user if fields are bad.

I've tried closures, different web::Form parameters, and other things, but nothing works.

Here is the error:

error[E0277]: the trait bound `contact_form: Handler<_>` is not satisfied
    |
73  | ...act-form", web::post().to(contact_form)) // Submit contact form
    |                           -- ^^^^^^^^^^^^ the trait `Handler<_>` is not implemented for `contact_form`
    |                           |
    |                           required by a bound introduced by this call
    |
note: required by a bound in `Route::to`
   --> /home/xx/.cargo/registry/src/index.crates.io-6f17d22bba15001f/actix-web-4.4.1/src/route.rs:211:12
    |
209 |     pub fn to<F, Args>(mut self, handler: F) -> Self
    |            -- required by a bound in this associated function
210 |     where
211 |         F: Handler<Args>,
    |            ^^^^^^^^^^^^^ required by this bound in `Route::to`

I'm using Rust Nightly
Cargo.toml:

[package]
name = "server"  
version = "0.1.0"  
edition = "2021"  

[dependencies]   
actix-files = "0.6.2"   
actix-rt = "2.8.0"   
actix-web = { version = "4", features = ["openssl"] }  
openssl = { version = "0.10", features = ["v110"] }  
serde = { version = "1.0", features = ["derive"] }  
serde_json = "1.0"  
rusqlite = "0.30"  
# Local dependencies  
rust_db_lib = { path = "../rust_db_lib" }  
[features]  
server = []  

main.rs:

use actix_files::NamedFile;
use actix_web::{middleware, post, web, App, HttpResponse, HttpServer, Responder};
use rust_db_lib;
use std::path::PathBuf;

#[post("/contact-form")]
async fn contact_form(form: web::Form<rust_db_lib::ContactFormEntry>) -> impl Responder {
    match form.into_inner().is_valid() {
        Ok(_) => HttpResponse::Ok().json("Form submitted successfully"),
        Err(errors) => {
            // Convert the Vec<String> into a JSON serializable format if necessary
            HttpResponse::BadRequest().json(&errors)
        }
    }
}

//... and down a bit lower ...
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Start the HTTP server
    let server = HttpServer::new(|| {
        App::new()
            .wrap(middleware::Logger::default())
            .route("/", web::get().to(index)) // Serve the index.html file
            .route("/contact-form", web::post().to(contact_form)) // Submit contact form
            .service(actix_files::Files::new("/static", "./static")) // Serve other static files under url/static
            .default_service(web::route().to(not_found))
    });
}

Over in rust_db_lib:

use rusqlite::{params, Connection, Error as RusqliteError, Result as RusqliteResult};
use serde::{Deserialize, Serialize};
//... and a few others ...
// The basic ContactFormEntry struct.
#[derive(Debug, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct ContactFormEntry {
    id: i32,
    first_name: Option<String>,
    last_name: Option<String>,
    company: Option<String>,
    email: Option<String>,
    country: Option<String>,
    phone_number: Option<String>,
    message: Option<String>,
}

impl ContactFormEntry {
    pub fn is_valid(&self) -> Result<(), Vec<String>> {
        let mut errors = Vec::new();
        if self.first_name.as_ref().map_or(false, |v| v.len() > 20) {
            errors.push("First name exceeds 20 characters.".into());
        }
        if self.last_name.as_ref().map_or(false, |v| v.len() > 20) {
            errors.push("Last name exceeds 20 characters.".into());
        }
        if self.company.as_ref().map_or(false, |v| v.len() > 50) {
            errors.push("Company exceeds 50 characters.".into());
        }
        if self.email.as_ref().map_or(false, |v| v.len() > 30) {
            errors.push("Email exceeds 30 characters.".into());
        }
        if self.country.as_ref().map_or(false, |v| v.len() > 20) {
            errors.push("Country exceeds 20 characters.".into());
        }
        if self.phone_number.as_ref().map_or(false, |v| v.len() > 20) {
            errors.push("Phone number exceeds 20 characters.".into());
        }
        if self.message.as_ref().map_or(false, |v| v.len() > 500) {
            errors.push("Message exceeds 500 characters.".into());
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

Solution

  • You are already referencing a post request in your app configuration. You don't need the macro at all, just reference the method:

    use actix_web::{middleware, web, App, HttpResponse, HttpServer, Responder};
    use serde::{Deserialize, Serialize};
    
    //... and a few others ...
    // The basic ContactFormEntry struct.
    #[derive(Debug, Serialize, Deserialize)]
    #[allow(dead_code)]
    pub struct ContactFormEntry {
        id: i32,
        first_name: Option<String>,
        last_name: Option<String>,
        company: Option<String>,
        email: Option<String>,
        country: Option<String>,
        phone_number: Option<String>,
        message: Option<String>,
    }
    
    impl ContactFormEntry {
        pub fn is_valid(&self) -> Result<(), Vec<String>> {
            let mut errors = Vec::new();
            if self.first_name.as_ref().map_or(false, |v| v.len() > 20) {
                errors.push("First name exceeds 20 characters.".into());
            }
            if self.last_name.as_ref().map_or(false, |v| v.len() > 20) {
                errors.push("Last name exceeds 20 characters.".into());
            }
            if self.company.as_ref().map_or(false, |v| v.len() > 50) {
                errors.push("Company exceeds 50 characters.".into());
            }
            if self.email.as_ref().map_or(false, |v| v.len() > 30) {
                errors.push("Email exceeds 30 characters.".into());
            }
            if self.country.as_ref().map_or(false, |v| v.len() > 20) {
                errors.push("Country exceeds 20 characters.".into());
            }
            if self.phone_number.as_ref().map_or(false, |v| v.len() > 20) {
                errors.push("Phone number exceeds 20 characters.".into());
            }
            if self.message.as_ref().map_or(false, |v| v.len() > 500) {
                errors.push("Message exceeds 500 characters.".into());
            }
    
            if errors.is_empty() {
                Ok(())
            } else {
                Err(errors)
            }
        }
    }
    
    async fn contact_form(form: web::Form<ContactFormEntry>) -> impl Responder {
        match form.into_inner().is_valid() {
            Ok(_) => HttpResponse::Ok().json("Form submitted successfully"),
            Err(errors) => {
                // Convert the Vec<String> into a JSON serializable format if necessary
                HttpResponse::BadRequest().json(&errors)
            }
        }
    }
    
    //... and down a bit lower ...
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        // Start the HTTP server
        HttpServer::new(|| {
            App::new()
                .wrap(middleware::Logger::default())
                .route("/contact-form", web::post().to(contact_form)) // Submit contact form
                .service(actix_files::Files::new("/static", "./static")) // Serve other static files under url/static
        })
            .bind(("127.0.0.1", 8080))?
            .run()
            .await?;
        Ok(())
    }