Upgraded to actix_web 1.0+

* Some refactoring
 * Adding CORS support
 * Updating API & tests with 405 for the relevent calls
 * Fixing test so they can be run
This commit is contained in:
2019-09-25 17:11:10 +02:00
parent aebe559130
commit 66d19f7a2d
19 changed files with 731 additions and 2760 deletions

View File

@@ -18,16 +18,21 @@ license = "MIT"
include = ["Cargo.toml", "README.md", "LICENSE", "ACKNOWLEDGEMENTS", "src/**/*.rs"]
[dependencies]
mercator_db = "^0.1"
mercator_parser = "^0.1"
[features]
static-error-pages = []
actix = "^0.7"
actix-web = "^0.7"
[dependencies]
actix-web = "^1.0"
actix-files = "^0.1"
actix-service = "^0.4"
actix-cors = "^0.1"
measure_time = "^0.6"
memmap = "^0.7"
mercator_db = "^0.1"
mercator_parser = "^0.1"
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
@@ -37,3 +42,8 @@ bincode = "^1.1"
#log = { version = "^0.4", features = ["max_level_trace", "release_max_level_info"] }
log = { version = "^0.4", features = ["max_level_trace", "release_max_level_trace"] }
pretty_env_logger = "^0.3" # Logger implementation
[dev-dependencies]
# Only for tests
actix-server-config = "^0.1"
actix-http = "^0.2"

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,10 @@ mod rest_api;
mod shared_state;
use std::process::exit;
use std::sync::Arc;
use std::sync::RwLock;
use actix_web::web::Data;
use mercator_db::json::model;
use mercator_db::json::storage;
use mercator_db::DataBase;
use shared_state::SharedState;
@@ -54,8 +53,6 @@ fn main() {
let hostname;
let port;
let base;
let allowed_origins: Vec<String>;
//let data;
match std::env::var("MERCATOR_HOST") {
@@ -80,27 +77,6 @@ fn main() {
}
};
match std::env::var("MERCATOR_BASE") {
Ok(val) => base = val,
Err(val) => {
error!("Could not fetch {} : `{}`", "MERCATOR_BASE", val);
exit(1);
}
};
match std::env::var("MERCATOR_ALLOWED_ORIGINS") {
Ok(val) => {
allowed_origins = val
.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
}
Err(val) => {
error!("Could not fetch {} : `{}`", "MERCATOR_ALLOWED_ORIGINS", val);
exit(1);
}
};
/* UNUSED FOR NOW
match std::env::var("MERCATOR_DATA") {
Ok(val) => data = val,
@@ -134,13 +110,10 @@ fn main() {
}
// END of Temporary bloc
}
let state = SharedState::new(db);
rest_api::run(
hostname,
&hostname,
port,
base,
allowed_origins,
Arc::new(RwLock::new(state)),
Data::new(RwLock::new(SharedState::new(db))),
);
}

View File

@@ -1,47 +1,86 @@
use actix_web::HttpRequest;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use actix_web::HttpResponse;
use actix_web::Json;
use super::error_400;
use super::AppState;
use super::Filters;
use super::StringOrStaticFileResult;
use crate::shared_state::SharedState;
pub fn health(_req: &HttpRequest<AppState>) -> HttpResponse {
use super::error_422;
use super::ok_200;
use super::HandlerResult;
#[derive(Debug, Deserialize)]
pub struct Query {
query: String,
}
// Also used for the root service.
pub fn health() -> HttpResponse {
HttpResponse::Ok().finish()
}
pub fn query(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn query((parameters, state): (Json<Query>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("query Triggered!");
error_400()
let context = state.read().unwrap();
let query = &parameters.query;
if query.is_empty() {
error_422(format!("Invalid query in '{:?}'", query))
} else {
// FIXME: MANAGE PROJECTIONS
let results = context
.db()
.core_keys()
.iter()
// FIXME: Specify from json output space + threshold volume
.flat_map(|core| match context.query(query, core, None, None) {
Err(_) => vec![], // FIXME: Return errors here instead!!
Ok(r) => {
let mut r = r.into_iter().map(|o| o.space_id).collect::<Vec<_>>();
r.sort_unstable();
r.dedup();
r
}
})
.collect::<Vec<_>>();
ok_200(&results)
}
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("/health").route(web::get().to(health)));
cfg.service(web::resource("/query").route(web::post().to(query)));
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
mod routing {
use super::super::tests_utils::*;
#[test]
fn health() {
let ep = get_path("/health".into());
expect_200(http::Method::GET, ep.clone());
let ep = &get_path("/health");
expect_400(http::Method::POST, ep.clone());
expect_400(http::Method::PUT, ep.clone());
expect_400(http::Method::PATCH, ep.clone());
expect_400(http::Method::DELETE, ep.clone());
expect_200(Method::GET, ep);
expect_405(Method::POST, ep);
expect_405(Method::PUT, ep);
expect_405(Method::PATCH, ep);
expect_405(Method::DELETE, ep);
}
#[test]
fn query() {
let ep = get_path("/query".into());
expect_200(http::Method::POST, ep.clone());
expect_422(http::Method::POST, ep.clone());
let ep = &get_path("/query");
expect_400(http::Method::GET, ep.clone());
expect_400(http::Method::PUT, ep.clone());
expect_400(http::Method::PATCH, ep.clone());
expect_400(http::Method::DELETE, ep.clone());
expect_200(Method::POST, ep);
expect_422(Method::POST, ep);
expect_405(Method::GET, ep);
expect_405(Method::PUT, ep);
expect_405(Method::PATCH, ep);
expect_405(Method::DELETE, ep);
}
}

View File

@@ -1,12 +1,15 @@
use actix_web::HttpRequest;
use actix_web::Json;
use actix_web::Path;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Path;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::ok_200;
use super::AppState;
use super::StringOrStaticFileResult;
use super::HandlerResult;
#[derive(Clone, Deserialize, Serialize)]
pub struct Core {
@@ -15,92 +18,86 @@ pub struct Core {
scales: Vec<Vec<i32>>,
}
pub fn put(
(_path, _core, _state): (Path<String>, Json<Core>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
trace!("PUT Triggered!");
fn put(path: Path<String>) -> HandlerResult {
trace!("PUT Triggered on {}", path);
error_400()
}
pub fn get((core, state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn get((core, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET Triggered!");
let core = core.to_string();
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
match context.db().core(core) {
Ok(core) => ok_200(&Core {
name: core.name().clone(),
version: core.version().clone(),
scales: vec![vec![0, 0, 0]],
//FIXME: Report the actual values. Might need to change the format
// to per reference space.
// FIXME: Report the actual values. Might need to change the format
// to per reference space.
}),
Err(_) => error_404(),
}
}
pub fn patch(
(_path, _core, _state): (Path<String>, Json<Core>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
trace!("PATCH Triggered!");
fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
pub fn delete((_path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
trace!("DELETE Triggered!");
fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/cores/{name}")
.route(web::get().to(get))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
mod routing {
use super::super::tests_utils::*;
const INSTANCE_EXISTS: &str = "/cores/42";
const INSTANCE_INVALID: &str = "/cores/21";
const INSTANCE_EXISTS: &str = CORE;
const INSTANCE_INVALID: &str = "/41-doesnotexists";
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_422(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_200(
http::Method::PUT,
get_path(INSTANCE_INVALID),
"".to_string(),
);
json::expect_200(Method::PUT, &get_core(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PUT, &get_core(INSTANCE_EXISTS), "".to_string());
json::expect_200(Method::PUT, &get_core(INSTANCE_INVALID), "".to_string());
}
#[test]
fn patch() {
json::expect_200(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
json::expect_422(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
expect_400(http::Method::PATCH, get_path(INSTANCE_INVALID));
json::expect_200(Method::PATCH, &get_core(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PATCH, &get_core(INSTANCE_EXISTS), "".to_string());
expect_404(Method::PATCH, &get_core(INSTANCE_INVALID));
}
#[test]
fn get() {
expect_200(http::Method::GET, get_path(INSTANCE_EXISTS));
expect_404(http::Method::GET, get_path(INSTANCE_INVALID));
expect_200(Method::GET, &get_core(INSTANCE_EXISTS));
expect_404(Method::GET, &get_core(INSTANCE_INVALID));
}
#[test]
fn delete() {
expect_200(http::Method::DELETE, get_path(INSTANCE_EXISTS));
expect_404(http::Method::DELETE, get_path(INSTANCE_INVALID));
expect_200(Method::DELETE, &get_core(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_core(INSTANCE_INVALID));
}
#[test]
fn post() {
expect_400(http::Method::POST, get_path(INSTANCE_EXISTS));
expect_400(http::Method::POST, get_path(INSTANCE_INVALID));
expect_405(Method::POST, &get_core(INSTANCE_EXISTS));
expect_405(Method::POST, &get_core(INSTANCE_INVALID));
}
}

View File

@@ -1,17 +1,19 @@
use actix_web::HttpRequest;
use actix_web::Json;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use crate::shared_state::SharedState;
use super::error_400;
use super::ok_200;
use super::AppState;
use super::Filters;
use super::StringOrStaticFileResult;
use super::HandlerResult;
pub fn post(
(parameters, state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn post((parameters, state): (Option<Json<Filters>>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("POST Triggered!");
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
let parameters = Filters::get(parameters);
let mut results = match parameters.filters {
@@ -20,8 +22,9 @@ pub fn post(
.db()
.core_keys()
.iter()
//FIXME: Specify from json output space + threshold volume
.filter_map(|core| match context.filter(&filter, core, None, None) {
Err(_) => None, //FIXME: Return errors here instead!!
Err(_) => None, // FIXME: Return errors here instead!!
Ok(_) => Some(core.to_string()),
})
.collect(),
@@ -33,30 +36,34 @@ pub fn post(
ok_200(&results)
}
pub fn put(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
pub fn patch(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
pub fn delete(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/cores")
.route(web::post().to(post))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
mod routing {
use super::super::tests_utils::*;
const COLLECTION: &str = "/cores";
@@ -64,43 +71,43 @@ mod tests {
#[test]
fn post() {
expect_200(http::Method::POST, get_path(COLLECTION));
json::expect_200(http::Method::POST, get_path(COLLECTION), "".to_string());
expect_200(Method::POST, &get_core(""));
json::expect_200(Method::POST, &get_core(""), "".to_string());
json::expect_422(http::Method::POST, get_path(COLLECTION), "".to_string());
json::expect_422(Method::POST, &get_core(""), "".to_string());
expect_400(http::Method::POST, get_path(COLLECTION));
expect_400(Method::POST, &get_core(""));
}
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PUT, &get_core(""), "".to_string());
json::expect_422(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PUT, &get_core(""), "".to_string());
expect_400(http::Method::PUT, get_path(COLLECTION));
expect_400(Method::PUT, &get_core(""));
}
#[test]
fn patch() {
json::expect_200(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PATCH, &get_core(""), "".to_string());
json::expect_422(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PATCH, &get_core(""), "".to_string());
expect_400(http::Method::PATCH, get_path(COLLECTION));
expect_400(Method::PATCH, &get_core(""));
}
#[test]
fn delete() {
json::expect_200(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_200(Method::DELETE, &get_core(""), "".to_string());
json::expect_422(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_422(Method::DELETE, &get_core(""), "".to_string());
expect_400(http::Method::DELETE, get_path(COLLECTION));
expect_400(Method::DELETE, &get_core(""));
}
#[test]
fn get() {
expect_400(http::Method::GET, get_path(COLLECTION));
expect_405(Method::GET, &get_core(""));
}
}

View File

@@ -1,101 +0,0 @@
use super::AppState;
use actix_web::fs;
use actix_web::http::StatusCode;
use actix_web::HttpRequest;
use actix_web::Path;
use actix_web::Result;
pub fn page_400(_req: &HttpRequest<AppState>) -> Result<fs::NamedFile> {
trace!("400 Triggered!");
Ok(fs::NamedFile::open("static/400.html")?.set_status_code(StatusCode::BAD_REQUEST))
}
pub fn page_400_no_state(_req: &HttpRequest) -> Result<fs::NamedFile> {
trace!("400 Triggered!");
Ok(fs::NamedFile::open("static/400.html")?.set_status_code(StatusCode::BAD_REQUEST))
}
pub fn page_404(_req: &HttpRequest) -> Result<fs::NamedFile> {
trace!("404 Triggered!");
Ok(fs::NamedFile::open("static/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
pub fn static_file((path, _req): (Path<String>, HttpRequest)) -> Result<fs::NamedFile> {
trace!("static/{} Triggered!", path);
match fs::NamedFile::open(format!("static/{}", path).as_str()) {
Ok(o) => Ok(o),
Err(_) => {
Ok(fs::NamedFile::open("static/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::RwLock;
use actix_web::http;
use actix_web::test::TestRequest;
#[test]
fn page_400() {
let response = TestRequest::with_state(AppState {
shared: Arc::new(RwLock::new(0)),
})
.run(&super::page_400)
.unwrap();
assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
}
#[test]
fn page_400_no_state() {
let response = TestRequest::default()
.run(&super::page_400_no_state)
.unwrap();
assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
}
#[test]
fn page_404() {
let response = TestRequest::default().run(&super::page_404).unwrap();
assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
}
}
#[cfg(test)]
mod routing {
use super::super::tests::*;
#[test]
fn default_no_path() {
expect_404(http::Method::GET, "".into());
}
#[test]
fn default_slash() {
expect_404(http::Method::GET, "/".into());
expect_404(http::Method::GET, "//".into());
expect_404(http::Method::GET, "/ /".into());
expect_404(http::Method::GET, "/ //".into());
expect_404(http::Method::GET, "// ".into());
}
#[test]
fn default_invalid_prefix() {
expect_404(http::Method::GET, "/test".into());
expect_404(http::Method::GET, format!("{}test", PREFIX));
}
#[test]
fn default_prefix_no_slash() {
expect_400(http::Method::GET, PREFIX.into());
}
#[test]
fn default_prefix_final_slash() {
expect_400(http::Method::GET, format!("{}/", PREFIX));
}
}

88
src/rest_api/helpers.rs Normal file
View File

@@ -0,0 +1,88 @@
use std::fmt::Debug;
use std::io::Error;
use std::io::ErrorKind;
use actix_files::NamedFile;
use actix_web::Either;
use actix_web::HttpResponse;
use serde::Serialize;
use super::HandlerResult;
use super::*;
pub fn ok_200<T>(data: &T) -> HandlerResult
where
T: Serialize,
{
match serde_json::to_string(data) {
Ok(response) => Ok(Either::A(HttpResponse::Ok().body(response))),
Err(e) => error_500(e),
}
}
pub fn error_422<S>(reason: S) -> HandlerResult
where
S: Debug,
{
Ok(Either::A(HttpResponse::UnprocessableEntity().body(
format!("422 - Unprocessable Entity:\n{:?}", reason),
)))
}
pub fn error_500<S>(reason: S) -> HandlerResult
where
S: Debug,
{
Err(Error::new(
ErrorKind::Other,
format!("500 - Internal Server Error: {:?}", reason),
))
}
//pub fn page_400() -> HandlerResult {
// trace!("400 Triggered!");
// error_400()
//}
pub fn page_404() -> HandlerResult {
trace!("404 Triggered!");
error_404()
}
//pub fn page_405() -> HandlerResult {
// trace!("405 Triggered!");
// error_405()
//}
pub fn api(path: Path<String>) -> Result<NamedFile, Error> {
trace!("api/{} Triggered!", path);
match NamedFile::open(format!("static/api/{}", path).as_str()) {
Ok(o) => Ok(o),
Err(_) => {
Ok(NamedFile::open("static/errors/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
}
}
pub fn static_file(path: Path<String>) -> Result<NamedFile, Error> {
trace!("static/{} Triggered!", path);
match NamedFile::open(format!("static/{}", path).as_str()) {
Ok(o) => Ok(o),
Err(_) => {
Ok(NamedFile::open("static/errors/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
}
}
#[cfg(test)]
mod tests {
use super::super::tests_utils::*;
use super::*;
#[test]
fn page_400() {
// expect_400(Method::PATCH, get_core(INSTANCE_INVALID));
}
}

View File

@@ -0,0 +1,23 @@
#![cfg(not(feature = "static-error-pages"))]
use actix_web::http::StatusCode;
use actix_web::Either;
use actix_web::HttpResponse;
use super::HandlerResult;
fn error(code: StatusCode) -> HandlerResult {
Ok(Either::A(HttpResponse::build(code).finish()))
}
pub fn error_400() -> HandlerResult {
error(StatusCode::BAD_REQUEST)
}
pub fn error_404() -> HandlerResult {
error(StatusCode::NOT_FOUND)
}
//pub fn error_405() -> HandlerResult {
// error(StatusCode::METHOD_NOT_ALLOWED)
//}

View File

@@ -0,0 +1,25 @@
#![cfg(feature = "static-error-pages")]
use actix_files::NamedFile;
use actix_web::http::StatusCode;
use actix_web::Either;
use super::HandlerResult;
fn error(code: StatusCode) -> HandlerResult {
let path = format!("static/errors/{}.html", u16::from(code));
Ok(Either::B(NamedFile::open(path)?.set_status_code(code)))
}
pub fn error_400() -> HandlerResult {
error(StatusCode::BAD_REQUEST)
}
pub fn error_404() -> HandlerResult {
error(StatusCode::NOT_FOUND)
}
//pub fn error_405() -> HandlerResult {
// error(StatusCode::METHOD_NOT_ALLOWED)
//}

View File

@@ -9,37 +9,46 @@ mod cores;
mod spatial_object;
mod spatial_objects;
mod default;
mod helpers;
mod helpers_dynamic_pages;
mod helpers_static_pages;
use std::sync::Arc;
use std::io::Error;
use std::process::exit;
use std::sync::RwLock;
use actix_web::fs;
use actix_cors::Cors;
use actix_files::NamedFile;
use actix_web::http;
use actix_web::http::Method;
use actix_web::http::StatusCode;
use actix_web::middleware;
use actix_web::middleware::cors::Cors;
use actix_web::pred;
use actix_web::server;
use actix_web::server::HttpHandler;
use actix_web::server::HttpHandlerTask;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::App;
use actix_web::Either;
use actix_web::Json;
use serde::Serialize;
use actix_web::HttpResponse;
use actix_web::HttpServer;
#[cfg(feature = "static-error-pages")]
pub use helpers_static_pages::*;
#[cfg(not(feature = "static-error-pages"))]
pub use helpers_dynamic_pages::*;
pub use helpers::*;
use crate::SharedState;
// Application shared state
pub struct AppState {
shared: Arc<RwLock<SharedState>>,
}
pub type HandlerResult = Result<Either<HttpResponse, NamedFile>, Error>;
#[derive(Debug, Deserialize)]
pub struct Filters {
filters: Option<String>,
ids_only: Option<bool>,
// resolution: Option<Vec<u64>>, // None means automatic selection, based on ViewPort
// view_port: Option<u64>,
}
impl Filters {
@@ -50,40 +59,13 @@ impl Filters {
None => Filters {
filters: None,
ids_only: Some(true),
//resolution: None,
},
Some(p) => p.0,
}
}
}
type StringOrStaticFileResult = Either<String, fs::NamedFile>;
pub fn ok_200<T>(data: &T) -> StringOrStaticFileResult
where
T: Serialize,
{
Either::A(
serde_json::to_string_pretty(data)
.unwrap_or_else(|e| format!("Internal Error 500: {:?}", e)),
)
}
pub fn error_400() -> StringOrStaticFileResult {
Either::B(
fs::NamedFile::open("static/400.html")
.unwrap()
.set_status_code(StatusCode::BAD_REQUEST),
)
}
pub fn error_404() -> StringOrStaticFileResult {
Either::B(
fs::NamedFile::open("static/404.html")
.unwrap()
.set_status_code(StatusCode::NOT_FOUND),
)
}
// From: https://stackoverflow.com/a/52367953
fn into_static<S>(s: S) -> &'static str
where
@@ -92,218 +74,252 @@ where
Box::leak(s.into().into_boxed_str())
}
fn get_app<S>(
prefix: S,
allowed_origins: &[&'static str],
state: Arc<RwLock<SharedState>>,
) -> Vec<Box<HttpHandler<Task = Box<HttpHandlerTask>>>>
where
S: Into<String>,
{
vec![
App::with_state(AppState { shared: state })
.prefix(into_static(prefix).to_string())
.middleware(middleware::Logger::new(
r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T[s] %D[ms]"#,
))
// ACTIONS -------------------------------------------------------------------
.resource("/health", |r| {
r.method(Method::GET).f(actions::health);
r.route()
.filter(pred::Not(pred::Get()))
.f(default::page_400);
})
// DEFAULT -------------------------------------------------------------------
.default_resource(|r| {
r.f(default::page_400);
})
// REQUIRES CORS Support ---------------------------------------------------------------
.configure(|app| {
let mut cors = Cors::for_app(app);
for origin in allowed_origins {
cors.allowed_origin(origin);
}
cors.allowed_methods(vec!["GET", "POST", "UPDATE", "PATCH", "DELETE", "OPTIONS"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(600)
.resource("/queries", |r| {
r.method(Method::POST).with(actions::query);
r.route()
.filter(pred::Not(pred::Post()))
.f(default::page_400);
})
// SPACES -------------------------------------------------------------------
.resource("/spaces", |r| {
r.method(Method::POST).with(spaces::post);
r.method(Method::PUT).with(spaces::put);
r.method(Method::PATCH).with(spaces::patch);
r.method(Method::DELETE).with(spaces::delete);
})
.resource("/spaces/{name}", |r| {
r.method(Method::PUT).with(space::put);
r.method(Method::PATCH).with(space::patch);
r.method(Method::GET).with(space::get);
r.method(Method::DELETE).with(space::delete);
})
// DATASETS -------------------------------------------------------------------
.resource("/cores", |r| {
r.method(Method::POST).with(&cores::post);
r.method(Method::PUT).with(&cores::put);
r.method(Method::PATCH).with(&cores::patch);
r.method(Method::DELETE).with(&cores::delete);
})
.resource("/cores/{name}", |r| {
r.method(Method::PUT).with(core::put);
r.method(Method::GET).with(core::get);
r.method(Method::PATCH).with(core::patch);
r.method(Method::DELETE).with(core::delete);
})
// SPATIAL OBJECTS -------------------------------------------------------------------
.resource("/cores/{name}/spatial_objects", |r| {
r.method(Method::POST).with(spatial_objects::post);
r.method(Method::PUT).with(spatial_objects::put);
r.method(Method::PATCH).with(spatial_objects::patch);
r.method(Method::DELETE).with(spatial_objects::delete);
})
.resource("/cores/{name}/spatial_objects/{id}", |r| {
r.method(Method::PUT).with(spatial_object::put);
r.method(Method::GET).with(spatial_object::get);
r.method(Method::PATCH).with(spatial_object::patch);
r.method(Method::DELETE).with(spatial_object::delete);
})
.register()
})
.boxed(),
App::new()
.resource("/static/{file}", |r| {
r.method(Method::GET).with(default::static_file)
})
.default_resource(|r| {
// 404 for GET request
r.method(Method::GET).f(default::page_404);
fn config_v1(cfg: &mut web::ServiceConfig) {
// Warning: Order matters, as a more generic path would catch calls for a
// more specific one when registered first.
space::config(cfg);
spaces::config(cfg);
// all requests that are not `GET`
r.route()
.filter(pred::Not(pred::Get()))
.f(default::page_400_no_state);
})
.boxed(),
]
core::config(cfg);
cores::config(cfg);
spatial_object::config(cfg);
spatial_objects::config(cfg);
actions::config(cfg);
cfg.route("/static/{file:.*}", web::get().to(static_file));
cfg.route("/api/{file:.*}", web::get().to(api));
cfg.route("/", web::to(page_404));
}
pub fn run<S>(
host: S,
port: u16,
prefix: S,
allowed_origins: Vec<S>,
state: Arc<RwLock<SharedState>>,
) where
S: Into<String>,
{
info!("Initializing server...");
pub fn config(cfg: &mut web::ServiceConfig) {
let prefix;
let sys = actix::System::new("spatial-search");
let prefix = into_static(prefix);
let host = host.into();
match std::env::var("MERCATOR_BASE") {
Ok(val) => prefix = val,
Err(val) => {
error!("Could not fetch {} : `{}`", "MERCATOR_BASE", val);
exit(1);
}
};
let mut origins = Vec::with_capacity(allowed_origins.len());
for origin in allowed_origins {
origins.push(into_static(origin));
cfg.service(web::scope(into_static(format!("{}/v1", prefix))).configure(config_v1))
.service(web::scope(into_static(prefix)).configure(config_v1))
.route("/health", web::get().to(actions::health))
.route("/static/{file:.*}", web::get().to(static_file));
}
pub fn get_cors() -> Cors {
// Setup CORS support.
let mut cors = Cors::new();
match std::env::var("MERCATOR_ALLOWED_ORIGINS") {
Ok(val) => {
let allowed_origins = val.split(',').map(|s| s.trim()).collect::<Vec<_>>();
for origin in allowed_origins {
if !origin.is_empty() {
cors = cors.allowed_origin(into_static(origin));
}
}
}
Err(val) => {
warn!(
"Could not fetch {} : `{}`, allowing all origins",
"MERCATOR_ALLOWED_ORIGINS", val
);
}
}
server::new(move || get_app(prefix, &origins, state.clone()))
cors.allowed_methods(vec!["GET", "POST", "UPDATE", "PATCH", "DELETE", "OPTIONS"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(600)
}
macro_rules! get_app {
($state:expr) => {
App::new()
.register_data($state.clone())
.wrap(middleware::Logger::new(
r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T[s] %D[ms]"#,
))
.wrap(get_cors())
.configure(config)
.default_service(
web::resource("/")
// 404 for GET request
.route(web::to(page_404)),
)
};
}
pub fn run(host: &str, port: u16, state: Data<RwLock<SharedState>>) {
info!("Starting http server: {}:{}", host, port);
// Create & run the server.
match HttpServer::new(move || get_app!(state))
.bind(format!("{}:{}", host, port))
.unwrap()
.start();
info!("Started http server: {}:{}{}", host, port, prefix);
let _ = sys.run();
.run()
{
Ok(_) => info!("Server Stopped!"),
Err(e) => error!("Error running the server: {}", e),
};
}
#[cfg(test)]
mod tests {
use super::get_app;
use super::{Arc, RwLock, SharedState};
mod tests_utils {
use super::*;
pub use actix_web::http;
pub use actix_web::http::Method;
pub use actix_web::test::TestServer;
//use actix_server_config::ServerConfig;
//use actix_service::IntoNewService;
//use actix_service::NewService;
use actix_service::Service;
//use actix_web::dev::ServiceResponse;
use actix_web::test;
//use actix_web::test::TestRequest;
use mercator_db::DataBase;
pub const PREFIX: &str = "spatial-search";
pub const CORE_ID: &str = "10k";
fn get_start_state() -> Arc<RwLock<SharedState>> {
Arc::new(RwLock::new(0))
}
pub const PREFIX: &str = "/spatial-search";
pub const CORE: &str = "/10k";
pub const SPACE: &str = "/std";
pub const SPATIAL_OBJECT: &str = "/oid0.44050628835072825";
pub fn get_test_server() -> TestServer {
TestServer::with_factory(move || get_app(PREFIX, get_start_state().clone()))
pub enum Method {
GET,
POST,
PUT,
PATCH,
DELETE,
}
pub fn get_path(path: &str) -> String {
format!("{}{}", PREFIX, path)
}
pub fn expect_200(method: Method, path: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).finish().unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::OK, response.status());
pub fn get_space(name: &str) -> String {
format!("{}{}", get_path("/spaces"), name)
}
pub fn expect_400(method: Method, path: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).finish().unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::BAD_REQUEST, response.status());
pub fn get_core(name: &str) -> String {
format!("{}{}", get_path("/cores"), name)
}
pub fn expect_404(method: Method, path: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).finish().unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::NOT_FOUND, response.status());
pub fn get_objects(name: &str) -> String {
format!("{}{}{}", get_core(CORE), "/spatial_objects", name)
}
pub fn expect_422(method: Method, path: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).finish().unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::UNPROCESSABLE_ENTITY, response.status());
pub fn expect(method: Method, path: &str, code: http::StatusCode) {
std::env::set_var("MERCATOR_BASE", PREFIX);
let mut app = test::init_service(get_app!(Data::new(RwLock::new(SharedState::new(
DataBase::load(CORE_ID).unwrap()
)))));
let request = match method {
Method::GET => test::TestRequest::get(),
Method::POST => test::TestRequest::post(),
Method::PUT => test::TestRequest::put(),
Method::PATCH => test::TestRequest::patch(),
Method::DELETE => test::TestRequest::delete(),
};
let request = request.uri(&path).to_request();
let response = test::block_on(app.call(request)).unwrap();
assert_eq!(response.status(), code);
}
pub fn expect_200(method: Method, path: &str) {
expect(method, path, http::StatusCode::OK);
}
pub fn expect_400(method: Method, path: &str) {
expect(method, path, http::StatusCode::BAD_REQUEST);
}
pub fn expect_404(method: Method, path: &str) {
expect(method, path, http::StatusCode::NOT_FOUND);
}
pub fn expect_405(method: Method, path: &str) {
expect(method, path, http::StatusCode::METHOD_NOT_ALLOWED);
}
pub fn expect_422(method: Method, path: &str) {
expect(method, path, http::StatusCode::UNPROCESSABLE_ENTITY);
}
pub mod json {
use super::*;
pub fn expect_200(method: Method, path: String, json: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).json(json).unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::OK, response.status());
pub fn expect_200(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::OK);
}
/*
pub fn expect_400(method: Method, path: String, json: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).json(json).unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::BAD_REQUEST, response.status());
}
*/
pub fn expect_404(method: Method, path: String, json: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).json(json).unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::NOT_FOUND, response.status());
pub fn expect_404(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::NOT_FOUND);
}
pub fn expect_422(method: Method, path: String, json: String) -> () {
let mut srv = get_test_server();
let req = srv.client(method, path.as_str()).json(json).unwrap();
let response = srv.execute(req.send()).unwrap();
assert_eq!(http::StatusCode::UNPROCESSABLE_ENTITY, response.status());
pub fn expect_422(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::UNPROCESSABLE_ENTITY);
}
}
}
#[cfg(test)]
mod routing {
use std::panic;
use super::tests_utils::*;
#[test]
fn default_no_path() {
// FIXME: Currently the string is validated by the URI constructor which
// simply unwraps, thus we have to resort to this ugly workaround.
// The goal is to catch if that behavior changes in the future.
let result = panic::catch_unwind(|| {
expect_404(Method::GET, "");
});
assert!(result.is_err());
}
#[test]
fn default_slash() {
// We have to manually URL-encode spaces.
expect_404(Method::GET, "/");
expect_404(Method::GET, "//");
expect_404(Method::GET, "/%20/");
expect_404(Method::GET, "/%20//");
expect_404(Method::GET, "//%20");
}
#[test]
fn default_invalid_prefix() {
expect_404(Method::GET, "/test");
expect_404(Method::GET, &format!("{}test", PREFIX));
}
#[test]
fn default_prefix_no_slash() {
expect_404(Method::PUT, PREFIX);
expect_404(Method::GET, PREFIX);
expect_404(Method::POST, PREFIX);
expect_404(Method::PATCH, PREFIX);
expect_404(Method::DELETE, PREFIX);
}
#[test]
fn default_prefix_final_slash() {
let path = &format!("{}/", PREFIX);
expect_404(Method::PUT, path);
expect_404(Method::GET, path);
expect_404(Method::POST, path);
expect_404(Method::PATCH, path);
expect_404(Method::DELETE, path);
}
}

View File

@@ -1,23 +1,26 @@
use actix_web::HttpRequest;
use actix_web::Path;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Path;
use crate::model;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::ok_200;
use super::AppState;
use super::StringOrStaticFileResult;
use super::HandlerResult;
pub fn put((path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn put(path: Path<String>) -> HandlerResult {
trace!("PUT Triggered on {}", path);
error_400()
}
pub fn get((path, state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn get((path, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET Triggered on '{}'", path);
let name = path.to_string();
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
match context.db().space(name) {
Ok(space) => {
@@ -28,66 +31,64 @@ pub fn get((path, state): (Path<String>, HttpRequest<AppState>)) -> StringOrStat
}
}
pub fn patch((path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
pub fn delete((path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/spaces/{name}")
.route(web::get().to(get))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
const INSTANCE_EXISTS: &str = "/spaces/42";
const INSTANCE_INVALID: &str = "/spaces/21";
#[cfg(test)]
mod routing {
use super::super::tests_utils::*;
const INSTANCE_EXISTS: &str = SPACE;
const INSTANCE_INVALID: &str = "/21-doesnotexists";
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_422(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_200(
http::Method::PUT,
get_path(INSTANCE_INVALID),
"".to_string(),
);
json::expect_200(Method::PUT, &get_space(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PUT, &get_space(INSTANCE_EXISTS), "".to_string());
json::expect_200(Method::PUT, &get_space(INSTANCE_INVALID), "".to_string());
}
#[test]
fn patch() {
json::expect_200(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
json::expect_422(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
expect_400(http::Method::PATCH, get_path(INSTANCE_INVALID));
json::expect_200(Method::PATCH, &get_space(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PATCH, &get_space(INSTANCE_EXISTS), "".to_string());
expect_400(Method::PATCH, &get_space(INSTANCE_INVALID));
}
#[test]
fn get() {
expect_200(http::Method::GET, get_path(INSTANCE_EXISTS));
expect_404(http::Method::GET, get_path(INSTANCE_INVALID));
expect_200(Method::GET, &get_space(INSTANCE_EXISTS));
expect_404(Method::GET, &get_space(INSTANCE_INVALID));
}
#[test]
fn delete() {
expect_200(http::Method::DELETE, get_path(INSTANCE_EXISTS));
expect_404(http::Method::DELETE, get_path(INSTANCE_INVALID));
expect_200(Method::DELETE, &get_space(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_space(INSTANCE_INVALID));
}
#[test]
fn post() {
expect_400(http::Method::POST, get_path(INSTANCE_EXISTS));
expect_400(http::Method::POST, get_path(INSTANCE_INVALID));
expect_405(Method::POST, &get_space(INSTANCE_EXISTS));
expect_405(Method::POST, &get_space(INSTANCE_INVALID));
}
}

View File

@@ -1,17 +1,19 @@
use actix_web::HttpRequest;
use actix_web::Json;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use crate::shared_state::SharedState;
use super::error_400;
use super::ok_200;
use super::AppState;
use super::Filters;
use super::StringOrStaticFileResult;
use super::HandlerResult;
pub fn post(
(parameters, state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn post((parameters, state): (Option<Json<Filters>>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("POST Triggered!");
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
let parameters = Filters::get(parameters);
let mut results = match parameters.filters {
@@ -20,8 +22,9 @@ pub fn post(
.db()
.core_keys()
.iter()
// FIXME: Specify from json output space + threshold volume
.flat_map(|core| match context.filter(&filter, core, None, None) {
Err(_) => vec![], //FIXME: Return errors here instead!!
Err(_) => vec![], // FIXME: Return errors here instead!!
Ok(r) => {
let mut r = r.into_iter().map(|o| o.space_id).collect::<Vec<_>>();
r.sort_unstable();
@@ -37,74 +40,76 @@ pub fn post(
ok_200(&results)
}
pub fn put(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
pub fn patch(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
pub fn delete(
(_parameters, _state): (Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/spaces")
.route(web::post().to(post))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
const COLLECTION: &str = "/spaces";
#[cfg(test)]
mod routing {
use super::super::tests_utils::*;
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn post() {
expect_200(http::Method::POST, get_path(COLLECTION));
json::expect_200(http::Method::POST, get_path(COLLECTION), "".to_string());
expect_200(Method::POST, &get_space(""));
json::expect_200(Method::POST, &get_space(""), "".to_string());
json::expect_422(http::Method::POST, get_path(COLLECTION), "".to_string());
json::expect_422(Method::POST, &get_space(""), "".to_string());
expect_400(http::Method::POST, get_path(COLLECTION));
expect_400(Method::POST, &get_space(""));
}
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PUT, &get_space(""), "".to_string());
json::expect_422(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PUT, &get_space(""), "".to_string());
expect_400(http::Method::PUT, get_path(COLLECTION));
expect_400(Method::PUT, &get_space(""));
}
#[test]
fn patch() {
json::expect_200(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PATCH, &get_space(""), "".to_string());
json::expect_422(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PATCH, &get_space(""), "".to_string());
expect_400(http::Method::PATCH, get_path(COLLECTION));
expect_400(Method::PATCH, &get_space(""));
}
#[test]
fn delete() {
json::expect_200(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_200(Method::DELETE, &get_space(""), "".to_string());
json::expect_422(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_422(Method::DELETE, &get_space(""), "".to_string());
expect_400(http::Method::DELETE, get_path(COLLECTION));
expect_400(Method::DELETE, &get_space(""));
}
#[test]
fn get() {
expect_400(http::Method::GET, get_path(COLLECTION));
expect_405(Method::GET, &get_space(""));
}
}

View File

@@ -1,104 +1,104 @@
use actix_web::HttpRequest;
use actix_web::Json;
use actix_web::Path;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Path;
use crate::model::to_spatial_objects;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::ok_200;
use super::AppState;
use super::StringOrStaticFileResult;
use crate::model::to_spatial_objects;
use super::HandlerResult;
pub fn put(
(core, id, state): (Path<String>, Path<String>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
trace!("PUT Triggered!");
fn put(path: Path<String>) -> HandlerResult {
trace!("PUT Triggered on {}", path);
error_400()
}
pub fn get(
(path, state): (Path<(String, String)>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn get((path, state): (Path<(String, String)>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET Triggered!");
let (core, id) = path.into_inner();
let core = core.to_string();
let id = id.to_string();
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
let db = context.db();
match db.core(core) {
Ok(core) => match core.get_by_id(db, &id, None, 0.0) {
Ok(objects) => ok_200(&to_spatial_objects(db, objects)),
Ok(objects) => {
let results = to_spatial_objects(db, objects);
if results.is_empty() {
error_404()
} else {
ok_200(&results)
}
}
Err(_) => error_404(),
},
Err(_) => error_404(),
}
}
pub fn patch(
(core, id, state): (Path<String>, Path<String>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
trace!("PATCH Triggered!");
fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
pub fn delete(
(core, id, state): (Path<String>, Path<String>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
trace!("DELETE Triggered!");
fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/cores/{name}/spatial_objects/{id}")
.route(web::get().to(get))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
mod routing {
use super::super::tests_utils::*;
const INSTANCE_EXISTS: &str = "/cores/42/spatial_objects/42";
const INSTANCE_INVALID: &str = "/cores/42/spatial_objects/21";
const INSTANCE_EXISTS: &str = SPATIAL_OBJECT;
const INSTANCE_INVALID: &str = "/21-doesnotexists";
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_422(http::Method::PUT, get_path(INSTANCE_EXISTS), "".to_string());
json::expect_200(
http::Method::PUT,
get_path(INSTANCE_INVALID),
"".to_string(),
);
json::expect_200(Method::PUT, &get_objects(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PUT, &get_objects(INSTANCE_EXISTS), "".to_string());
json::expect_200(Method::PUT, &get_objects(INSTANCE_INVALID), "".to_string());
}
#[test]
fn patch() {
json::expect_200(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
json::expect_422(
http::Method::PATCH,
get_path(INSTANCE_EXISTS),
"".to_string(),
);
expect_400(http::Method::PATCH, get_path(INSTANCE_INVALID));
json::expect_200(Method::PATCH, &get_objects(INSTANCE_EXISTS), "".to_string());
json::expect_422(Method::PATCH, &get_objects(INSTANCE_EXISTS), "".to_string());
expect_400(Method::PATCH, &get_objects(INSTANCE_INVALID));
}
#[test]
fn get() {
expect_200(http::Method::GET, get_path(INSTANCE_EXISTS));
expect_404(http::Method::GET, get_path(INSTANCE_INVALID));
expect_200(Method::GET, &get_objects(INSTANCE_EXISTS));
expect_404(Method::GET, &get_objects(INSTANCE_INVALID));
}
#[test]
fn delete() {
expect_200(http::Method::DELETE, get_path(INSTANCE_EXISTS));
expect_404(http::Method::DELETE, get_path(INSTANCE_INVALID));
expect_200(Method::DELETE, &get_objects(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_objects(INSTANCE_INVALID));
}
#[test]
fn post() {
expect_400(http::Method::POST, get_path(INSTANCE_EXISTS));
expect_400(http::Method::POST, get_path(INSTANCE_INVALID));
expect_405(Method::POST, &get_objects(INSTANCE_EXISTS));
expect_405(Method::POST, &get_objects(INSTANCE_INVALID));
}
}

View File

@@ -1,20 +1,28 @@
use actix_web::HttpRequest;
use actix_web::Json;
use actix_web::Path;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use actix_web::web::Path;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::ok_200;
use super::AppState;
use super::Filters;
use super::StringOrStaticFileResult;
use super::HandlerResult;
pub fn post(
(core_id, parameters, state): (Path<String>, Option<Json<Filters>>, HttpRequest<AppState>),
) -> StringOrStaticFileResult {
fn post(
(core_id, parameters, state): (
Path<String>,
Option<Json<Filters>>,
Data<RwLock<SharedState>>,
),
) -> HandlerResult {
trace!("POST Triggered!");
let core = core_id.to_string();
let context = state.state().shared.read().unwrap();
let context = state.read().unwrap();
match context.db().core(core) {
Ok(core) => {
@@ -23,8 +31,9 @@ pub fn post(
// Generate a list of oid.
let mut results = match parameters.filters {
None => core.keys().iter().map(|o| o.id().clone()).collect(),
// FIXME: Specify from json output space + threshold volume
Some(filter) => match context.filter(&filter, &core_id, None, None) {
Err(_) => vec![], //FIXME: Return errors here instead!!
Err(_) => vec![], // FIXME: Return errors here instead!!
Ok(objects) => objects.iter().map(|o| o.value.id().clone()).collect(),
},
};
@@ -37,68 +46,76 @@ pub fn post(
}
}
pub fn put((_path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
pub fn patch((_path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
pub fn delete((_path, _state): (Path<String>, HttpRequest<AppState>)) -> StringOrStaticFileResult {
fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/cores/{name}/spatial_objects")
.route(web::post().to(post))
.route(web::put().to(put))
.route(web::patch().to(patch))
.route(web::delete().to(delete)),
);
}
const COLLECTION: &str = "/cores/42/spatial_objects";
#[cfg(test)]
mod routing {
use super::super::tests_utils::*;
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn post() {
expect_200(http::Method::POST, get_path(COLLECTION));
json::expect_200(http::Method::POST, get_path(COLLECTION), "".to_string());
expect_200(Method::POST, &get_objects(""));
json::expect_200(Method::POST, &get_objects(""), "".to_string());
json::expect_422(http::Method::POST, get_path(COLLECTION), "".to_string());
json::expect_422(Method::POST, &get_objects(""), "".to_string());
expect_400(http::Method::POST, get_path(COLLECTION));
expect_400(Method::POST, &get_objects(""));
}
#[test]
fn put() {
json::expect_200(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PUT, &get_objects(""), "".to_string());
json::expect_422(http::Method::PUT, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PUT, &get_objects(""), "".to_string());
expect_400(http::Method::PUT, get_path(COLLECTION));
expect_400(Method::PUT, &get_objects(""));
}
#[test]
fn patch() {
json::expect_200(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_200(Method::PATCH, &get_objects(""), "".to_string());
json::expect_422(http::Method::PATCH, get_path(COLLECTION), "".to_string());
json::expect_422(Method::PATCH, &get_objects(""), "".to_string());
expect_400(http::Method::PATCH, get_path(COLLECTION));
expect_400(Method::PATCH, &get_objects(""));
}
#[test]
fn delete() {
json::expect_200(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_200(Method::DELETE, &get_objects(""), "".to_string());
json::expect_422(http::Method::DELETE, get_path(COLLECTION), "".to_string());
json::expect_422(Method::DELETE, &get_objects(""), "".to_string());
expect_400(http::Method::DELETE, get_path(COLLECTION));
expect_400(Method::DELETE, &get_objects(""));
}
#[test]
fn get() {
expect_400(http::Method::GET, get_path(COLLECTION));
expect_405(Method::GET, &get_objects(""));
}
}

View File

@@ -34,13 +34,13 @@ paths:
summary: >
Health check of the service.
description: >
Please note that making anything but a **GET** call is a bad request.
Please note that making anything but a **GET** call is a bad request, and will return a 405.
operationId: get_health_check
responses:
'200':
$ref: '#/responses/Standard200'
default:
$ref: '#/responses/Standard400'
$ref: '#/responses/Standard405'
/queries:
post:
@@ -58,7 +58,7 @@ paths:
'422':
$ref: '#/responses/Standard422'
default:
$ref: '#/responses/Standard400'
$ref: '#/responses/Standard405'
#--------------------------------------------------------------------
# SPACES QUERIES
@@ -748,6 +748,9 @@ responses:
Standard422:
description: >
Unprocessable Entity
Standard405:
description: >
Invalid Method
Standard404:
description: >
Object not found

8
static/errors/405.html Normal file
View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>405 - Method not allowed</title>
</head>
<body>
<h1>405 - Method not allowed</h1>
</body>
</html>