Skip to content
Permalink
Browse files
locations-rs: rewrite to actix web framework
Tide was out to be promising, but immature and brought the async-std framework,
which has arguable less mature ecosystem. The worst problem was the web client
surf, which seemed to be inefficient.
  • Loading branch information
strohel committed Mar 31, 2020
1 parent 737ad4c commit 76a96f0
Show file tree
Hide file tree
Showing 8 changed files with 646 additions and 786 deletions.
1,255 Cargo.lock

Large diffs are not rendered by default.

@@ -5,18 +5,12 @@ authors = ["Matěj Laitl <matej@laitl.cz>"]
edition = "2018"

[dependencies]
elasticsearch = { git = "https://github.com/strohel/elasticsearch-rs.git", branch = "port-from-reqwest-to-surf" }
async-std = { version = "1.5", features = ["attributes"]}
actix-rt = "1.0"
actix-web = "2.0"
elasticsearch = "7.6.1-alpha.1"
env_logger = "0.7"
http = "0.1"
log = "0.4"
pretty_env_logger = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tide = "0.6"
thiserror = "1.0"

[patch.crates-io]
tide = { git = "https://github.com/strohel/tide.git", branch = "intoresponse-for-result" }
# Use newer master version, due to https://github.com/http-rs/surf/issues/143#issuecomment-602205453
http-client = { git = "https://github.com/http-rs/http-client.git", rev = "cc5ee280bd65e1522ff15b19e1575c0530e0e695" }
@@ -1,6 +1,6 @@
# locations-rs (Locations Service in Rust)

Little proof-of-concept webservice in Rust, using experimental [Tide](https://github.com/http-rs/tide) web framework.
Little proof-of-concept webservice in Rust, using [Actix](https://actix.rs/) web framework.

## Build, Build Documentation, Run

@@ -0,0 +1,35 @@
//! Module that implements our JSON error handling for actix-web.

use actix_web::{
body::ResponseBody,
dev::{Body, ServiceResponse},
middleware::errhandlers::ErrorHandlerResponse,
HttpResponse,
};
use serde::Serialize;

/// Process errors from all handlers including the default handler, convert them to JSON.
pub(crate) fn json_error(
mut sres: ServiceResponse<Body>,
) -> Result<ErrorHandlerResponse<Body>, actix_web::Error> {
let response = sres.response_mut();
let body = match response.take_body() {
ResponseBody::Body(body) | ResponseBody::Other(body) => body,
};

// Use existing body as message, otherwise just pretty-printed HTTP code.
let message = match body {
Body::None | Body::Empty => response.status().to_string(),
Body::Bytes(bytes) => String::from_utf8(bytes.to_vec()).expect("valid UTF-8 we've encoded"),
Body::Message(_) => panic!("did not expect Body::Message()"),
};

#[derive(Serialize)]
struct ErrorPayload {
message: String,
}

let body = serde_json::to_string(&ErrorPayload { message })?;
*response = HttpResponse::build(response.status()).content_type("application/json").body(body);
Ok(ErrorHandlerResponse::Response(sres))
}
@@ -1,15 +1,19 @@
//! Handlers for /city/* endpoints.

use crate::{
response::{ErrorResponse::BadRequest, JsonResponse, JsonResult},
response::{ErrorResponse::BadRequest, JsonResult},
services::locations_repo::LocationsElasticRepository,
Request,
AppState,
};
use actix_web::{
get,
web::{Data, Json, Query},
};
use serde::{Deserialize, Serialize};

/// Query for the `/city/v1/get` endpoint.
#[derive(Deserialize)]
struct CityQuery {
pub(crate) struct CityQuery {
id: u64,
language: String,
}
@@ -26,10 +30,9 @@ pub(crate) struct CityResponse {
}

/// The `/city/v1/get` endpoint. Request: [CityQuery], response: [CityResponse].
pub(crate) async fn get(req: Request) -> JsonResult<CityResponse> {
let query: CityQuery = req.query()?;

let locations_es_repo = LocationsElasticRepository(req.state());
#[get("/city/v1/get")]
pub(crate) async fn get(query: Query<CityQuery>, app: Data<AppState>) -> JsonResult<CityResponse> {
let locations_es_repo = LocationsElasticRepository(app.get_ref());
let es_city = locations_es_repo.get_city(query.id).await?;
let es_region = locations_es_repo.get_region(es_city.regionId).await?;

@@ -44,5 +47,5 @@ pub(crate) async fn get(req: Request) -> JsonResult<CityResponse> {
name: name.to_string(),
regionName: region_name.to_string(),
};
Ok(JsonResponse(city))
Ok(Json(city))
}

This file was deleted.

@@ -1,4 +1,4 @@
//! Little proof-of-concept webservice in Rust, using experimental [tide] web framework.
//! Little proof-of-concept webservice in Rust, using Actix web framework.

// Make writing "unsafe" in code a compilation error. We should not need unsafe at all.
#![forbid(unsafe_code)]
@@ -12,14 +12,20 @@
#![warn(clippy::all)]

use crate::stateful::elasticsearch::WithElasticsearch;
use actix_web::{
http::StatusCode,
middleware::{errhandlers::ErrorHandlers, Logger},
web::Data,
App, HttpServer,
};
use elasticsearch::Elasticsearch;
use env_logger::DEFAULT_FILTER_ENV;
use std::{env, io};

mod error;
/// Module for endpoint handlers (also known as controllers).
mod handlers {
pub(crate) mod city;
pub(crate) mod fallback;
}
mod response;
/// Module for stateless services (that may depend on stateful ones from [stateful] module).
@@ -31,26 +37,30 @@ mod stateful {
pub(crate) mod elasticsearch;
}

/// Convenience type alias to be used by handlers.
type Request = tide::Request<AppState>;

#[async_std::main]
#[actix_rt::main]
async fn main() -> io::Result<()> {
// Set default log level to info and then init logging.
if env::var(DEFAULT_FILTER_ENV).is_err() {
env::set_var(DEFAULT_FILTER_ENV, "info");
}
pretty_env_logger::init_timed();

let mut app = tide::with_state(AppState::new().await);
app.middleware(tide::middleware::RequestLogger::new());

app.at("/city/v1/get").get(handlers::city::get);

app.at("/").all(handlers::fallback::not_found);
app.at("/*").all(handlers::fallback::not_found);

app.listen("0.0.0.0:8080").await
let app_state_data = Data::new(AppState::new().await);
HttpServer::new(move || {
App::new()
.app_data(app_state_data.clone())
.wrap(
ErrorHandlers::new()
.handler(StatusCode::BAD_REQUEST, error::json_error)
.handler(StatusCode::NOT_FOUND, error::json_error)
.handler(StatusCode::INTERNAL_SERVER_ERROR, error::json_error),
)
.wrap(Logger::default())
.service(handlers::city::get)
})
.bind("0.0.0.0:8080")?
.run()
.await
}

struct AppState {
@@ -1,24 +1,9 @@
//! OK and error response types to be used by endpoints.

use http::status::StatusCode;
use log::{log, Level};
use serde::Serialize;
use tide::{IntoResponse, QueryParseError, Response};
use actix_web::{http::StatusCode, web::Json, ResponseError};

/// Result type to be used by endpoints. Either OK [JsonResponse] or error [ErrorResponse].
pub(crate) type JsonResult<T> = Result<JsonResponse<T>, ErrorResponse>;

/// Wrapper for OK endpoint responses.
pub(crate) struct JsonResponse<T: Serialize + Send>(pub(crate) T);

/// Make Tide framework understand our OK responses.
impl<T: Serialize + Send> IntoResponse for JsonResponse<T> {
fn into_response(self) -> Response {
Response::new(200)
.body_json(&self.0)
.unwrap_or_else(|e| ErrorResponse::from(e).into_response())
}
}
/// Result type to be used by endpoints. Either OK [Json] or error [ErrorResponse].
pub(crate) type JsonResult<T> = Result<Json<T>, ErrorResponse>;

/// Wrapper for error endpoint responses.
#[derive(Debug, thiserror::Error)]
@@ -31,43 +16,15 @@ pub(crate) enum ErrorResponse {
InternalServerError(String),
}

impl ErrorResponse {
fn status(&self) -> StatusCode {
/// Make actix-web understand our error responses.
impl ResponseError for ErrorResponse {
fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}

fn severity(&self) -> Level {
match self {
Self::BadRequest(_) | Self::NotFound(_) => Level::Warn,
Self::InternalServerError(_) => Level::Error,
}
}
}

/// Make Tide framework understand our error responses.
impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
#[derive(Serialize)]
struct ErrorPayload {
message: String,
}

let status = self.status();
let message = self.to_string();
log!(self.severity(), "Responding with HTTP {}: {}", status, message);
JsonResponse(ErrorPayload { message }).with_status(status).into_response()
}
}

/// Convert Tide query parse error into bad request.
impl From<QueryParseError> for ErrorResponse {
fn from(err: QueryParseError) -> Self {
Self::BadRequest(err.to_string())
}
}

/// Convert Elasticsearch errors into internal server errors.
@@ -76,10 +33,3 @@ impl From<elasticsearch::Error> for ErrorResponse {
Self::InternalServerError(format!("Elasticsearch error: {}", err))
}
}

/// Convert Serde (serialization, deserialization) errors into internal server errors.
impl From<serde_json::Error> for ErrorResponse {
fn from(err: serde_json::Error) -> Self {
Self::InternalServerError(format!("Serde JSON error: {}", err))
}
}

0 comments on commit 76a96f0

Please sign in to comment.