3

I found in the docs an example of how to create global state, protected by Mutex, shared among processing threads that is made available to all your route handlers. Perfect! However, I prefer to use attributes attached to my functions to wire up my route handlers. I do not know the syntax (if permitted) to use attributed functions and also pass in the global state.

Here is the example from the actix-web docs, from https://docs.rs/actix-web/1.0.2/actix_web/web/struct.Data.html

use std::sync::Mutex;
use actix_web::{web, App};

struct MyData {
    counter: usize,
}

/// Use `Data<T>` extractor to access data in handler.
fn index(data: web::Data<Mutex<MyData>>) {
    let mut data = data.lock().unwrap();
    data.counter += 1;
}

fn main() {
    let data = web::Data::new(Mutex::new(MyData{ counter: 0 }));

    let app = App::new()
        // Store `MyData` in application storage.
        .register_data(data.clone())
        .service(
            web::resource("/index.html").route(
                web::get().to(index)));
}

Notice how the route handler named index is being passed the web::Data.

Now here are some snippets of my code.

use actix_web::{get, App, HttpResponse, HttpServer, Responder};
pub mod request;
pub mod routes;

const SERVICE_NAME : &str = "Shy Rules Engine";
const SERVICE_VERSION : &str  = "0.1";

#[get("/")]
fn index() -> impl Responder {
    HttpResponse::Ok().body(format!("{} version {}", SERVICE_NAME, SERVICE_VERSION))
}

mod expression_execute {

  #[post("/expression/execute")]
  fn route(req: web::Json<ExpressionExecuteRequest>) -> HttpResponse {

    // ... lots of code omitted ...

    if response.has_error() {
        HttpResponse::Ok().json(response)
    }
    else {
        HttpResponse::BadRequest().json(response)
    }
  }

}

pub fn shy_service(ip : &str, port : &str) {
    HttpServer::new(|| {
        App::new()
            .service(index)
            .service(expression_execute::route)
    })
    .bind(format!("{}:{}", ip, port))
    .unwrap()
    .run()
    .unwrap();
}

Notice how I am calling method App::service to wire up my route handlers.

Also notice how my route handler does not receive global state (because I have not yet added it to my app). If I used a similar pattern as the docs using register_data to create global App data, what changes do I make to my method signature, the get and post attributes and anything else so that I can pass that global state to the handler?

Or is it not possible using get and post attributes to gain access to global state?

Paul Chernoch
  • 5,275
  • 3
  • 52
  • 73
  • What prevents you from using the same `Data` extractor in the second example as in the first one? – edwardw Oct 17 '19 at 16:47
  • Nothing prevents me from doing it using the fluent interface instead of adding attributes to methods. However, my colleagues and I are familiar using Express and similar tools in node.js and C# .Net core that use attributes, and that style is more natural and easier to read (in my opinion). It is self documenting, informing the reader that the function is a route handler and telling them what route it handles concisely. – Paul Chernoch Oct 17 '19 at 17:47
  • No, I meant using `Data` extractor and `get` attribute together. See my answer. – edwardw Oct 18 '19 at 07:27

1 Answers1

5

The two cases you listed really don't have much difference:

//# actix-web = "1.0.8"
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use std::sync::Mutex;

const SERVICE_NAME : &str = "Shy Rules Engine";
const SERVICE_VERSION : &str  = "0.1";

struct MyData {
    counter: usize,
}

#[get("/")]
fn index(data: web::Data<Mutex<MyData>>) -> impl Responder {
    let mut data = data.lock().unwrap();
    data.counter += 1;
    println!("Endpoint visited: {}", data.counter);
    HttpResponse::Ok().body(format!("{} version {}", SERVICE_NAME, SERVICE_VERSION))
}

pub fn shy_service(ip : &str, port : &str) {
    let data = web::Data::new(Mutex::new(MyData{ counter: 0 }));

    HttpServer::new(move || {
        App::new()
            .register_data(data.clone())
            .service(index)
    })
    .bind(format!("{}:{}", ip, port))
    .unwrap()
    .run()
    .unwrap();
}

fn main() {
    shy_service("127.0.0.1", "8080");
}

You can verify that it works by simply curl the http endpoint. For multiple extractors, you'll have to use tuple:

    #[post("/expression/execute")]
    fn route((req, data): (web::Json<ExpressionExecuteRequest>, web::Data<Mutex<MyData>>)) -> HttpResponse {
        unimplemented!()
    }
edwardw
  • 12,652
  • 3
  • 40
  • 51
  • Thank you! I have one confusion. I know that Rust does not have method overloading. How is it that in my original code, I pass a function to the App::service method that has one method signature, while you have passed it a function with a different signature, adding in the app data? – Paul Chernoch Oct 18 '19 at 11:55
  • @PaulChernoch `get` attribute is just a convenient way to say `web::resource(...).route(web::get().to(...))`. [`actix_web::Resource.to`](https://docs.rs/actix-web/1.0.8/actix_web/struct.Resource.html#method.to) takes a handler of type trait `Factory`. Dig into `actix-web` source code, one can see the trait `Factory` is implemented for [`Fn()`](https://github.com/actix/actix-web/blob/master/src/handler.rs#L22) and also [`Fn(tuple)`](https://github.com/actix/actix-web/blob/master/src/handler.rs#L401) with no more than 10 elements. That should answer your Q. – edwardw Oct 18 '19 at 13:00
  • Thanks. I tried looking in the source, but didn't know where to look! – Paul Chernoch Oct 18 '19 at 14:31
  • 2
    Worth noting that in [version 2.0.0](https://github.com/easyaspi314/actix-web/blob/master/MIGRATION.md) in actix-web `register_data` was renamed to `app_data`. – cascading-jox Apr 09 '22 at 18:21