-1

I want to create a struct of which has four methods: new(), handle_interaction(), add_shortcut(), async start_server()

The struct should contain a web server, which is started using the async start_server(), a list of available shortcuts (callback-closures), added by the user.

When the server is started, i want to have a single endpoint (/) which then will recieve JSON-data. When the json data is recieved, the server will determine which interaction is incoming, and start the interaction-closure concurrently.

I've tried a lot of different solutions, and i must say that a lot of them incorporate rust-types and concepts that i'm not familiar with just yet.

The code i've landed on right now, which still fails however, is this: (I've highlighted the errors with comments, just so you know what fails)

use axum::{
    routing::{get, post},
    http::StatusCode,
    Json, Router,
};
use std::{net::SocketAddr, future::Future, pin::Pin, collections::HashMap};
use tracing::{debug, info, error};
use futures::{FutureExt, future::BoxFuture};


pub struct Interaction {
    r#type: String,
}

#[derive(Default)]
pub struct App {
    router: Router,
    shortcuts: HashMap<String, Box<dyn Fn(Interaction) -> BoxFuture<'static, ()>>>
}

impl App {
    pub fn new() -> Self {
        Self::default()
    }

    async fn handle_interaction(
        &self,
        Json(interaction): Json<Interaction> 
    ) -> StatusCode {
        let t = interaction.r#type.clone();
        debug!("Got shortcut");
        debug!("{t}");

        let closure = self.shortcuts.get(&t).unwrap();
        tokio::task::spawn(closure(interaction));

        StatusCode::OK
    }

    pub fn add_shortcut<C, Fut>(
        mut self, 
        callback_id: &str,
        fun: C,
    ) -> Self 
    where
        Fut: Future<Output = ()> + Send + 'static,
        C: Fn(Interaction) -> Pin<Box<Fut>>,
    {
        self.shortcuts.insert(callback_id.to_string(),  Box::new(move |interaction| Box::pin(fun(interaction))));
        self
    }

    async fn start(self, addr: SocketAddr) {
        let interaction_handler = move |interaction| async move {
            self.handle_interaction(interaction).await
        };

        // ERROR: `interaction_handler` doesn't match axums route-types... Don't know why this is the case either.
        let router = Router::new()
            .route("/", post(interaction_handler));

        debug!("listening on {}", addr);

        axum::Server::bind(&addr)
            .serve(self.router.into_make_service())
            .await
            .unwrap();
    }
}

async fn shortcut1(i: Interaction) {
    println!("Hello, World!");
}

async fn shortcut2(i: Interaction) {
    println!("Hello, Other World!")
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    // ERROR: `shortcut1` and `shortcut2` doesn't match input type, 
    // and i don't know how to do this properly.
    let app = App::new()
        .add_shortcut("shortcut1", shortcut1)
        .add_shortcut("shortcut2", shortcut2);

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    app.start(addr).await;
}

A lot of this is the result of trail and error, chatgpt and random docs - I'm not familiar with box, pin and alike.

I've also read Mutex or Arc could maybe help solve the issue, but i can't get that to work either.

I hope someone here can help point me in the right direction!

1 Answers1

0

There are three main problems with your code:

  1. Interaction does not implement Deserialize, and thus cannot be used with Json.
  2. The handler does not implement Handler, because self is not cloneable + not Send.
  3. You're using self.router instead of the local router.

Also, you don't need to spawn() tasks - axum will handle that for you, and you don't need to returns StatusCode::OK - just return ().

The code after all fixes:

use axum::{
    body::Body,
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use futures::future::BoxFuture;
use serde::Deserialize;
use std::sync::Arc;
use std::{collections::HashMap, future::Future, net::SocketAddr};

#[derive(Deserialize)]
pub struct Interaction {
    r#type: String,
}

type Shortcuts = HashMap<String, Box<dyn Fn(Interaction) -> BoxFuture<'static, ()> + Send + Sync>>;

#[derive(Default)]
pub struct App {
    shortcuts: Shortcuts,
}

impl App {
    pub fn new() -> Self {
        Self::default()
    }

    async fn handle_interaction(shortcuts: Arc<Shortcuts>, Json(interaction): Json<Interaction>) {
        let closure = shortcuts.get(&interaction.r#type).unwrap();
        closure(interaction);
    }

    pub fn add_shortcut<C, Fut>(mut self, callback_id: &str, fun: C) -> Self
    where
        Fut: Future<Output = ()> + Send + 'static,
        C: Fn(Interaction) -> Fut + Send + Sync + 'static,
    {
        self.shortcuts.insert(
            callback_id.to_string(),
            Box::new(move |interaction| Box::pin(fun(interaction))),
        );
        self
    }

    async fn start(self, addr: SocketAddr) {
        let shortcuts = Arc::new(self.shortcuts);
        let interaction_handler = move |interaction| async move {
            Self::handle_interaction(shortcuts, interaction).await
        };

        let router = Router::new().route("/", post(interaction_handler));

        axum::Server::bind(&addr)
            .serve(router.into_make_service())
            .await
            .unwrap();
    }
}

async fn shortcut1(i: Interaction) {
    println!("Hello, World!");
}

async fn shortcut2(i: Interaction) {
    println!("Hello, Other World!")
}

#[tokio::main]
async fn main() {
    // ERROR: `shortcut1` and `shortcut2` doesn't match input type,
    // and i don't know how to do this properly.
    let app = App::new()
        .add_shortcut("shortcut1", shortcut1)
        .add_shortcut("shortcut2", shortcut2);

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    // app.start(addr).await;
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Thank you so much! At first i thought something was wrong because the functions didn't output anything. Then i noticed the closure wasn't being awaited - It now works as intended! – Mads Ahlquist Jensen Mar 23 '23 at 18:26