Skip to content

Commit 76a96f0

Browse files
committed
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.
1 parent 737ad4c commit 76a96f0

File tree

8 files changed

+646
-786
lines changed

8 files changed

+646
-786
lines changed

Cargo.lock

Lines changed: 565 additions & 690 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,12 @@ authors = ["Matěj Laitl <matej@laitl.cz>"]
55
edition = "2018"
66

77
[dependencies]
8-
elasticsearch = { git = "https://github.com/strohel/elasticsearch-rs.git", branch = "port-from-reqwest-to-surf" }
9-
async-std = { version = "1.5", features = ["attributes"]}
8+
actix-rt = "1.0"
9+
actix-web = "2.0"
10+
elasticsearch = "7.6.1-alpha.1"
1011
env_logger = "0.7"
11-
http = "0.1"
1212
log = "0.4"
1313
pretty_env_logger = "0.4"
1414
serde = { version = "1.0", features = ["derive"] }
1515
serde_json = "1.0"
16-
tide = "0.6"
1716
thiserror = "1.0"
18-
19-
[patch.crates-io]
20-
tide = { git = "https://github.com/strohel/tide.git", branch = "intoresponse-for-result" }
21-
# Use newer master version, due to https://github.com/http-rs/surf/issues/143#issuecomment-602205453
22-
http-client = { git = "https://github.com/http-rs/http-client.git", rev = "cc5ee280bd65e1522ff15b19e1575c0530e0e695" }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# locations-rs (Locations Service in Rust)
22

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

55
## Build, Build Documentation, Run
66

src/error.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//! Module that implements our JSON error handling for actix-web.
2+
3+
use actix_web::{
4+
body::ResponseBody,
5+
dev::{Body, ServiceResponse},
6+
middleware::errhandlers::ErrorHandlerResponse,
7+
HttpResponse,
8+
};
9+
use serde::Serialize;
10+
11+
/// Process errors from all handlers including the default handler, convert them to JSON.
12+
pub(crate) fn json_error(
13+
mut sres: ServiceResponse<Body>,
14+
) -> Result<ErrorHandlerResponse<Body>, actix_web::Error> {
15+
let response = sres.response_mut();
16+
let body = match response.take_body() {
17+
ResponseBody::Body(body) | ResponseBody::Other(body) => body,
18+
};
19+
20+
// Use existing body as message, otherwise just pretty-printed HTTP code.
21+
let message = match body {
22+
Body::None | Body::Empty => response.status().to_string(),
23+
Body::Bytes(bytes) => String::from_utf8(bytes.to_vec()).expect("valid UTF-8 we've encoded"),
24+
Body::Message(_) => panic!("did not expect Body::Message()"),
25+
};
26+
27+
#[derive(Serialize)]
28+
struct ErrorPayload {
29+
message: String,
30+
}
31+
32+
let body = serde_json::to_string(&ErrorPayload { message })?;
33+
*response = HttpResponse::build(response.status()).content_type("application/json").body(body);
34+
Ok(ErrorHandlerResponse::Response(sres))
35+
}

src/handlers/city.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
//! Handlers for /city/* endpoints.
22
33
use crate::{
4-
response::{ErrorResponse::BadRequest, JsonResponse, JsonResult},
4+
response::{ErrorResponse::BadRequest, JsonResult},
55
services::locations_repo::LocationsElasticRepository,
6-
Request,
6+
AppState,
7+
};
8+
use actix_web::{
9+
get,
10+
web::{Data, Json, Query},
711
};
812
use serde::{Deserialize, Serialize};
913

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

2832
/// The `/city/v1/get` endpoint. Request: [CityQuery], response: [CityResponse].
29-
pub(crate) async fn get(req: Request) -> JsonResult<CityResponse> {
30-
let query: CityQuery = req.query()?;
31-
32-
let locations_es_repo = LocationsElasticRepository(req.state());
33+
#[get("/city/v1/get")]
34+
pub(crate) async fn get(query: Query<CityQuery>, app: Data<AppState>) -> JsonResult<CityResponse> {
35+
let locations_es_repo = LocationsElasticRepository(app.get_ref());
3336
let es_city = locations_es_repo.get_city(query.id).await?;
3437
let es_region = locations_es_repo.get_region(es_city.regionId).await?;
3538

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

src/handlers/fallback.rs

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Little proof-of-concept webservice in Rust, using experimental [tide] web framework.
1+
//! Little proof-of-concept webservice in Rust, using Actix web framework.
22
33
// Make writing "unsafe" in code a compilation error. We should not need unsafe at all.
44
#![forbid(unsafe_code)]
@@ -12,14 +12,20 @@
1212
#![warn(clippy::all)]
1313

1414
use crate::stateful::elasticsearch::WithElasticsearch;
15+
use actix_web::{
16+
http::StatusCode,
17+
middleware::{errhandlers::ErrorHandlers, Logger},
18+
web::Data,
19+
App, HttpServer,
20+
};
1521
use elasticsearch::Elasticsearch;
1622
use env_logger::DEFAULT_FILTER_ENV;
1723
use std::{env, io};
1824

25+
mod error;
1926
/// Module for endpoint handlers (also known as controllers).
2027
mod handlers {
2128
pub(crate) mod city;
22-
pub(crate) mod fallback;
2329
}
2430
mod response;
2531
/// Module for stateless services (that may depend on stateful ones from [stateful] module).
@@ -31,26 +37,30 @@ mod stateful {
3137
pub(crate) mod elasticsearch;
3238
}
3339

34-
/// Convenience type alias to be used by handlers.
35-
type Request = tide::Request<AppState>;
36-
37-
#[async_std::main]
40+
#[actix_rt::main]
3841
async fn main() -> io::Result<()> {
3942
// Set default log level to info and then init logging.
4043
if env::var(DEFAULT_FILTER_ENV).is_err() {
4144
env::set_var(DEFAULT_FILTER_ENV, "info");
4245
}
4346
pretty_env_logger::init_timed();
4447

45-
let mut app = tide::with_state(AppState::new().await);
46-
app.middleware(tide::middleware::RequestLogger::new());
47-
48-
app.at("/city/v1/get").get(handlers::city::get);
49-
50-
app.at("/").all(handlers::fallback::not_found);
51-
app.at("/*").all(handlers::fallback::not_found);
52-
53-
app.listen("0.0.0.0:8080").await
48+
let app_state_data = Data::new(AppState::new().await);
49+
HttpServer::new(move || {
50+
App::new()
51+
.app_data(app_state_data.clone())
52+
.wrap(
53+
ErrorHandlers::new()
54+
.handler(StatusCode::BAD_REQUEST, error::json_error)
55+
.handler(StatusCode::NOT_FOUND, error::json_error)
56+
.handler(StatusCode::INTERNAL_SERVER_ERROR, error::json_error),
57+
)
58+
.wrap(Logger::default())
59+
.service(handlers::city::get)
60+
})
61+
.bind("0.0.0.0:8080")?
62+
.run()
63+
.await
5464
}
5565

5666
struct AppState {

src/response.rs

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
//! OK and error response types to be used by endpoints.
22
3-
use http::status::StatusCode;
4-
use log::{log, Level};
5-
use serde::Serialize;
6-
use tide::{IntoResponse, QueryParseError, Response};
3+
use actix_web::{http::StatusCode, web::Json, ResponseError};
74

8-
/// Result type to be used by endpoints. Either OK [JsonResponse] or error [ErrorResponse].
9-
pub(crate) type JsonResult<T> = Result<JsonResponse<T>, ErrorResponse>;
10-
11-
/// Wrapper for OK endpoint responses.
12-
pub(crate) struct JsonResponse<T: Serialize + Send>(pub(crate) T);
13-
14-
/// Make Tide framework understand our OK responses.
15-
impl<T: Serialize + Send> IntoResponse for JsonResponse<T> {
16-
fn into_response(self) -> Response {
17-
Response::new(200)
18-
.body_json(&self.0)
19-
.unwrap_or_else(|e| ErrorResponse::from(e).into_response())
20-
}
21-
}
5+
/// Result type to be used by endpoints. Either OK [Json] or error [ErrorResponse].
6+
pub(crate) type JsonResult<T> = Result<Json<T>, ErrorResponse>;
227

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

34-
impl ErrorResponse {
35-
fn status(&self) -> StatusCode {
19+
/// Make actix-web understand our error responses.
20+
impl ResponseError for ErrorResponse {
21+
fn status_code(&self) -> StatusCode {
3622
match self {
3723
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
3824
Self::NotFound(_) => StatusCode::NOT_FOUND,
3925
Self::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
4026
}
4127
}
42-
43-
fn severity(&self) -> Level {
44-
match self {
45-
Self::BadRequest(_) | Self::NotFound(_) => Level::Warn,
46-
Self::InternalServerError(_) => Level::Error,
47-
}
48-
}
49-
}
50-
51-
/// Make Tide framework understand our error responses.
52-
impl IntoResponse for ErrorResponse {
53-
fn into_response(self) -> Response {
54-
#[derive(Serialize)]
55-
struct ErrorPayload {
56-
message: String,
57-
}
58-
59-
let status = self.status();
60-
let message = self.to_string();
61-
log!(self.severity(), "Responding with HTTP {}: {}", status, message);
62-
JsonResponse(ErrorPayload { message }).with_status(status).into_response()
63-
}
64-
}
65-
66-
/// Convert Tide query parse error into bad request.
67-
impl From<QueryParseError> for ErrorResponse {
68-
fn from(err: QueryParseError) -> Self {
69-
Self::BadRequest(err.to_string())
70-
}
7128
}
7229

7330
/// Convert Elasticsearch errors into internal server errors.
@@ -76,10 +33,3 @@ impl From<elasticsearch::Error> for ErrorResponse {
7633
Self::InternalServerError(format!("Elasticsearch error: {}", err))
7734
}
7835
}
79-
80-
/// Convert Serde (serialization, deserialization) errors into internal server errors.
81-
impl From<serde_json::Error> for ErrorResponse {
82-
fn from(err: serde_json::Error) -> Self {
83-
Self::InternalServerError(format!("Serde JSON error: {}", err))
84-
}
85-
}

0 commit comments

Comments
 (0)