DEV Community

Werner Echezuría
Werner Echezuría

Posted on

Migrate to Actix-Web 2.0

I decided it was time for a version upgrade to Actix Web 2.0, so, in order to make it easier I migrated most of the endpoints to GraphQL.

GraphQL

I have to recognize I had my doubts about GraphQL, but after a few steps I took in this project, I recognize why it's growing in popularity. A few benefits:

  • I just need one endpoint and define through functions and methods what resource will be modified or added.
  • I don't need to define every detail, that's what GraphQL is for, the client will define what is the content that will be defined and what will be modified.
  • I don't need to define a standard, it's already established.

In order to make it work, I created one file for mutations and another for queries, like this:

src/graphql/mutation.rs:

use juniper::FieldResult;
use crate::models::Context;
use crate::models::sale::{Sale, NewSale, FullSale};
use crate::models::sale_product::NewSaleProducts;
use crate::models::price::NewPriceProductsToUpdate;
use crate::models::product::{FullProduct, Product, NewProduct};
use crate::models::sale_state::Event;
use crate::models::price::{NewPrice, Price};

pub struct Mutation;

#[juniper::object(
    Context = Context,
)]
impl Mutation {
    fn createSale(
        context: &Context,
        param_new_sale: NewSale,
        param_new_sale_products: NewSaleProducts,
    ) -> FieldResult<FullSale> {
        Sale::create_sale(context, param_new_sale, param_new_sale_products)
    }

    fn approveSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        Sale::set_state(context, sale_id, Event::Approve)
    }

    fn cancelSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform credit note or debit note
        Sale::set_state(context, sale_id, Event::Cancel)
    }

    fn paySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::Pay)
    }

    fn partiallyPaySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::PartiallyPay)
    }

    fn updateSale(
        context: &Context,
        param_sale: NewSale,
        param_sale_products: NewSaleProducts,
    ) -> FieldResult<FullSale> {
        Sale::update_sale(context, param_sale, param_sale_products)
    }

    fn destroySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        Sale::destroy_sale(context, sale_id)
    }

    fn createProduct(
        context: &Context,
        param_new_product: NewProduct,
        param_new_price_products: NewPriceProductsToUpdate,
    ) -> FieldResult<FullProduct> {
        Product::create_product(context, param_new_product, param_new_price_products)
    }

    fn updateProduct(
        context: &Context,
        param_product: NewProduct,
        param_price_products: NewPriceProductsToUpdate,
    ) -> FieldResult<FullProduct> {
        Product::update_product(context, param_product, param_price_products)
    }

    fn destroyProduct(context: &Context, product_id: i32) -> FieldResult<bool> {
        Product::destroy_product(context, product_id)
    }

    fn createPrice(context: &Context, new_price: NewPrice) -> FieldResult<Price> {
        Price::create(context, new_price)
    }

    fn updatePrice(context: &Context, edit_price: NewPrice) -> FieldResult<Price> {
        Price::update(context, edit_price)
    }

    fn destroyPrice(context: &Context, price_id: i32) -> FieldResult<bool> {
        Price::destroy(context, price_id)
    }
}

src/graphql/query.rs:

use juniper::FieldResult;
use crate::models::Context;
use crate::models::sale::{Sale, NewSale, ListSale, FullSale};
use crate::models::product::{Product, ListProduct, FullProduct};
use crate::models::price::{PriceList, Price};

pub struct Query;

#[juniper::object(
    Context = Context,
)]
impl Query {
    fn listSale(context: &Context, search: Option<NewSale>, limit: i32) -> FieldResult<ListSale> {
        Sale::list_sale(context, search, limit)
    }

    fn sale(context: &Context, sale_id: i32) -> FieldResult<FullSale> {
        Sale::sale(context, sale_id)
    }

    fn listProduct(context: &Context, search: String, limit: i32, rank: f64) -> FieldResult<ListProduct> {
        Product::list_product(context, search, limit, rank)
    }

    fn product(context: &Context, product_id: i32) -> FieldResult<FullProduct> {
        Product::product(context, product_id)
    }

    fn ListPrice(context: &Context) -> FieldResult<PriceList> {
        PriceList::list(context)
    }

    fn price(context: &Context, price_id: i32) -> FieldResult<Price> {
        Price::find(context, price_id)
    }

}

And another file for the schema:

src/graphql/schema.rs:

use crate::graphql::query::Query;
use crate::graphql::mutation::Mutation;

pub type Schema = juniper::RootNode<'static, Query, Mutation>;

pub fn create_schema() -> Schema {
    Schema::new(Query {}, Mutation {})
}

Actix Web 2.0

I think I like the new way to define endpoints in ActixWeb, it looks like Rocket, especially because of routes annotations. The big change was defined in src/graphql/mod.rs.

src/graphql/mod.rs:

pub mod query;
pub mod mutation;
pub mod schema;

use std::sync::Arc;
use actix_web::{web, Error, HttpResponse, post, get};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
use schema::Schema;

use crate::models::create_context;
use crate::handlers::LoggedUser;
use crate::db_connection::PgPool;
use crate::serde::ser::Error as SerdeError;

#[get("/graphiql")]
pub async fn graphiql() -> HttpResponse {
    let html = graphiql_source("http://127.0.0.1:8080/graphql");
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

#[post("/graphql")]
pub async fn graphql(
    st: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
    user: LoggedUser,
    pool: web::Data<PgPool>
) -> Result<HttpResponse, Error> {
    let user =
        web::block(move || {
            let pg_pool = pool
                .get()
                .map_err(|e| {
                    serde_json::Error::custom(e)
                })?;

            let ctx = create_context(user.id, pg_pool);

            let res = data.execute(&st, &ctx);
            Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
        })
        .await?;
    Ok(HttpResponse::Ok()
        .content_type("application/json")
        .body(user))
}

There are other changes, more difficult that implies altering the integration tests, the more important one was the test server.

tests/sale_test.rs:

        let srv = RefCell::new(test_server(move || {
            HttpService::build()
                .h1(map_config(
                    App::new()
                        .wrap(
                            IdentityService::new(
                                CookieIdentityPolicy::new(dotenv!("SECRET_KEY").as_bytes())
                                    .domain("localhost")
                                    .name("mystorejwt")
                                    .path("/")
                                    .max_age(Duration::days(1).num_seconds())
                                    .secure(false)
                            )
                        )
                        .wrap(
                            Cors::new()
                                .allowed_origin("localhost")
                                .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"])
                                .allowed_headers(vec![header::AUTHORIZATION,
                                                    header::CONTENT_TYPE,
                                                    header::ACCEPT,
                                                    csrf_token_header.clone()])
                                .expose_headers(vec![csrf_token_header.clone()])
                                .max_age(3600)
                                .finish()
                        )
                        .data(
                            CsrfTokenGenerator::new(
                                dotenv!("CSRF_TOKEN_KEY").as_bytes().to_vec(),
                                Duration::hours(1)
                            )
                        )
                        .data(establish_connection())
                        .data(schema.clone())
                        .service(graphql)
                        .service(graphiql)
                        .service(::mystore_lib::handlers::authentication::login)
                        .service(::mystore_lib::handlers::authentication::logout)
                    ,  |_| AppConfig::default(),
                ))
                .tcp()
            }
        ));

If you want to take a look at the source code, you can do it through this link

PS. I'm openly looking for a new job (remote), Experience in Ruby on Rails and Rust enthusiast, :).

Top comments (1)

Collapse
 
sunsoftio profile image
Sunsoft-io

Hi Werner,

do you want to rewrite the UNDollar.org software with Rust and Actix?

Thanks...Yours...Willi, UNDollar.org

willi@rauffer.org