Compare commits

..

17 Commits

Author SHA1 Message Date
fd2b4e098b Upgraded to actix-web 4 2024-08-11 15:19:36 +02:00
e05159c7d7 Specify toolchain version
This prevents compilation issues with newer standard libraries.
2024-08-06 10:58:55 +02:00
1e5b5a8685 Remove .idea folder 2020-04-02 11:13:21 +02:00
72e5296a7d parser module was renamed to mercator_parser 2020-04-01 18:16:33 +02:00
1da288b826 Fix links to grammar definitions 2020-04-01 17:46:07 +02:00
478563f820 Adding documentation 2020-04-01 11:32:45 +02:00
5dc9f13a78 Change timing information level to debug 2020-01-27 13:27:26 +01:00
6532bd5a0d Remove .unwrap() calls, and add more information. 2020-01-20 15:40:00 +01:00
fc84eee48b Prevent unsafe blocks for now 2020-01-20 14:59:39 +01:00
eba485519c Updated imports
* Reorganised so that each module part imports from the mod.rs
   This allows for coherent types within the module to be updated
   simply.
 * Updated imports to the new mercator_db organisation
2020-01-14 18:01:49 +01:00
16ac1ea7d1 Add content-type for responses 2019-11-28 13:42:43 +01:00
d1dffdf890 Quiet some new clippy warnings 2019-11-28 13:42:14 +01:00
e8e50bca8f Enable compression. The client has to request it. 2019-11-08 16:50:19 +01:00
a6865c6cbd Update to new Index API, reduce allocations 2019-10-30 21:33:37 +01:00
7c54dd4fc9 Fix indentation errors in Swagger file 2019-10-23 16:21:30 +02:00
bba41629f4 Removing memory allocations 2019-10-18 20:59:10 +02:00
5cbd2a0b69 Add support for view port & resolution selection. 2019-10-15 20:14:49 +02:00
21 changed files with 1233 additions and 635 deletions

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mercator_service.iml" filepath="$PROJECT_DIR$/.idea/mercator_service.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -18,32 +18,28 @@ license = "MIT"
include = ["Cargo.toml", "README.md", "LICENSE", "ACKNOWLEDGEMENTS", "src/**/*.rs"]
#[profile.release]
#lto = true
[features]
static-error-pages = []
[dependencies]
actix-web = "^1.0"
actix-files = "^0.1"
actix-service = "^0.4"
actix-cors = "^0.1"
glob = "^0.3"
actix-web = "4.8"
actix-files = "0.6"
actix-cors = "0.7"
glob = "0.3"
measure_time = "^0.6"
memmap = "^0.7"
measure_time = "0.8"
memmap = "0.7"
mercator_db = "^0.1"
mercator_parser = "^0.1"
mercator_db = "0.1"
mercator_parser = "0.1"
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
bincode = "^1.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = "1.3"
# Logging macros API
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"
log = { version = "0.4", features = ["max_level_trace", "release_max_level_trace"] }
pretty_env_logger = "0.5" # Logger implementation

115
README.md
View File

@@ -18,18 +18,10 @@ This enables the index implementations to be agnostic from the underlying data s
## Requirements
### Hardware
* **Processor:** XGHz CPU
* **RAM:** Y MB per MB of indexed data
* **Available storage space:** X MB per MB of indexed data
### Software
* Rust: https://www.rust-lang.org
## Quick start
## Building from sources
To build this project, you will need to run the following:
@@ -38,44 +30,107 @@ To build this project, you will need to run the following:
cargo build --release
```
### Installation
## Installation
To install the software on the system you can use:
To install the software on the system, after checking out the
dependencies you can use:
```sh
cargo install --release
cargo install --path .
```
### Usage
## Usage
In order to configure the behavior of the service, there is couple of environment variables:
In order to configure the behavior of the service, there is couple of
environment variables, in bold their default values:
* `RUST_LOG`: Set the level of logging, for example (**error**, **warn**, **info**, **debug**, **trace**). This can be controlled per subsystem of the service, or globally by specifying th subsystem and the level in a list, or omitting the subsystem part. For example:
```sh
RUST_LOG="actix_web=debug,mercator_service=trace" # Set actix_web to debug, mercator_service to trace
RUST_LOG="trace" # Set everything to trace
```
* `RUST_LOG` = `info`:
* `MERCATOR_HOST`: Name or IP address to bind to.
* `MERCATOR_PORT`: Port on which to listen.
* `MERCATOR_BASE`: Prefix du service web.
* `MERCATOR_ALLOWED_ORIGINS`: Allowed origins for CORS requests:
```sh
MERCATOR_ALLOWED_ORIGINS="http://localhost:3200,http://localhost:3201, http://localhost:3202"
```
Set the level of logging, for example (**error**, **warn**, **info**,
**debug**, **trace**). This can be controlled per subsystem of the
service, or globally by specifying th subsystem and the level in a
list, or omitting the subsystem part.
* `MERCATOR_IMPORT_DATA`: Provide the data set to expose.
For example:
```sh
# Set actix_web to debug, mercator_service to trace,
# fall back to info for everything else:
RUST_LOG="info,actix_web=debug,mercator_service=trace"
# Set everything to trace
RUST_LOG="trace"
```
[More details](https://epfl-dias.github.io/mercator_service/env_logger/index.html)
on how to control the logs.
* `MERCATOR_HOST` = **0.0.0.0** :
Name or IP address to bind to.
* `MERCATOR_PORT` = **8888** :
Port on which to listen.
* `MERCATOR_BASE` = **/spatial-search** :
Web service URL prefix.
* `MERCATOR_ALLOWED_ORIGINS` = **http://localhost:3200** :
Allowed origins for CORS requests.
* `MERCATOR_DATA` = **.**:
Provide the root folder of the data sets to expose.
### Example
Complete example of a run:
```sh
RUST_LOG="warn,actix_web=info,mercator_service=trace" MERCATOR_IMPORT_DATA="1000k" MERCATOR_ALLOWED_ORIGINS="http://localhost:3200,http://localhost:3201, http://localhost:3202" cargo run --release
RUST_LOG="warn,actix_web=info,mercator_service=trace" \
MERCATOR_HOST="mercator.example.org" \
MERCATOR_PORT="1234" \
MERCATOR_BASE="/" \
MERCATOR_DATA="../mercator_indexer" \
MERCATOR_ALLOWED_ORIGINS="http://localhost:3200,http://localhost:3201, http://localhost:3202" \
mercator_service
```
## Documentation
For more information, please refer to the [documentation](https://epfl-dias.github.io/mercator_service/).
### User documentation
If you want to build the documentation and access it locally, you can use:
By this, we mean the REST API documentation. To access it and be able
to test it live you can use the following procedure:
1. Install [docker](https://www.docker.com).
2. Start mercator_service, making sure there is the `static` folder,
or a symlink to it, in the current working directory.
3. Start swagger to have access to the live documentation:
```sh
docker run \
--rm \
-p 3200:8080 \
-e API_URL='http://127.0.0.1:8888/spatial-search/static/api/v1.0.yaml' \
swaggerapi/swagger-ui
```
4. Using your web navigator, got to [http://localhost:3200](http://localhost:3200).
5. You have the whole user documentation accessible, and you can
trigger actions on your live mercator_service instance, running
queries on your data.
Otherwise you can read `static/api/v1.0.yaml` directly.
### Developer documentation
For people looking at the internal structure, you can use the
[developer documentation](https://epfl-dias.github.io/mercator_service/).
If you want to build this documentation and access it locally, you can
use:
```sh
cargo doc --open

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "1.80.0"

View File

@@ -1,8 +1,36 @@
#[macro_use]
extern crate measure_time;
#![forbid(unsafe_code)]
#![warn(missing_docs)]
//! # Mercator Service
//!
//! REST-based HTTP service for Mercator.
//!
//! ## Mercator: Spatial Index
//!
//! **Mercator** is a spatial *volumetric* index for the
//! [Human Brain Project]. It is a component of the [Knowledge Graph]
//! service, which provides the spatial anchoring for the metadata
//! registered as well as processes the volumetric queries.
//!
//! It is build on top of the Iron Sea database toolkit.
//!
//! ## Iron Sea: Database Toolkit
//! **Iron Sea** provides a set of database engine bricks, which can be
//! combined and applied on arbitrary data structures.
//!
//! Unlike a traditional database, it does not assume a specific
//! physical structure for the tables nor the records, but relies on the
//! developer to provide a set of extractor functions which are used by
//! the specific indices provided.
//!
//! This enables the index implementations to be agnostic from the
//! underlying data structure, and re-used.
//!
//! [Human Brain Project]: http://www.humanbrainproject.eu
//! [Knowledge Graph]: http://www.humanbrainproject.eu/en/explore-the-brain/search/
#[macro_use]
extern crate serde_derive;
extern crate measure_time;
mod rest_api;
mod shared_state;
@@ -10,11 +38,10 @@ mod shared_state;
use std::process::exit;
use std::sync::RwLock;
use actix_web::web::Data;
use glob::glob;
use mercator_db::json::model;
use mercator_db::DataBase;
use rest_api::Data;
use rest_api::DataBase;
use shared_state::SharedState;
/*
@@ -23,7 +50,8 @@ fn into_bool(string: &str) -> bool {
}
*/
fn main() {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// If RUST_LOG is unset, set it to INFO, otherwise keep it as-is.
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -51,21 +79,17 @@ fn main() {
std::env::set_var("MERCATOR_DATA", ".");
}
let hostname;
let port;
let data;
match std::env::var("MERCATOR_HOST") {
Ok(val) => hostname = val,
let hostname = match std::env::var("MERCATOR_HOST") {
Ok(val) => val,
Err(val) => {
error!("Invalid environment {} : `{}`", "MERCATOR_HOST", val);
exit(1);
}
};
match std::env::var("MERCATOR_PORT") {
let port = match std::env::var("MERCATOR_PORT") {
Ok(val) => match val.parse::<u16>() {
Ok(v) => port = v,
Ok(v) => v,
Err(e) => {
error!("Could not convert to u16 {} : `{}`", "MERCATOR_PORT", e);
exit(1);
@@ -77,8 +101,8 @@ fn main() {
}
};
match std::env::var("MERCATOR_DATA") {
Ok(val) => data = val,
let data = match std::env::var("MERCATOR_DATA") {
Ok(val) => val,
Err(val) => {
error!("Could not fetch {} : `{}`", "MERCATOR_DATA", val);
exit(1);
@@ -96,20 +120,21 @@ fn main() {
})
.collect::<Vec<_>>();
// FIXME: Why do we have to go through a temporary variable?
let datasets = datasets.iter().map(String::as_str).collect::<Vec<_>>();
let db;
// Load a Database:
{
// Load all the index contained in the folder, and fail if anyone of
// those is corrupted / incompatible.
info_time!("Loading database index");
db = DataBase::load(&datasets).unwrap();
db = DataBase::load(&datasets.iter().map(String::as_str).collect::<Vec<_>>())
.unwrap_or_else(|e| panic!("Error while loading indices: {}", e));
}
rest_api::run(
&hostname,
port,
Data::new(RwLock::new(SharedState::new(db))),
);
)
.await
}

View File

@@ -1,21 +1,22 @@
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use actix_web::HttpResponse;
use crate::model::to_spatial_objects;
use crate::shared_state::SharedState;
use serde::Deserialize;
use super::error_422;
use super::from_properties_by_spaces;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Json;
use super::HandlerResult;
use super::HttpResponse;
use super::SharedState;
use mercator_db::CoreQueryParameters;
#[derive(Debug, Deserialize)]
pub struct Query {
query: String,
resolution: Option<Vec<u64>>, // None means automatic selection, based on ViewPort
resolution: Option<Vec<u32>>, // None means automatic selection, based on ViewPort
view_port: Option<(Vec<f64>, Vec<f64>)>,
}
@@ -24,8 +25,8 @@ impl Query {
&self.query
}
pub fn resolution(&self) -> Option<Vec<u64>> {
self.resolution.clone()
pub fn resolution(&self) -> &Option<Vec<u32>> {
&self.resolution
}
pub fn volume(&self) -> Option<f64> {
@@ -36,31 +37,44 @@ impl Query {
}
}
// Also used for the root service.
pub fn health() -> HttpResponse {
pub async fn health() -> HttpResponse {
HttpResponse::Ok().finish()
}
fn query((parameters, state): (Json<Query>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn query((parameters, state): (Json<Query>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("POST '{:?}'", parameters);
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
let query = parameters.query();
if query.is_empty() {
error_422(format!("Invalid query in '{:?}'", query))
} else {
ok_200(
&context
.db()
.core_keys()
.iter()
.flat_map(|core| {
match context.query(query, core, parameters.volume(), parameters.resolution()) {
Err(_) => vec![], // FIXME: Return errors here instead!!
Ok(objects) => to_spatial_objects(context.db(), objects),
}
})
.collect::<Vec<_>>(),
)
let parameters = CoreQueryParameters {
db: context.db(),
output_space: None,
threshold_volume: parameters.volume(),
view_port: &parameters.view_port,
resolution: parameters.resolution(),
};
let results = context
.db()
.core_keys()
.iter()
.filter_map(|core| {
match context.query(query) {
Err(_) => None, // FIXME: Return errors here instead!!
Ok(tree) => match context.execute(&tree, core, &parameters) {
Err(_) => None, // FIXME: Return errors here instead!!
Ok(objects) => Some(from_properties_by_spaces(objects).collect::<Vec<_>>()),
},
}
})
.flatten()
.collect::<Vec<_>>();
ok_200(&results)
}
}
@@ -71,30 +85,41 @@ pub fn config(cfg: &mut web::ServiceConfig) {
#[cfg(test)]
mod routing {
use super::super::tests_utils::*;
use crate::rest_api::tests_utils::*;
use serde_json::json;
#[test]
fn health() {
#[actix_web::test]
async fn health() {
let ep = &get_path("/health");
expect_200(Method::GET, ep);
expect_200(TestRequest::get(), ep).await;
expect_405(Method::POST, ep);
expect_405(Method::PUT, ep);
expect_405(Method::PATCH, ep);
expect_405(Method::DELETE, ep);
expect_405(TestRequest::post(), ep).await;
expect_405(TestRequest::put(), ep).await;
expect_405(TestRequest::patch(), ep).await;
expect_405(TestRequest::delete(), ep).await;
}
#[test]
fn query() {
#[actix_web::test]
async fn query() {
let ep = &get_path("/query");
expect_200(Method::POST, ep);
expect_422(Method::POST, ep);
expect_200(
TestRequest::post()
.set_json(json!({"query": "json(.,inside(hyperrectangle{[0,0,0],[0,1,1]}))"})),
ep,
)
.await;
expect_405(Method::GET, ep);
expect_405(Method::PUT, ep);
expect_405(Method::PATCH, ep);
expect_405(Method::DELETE, ep);
expect_422(TestRequest::post().set_json(json!({"query": "toto"})), ep).await;
expect_422(TestRequest::post().set_json(json!({"query": ""})), ep).await;
expect_400(TestRequest::post().set_json(json!({"invalid": true})), ep).await;
expect_400(TestRequest::post().set_json(json!({})), ep).await;
expect_400(TestRequest::post(), ep).await;
expect_405(TestRequest::get(), ep).await;
expect_405(TestRequest::put(), ep).await;
expect_405(TestRequest::patch(), ep).await;
expect_405(TestRequest::delete(), ep).await;
}
}

View File

@@ -1,39 +1,39 @@
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::web;
use super::web::Data;
use super::web::Path;
use super::Core;
use super::HandlerResult;
use super::SharedState;
fn put(path: Path<String>) -> HandlerResult {
async fn put(path: Path<String>) -> HandlerResult {
trace!("PUT Triggered on {}", path);
error_400()
}
fn get((core, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn get((core, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET '{:?}'", core);
let core = core.to_string();
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
match context.db().core(core) {
match context.db().core(&core) {
Ok(core) => ok_200(&Core::from(core)),
Err(_) => error_404(),
}
}
fn patch(path: Path<String>) -> HandlerResult {
async fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
fn delete(path: Path<String>) -> HandlerResult {
async fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
@@ -57,35 +57,35 @@ mod routing {
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
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());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_core(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::put(), &get_core(INSTANCE_EXISTS), "".to_string()).await;
json::expect_200(TestRequest::put(), &get_core(INSTANCE_INVALID), "".to_string()).await;
}
#[test]
fn patch() {
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));
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_core(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::patch(), &get_core(INSTANCE_EXISTS), "".to_string()).await;
expect_404(TestRequest::patch(), &get_core(INSTANCE_INVALID)).await;
}
#[test]
fn get() {
expect_200(Method::GET, &get_core(INSTANCE_EXISTS));
expect_404(Method::GET, &get_core(INSTANCE_INVALID));
#[actix_web::test]
async fn get() {
expect_200(TestRequest::get(), &get_core(INSTANCE_EXISTS)).await;
expect_404(TestRequest::get(), &get_core(INSTANCE_INVALID)).await;
}
#[test]
fn delete() {
expect_200(Method::DELETE, &get_core(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_core(INSTANCE_INVALID));
#[actix_web::test]
async fn delete() {
expect_200(TestRequest::delete(), &get_core(INSTANCE_EXISTS)).await;
expect_404(TestRequest::delete(), &get_core(INSTANCE_INVALID)).await;
}
#[test]
fn post() {
expect_405(Method::POST, &get_core(INSTANCE_EXISTS));
expect_405(Method::POST, &get_core(INSTANCE_INVALID));
#[actix_web::test]
async fn post() {
expect_405(TestRequest::post(), &get_core(INSTANCE_EXISTS)).await;
expect_405(TestRequest::post(), &get_core(INSTANCE_INVALID)).await;
}
}

View File

@@ -1,22 +1,23 @@
use std::collections::HashSet;
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::error_422;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Json;
use super::Core;
use super::CoreQueryParameters;
use super::Filters;
use super::HandlerResult;
use super::SharedState;
fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("POST '{:?}'", parameters);
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
let db = context.db();
match parameters.space(db) {
@@ -40,17 +41,23 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
}
}
Some(filter) => {
let core_parameters = CoreQueryParameters {
db,
output_space: space.as_ref().map(String::as_str),
threshold_volume: parameters.volume(),
view_port: &parameters.view_port,
resolution: parameters.resolution(),
};
let tree = match context.filter(filter) {
Err(e) => return error_422(e),
Ok(tree) => tree,
};
// Retrieve the list of core ids.
let mut results = HashSet::new();
for core in db.core_keys() {
match context.filter(
filter,
core,
space.clone(),
parameters.volume(),
parameters.resolution(),
) {
match context.execute(&tree, core, &core_parameters) {
Err(e) => return error_422(e),
Ok(objects) => {
// If the list of SpaceObjects is not empty, add
@@ -59,7 +66,7 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
results.insert(core.to_string());
}
}
}
};
}
// Format the list or the whole core objects.
@@ -69,7 +76,7 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
ok_200(
&results
.drain()
.map(|x| Core::from(db.core(x).unwrap()))
.map(|x| Core::from(db.core(&x).unwrap()))
.collect::<Vec<_>>(),
)
}
@@ -79,17 +86,17 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
}
}
fn put() -> HandlerResult {
async fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
fn patch() -> HandlerResult {
async fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
fn delete() -> HandlerResult {
async fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
@@ -108,49 +115,47 @@ pub fn config(cfg: &mut web::ServiceConfig) {
mod routing {
use super::super::tests_utils::*;
const COLLECTION: &str = "/cores";
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn post() {
expect_200(Method::POST, &get_core(""));
json::expect_200(Method::POST, &get_core(""), "".to_string());
#[actix_web::test]
async fn post() {
expect_200(TestRequest::post(), &get_core("")).await;
json::expect_200(TestRequest::post(), &get_core(""), "".to_string()).await;
json::expect_422(Method::POST, &get_core(""), "".to_string());
json::expect_422(TestRequest::post(), &get_core(""), "".to_string()).await;
expect_400(Method::POST, &get_core(""));
expect_400(TestRequest::post(), &get_core("")).await;
}
#[test]
fn put() {
json::expect_200(Method::PUT, &get_core(""), "".to_string());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_core(""), "".to_string()).await;
json::expect_422(Method::PUT, &get_core(""), "".to_string());
json::expect_422(TestRequest::put(), &get_core(""), "".to_string()).await;
expect_400(Method::PUT, &get_core(""));
expect_400(TestRequest::put(), &get_core("")).await;
}
#[test]
fn patch() {
json::expect_200(Method::PATCH, &get_core(""), "".to_string());
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_core(""), "".to_string()).await;
json::expect_422(Method::PATCH, &get_core(""), "".to_string());
json::expect_422(TestRequest::patch(), &get_core(""), "".to_string()).await;
expect_400(Method::PATCH, &get_core(""));
expect_400(TestRequest::patch(), &get_core("")).await;
}
#[test]
fn delete() {
json::expect_200(Method::DELETE, &get_core(""), "".to_string());
#[actix_web::test]
async fn delete() {
json::expect_200(TestRequest::delete(), &get_core(""), "".to_string()).await;
json::expect_422(Method::DELETE, &get_core(""), "".to_string());
json::expect_422(TestRequest::delete(), &get_core(""), "".to_string()).await;
expect_400(Method::DELETE, &get_core(""));
expect_400(TestRequest::delete(), &get_core("")).await;
}
#[test]
fn get() {
expect_405(Method::GET, &get_core(""));
#[actix_web::test]
async fn get() {
expect_405(TestRequest::get(), &get_core("")).await;
}
}

View File

@@ -2,20 +2,26 @@ 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::error_404;
use super::web::Path;
use super::Either;
use super::HandlerResult;
use super::*;
use super::HttpResponse;
use super::NamedFile;
use super::StatusCode;
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))),
Ok(response) => Ok(Either::Left(
HttpResponse::Ok()
.content_type("application/json")
.body(response),
)),
Err(e) => error_500(e),
}
}
@@ -24,7 +30,7 @@ pub fn error_422<S>(reason: S) -> HandlerResult
where
S: Debug,
{
Ok(Either::A(HttpResponse::UnprocessableEntity().body(
Ok(Either::Left(HttpResponse::UnprocessableEntity().body(
format!("422 - Unprocessable Entity:\n{:?}", reason),
)))
}
@@ -44,7 +50,7 @@ where
// error_400()
//}
pub fn page_404() -> HandlerResult {
pub async fn page_404() -> HandlerResult {
trace!("404 Triggered!");
error_404()
}
@@ -54,7 +60,7 @@ pub fn page_404() -> HandlerResult {
// error_405()
//}
pub fn api(path: Path<String>) -> Result<NamedFile, Error> {
pub async fn api(path: Path<String>) -> Result<NamedFile, Error> {
trace!("api/{} Triggered!", path);
match NamedFile::open(format!("static/api/{}", path).as_str()) {
@@ -65,7 +71,7 @@ pub fn api(path: Path<String>) -> Result<NamedFile, Error> {
}
}
pub fn static_file(path: Path<String>) -> Result<NamedFile, Error> {
pub async fn static_file(path: Path<String>) -> Result<NamedFile, Error> {
trace!("static/{} Triggered!", path);
match NamedFile::open(format!("static/{}", path).as_str()) {
@@ -79,10 +85,9 @@ pub fn static_file(path: Path<String>) -> Result<NamedFile, Error> {
#[cfg(test)]
mod tests {
use super::super::tests_utils::*;
use super::*;
#[test]
fn page_400() {
// expect_400(Method::PATCH, get_core(INSTANCE_INVALID));
#[actix_web::test]
async fn page_400() {
expect_400(TestRequest::patch(), &get_core(INVALID_CORE)).await;
}
}

View File

@@ -7,7 +7,7 @@ use actix_web::HttpResponse;
use super::HandlerResult;
fn error(code: StatusCode) -> HandlerResult {
Ok(Either::A(HttpResponse::build(code).finish()))
Ok(Either::Left(HttpResponse::build(code).finish()))
}
pub fn error_400() -> HandlerResult {

View File

@@ -9,7 +9,7 @@ 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)))
Ok(Either::Right(NamedFile::open(path)?.set_status_code(code)))
}
pub fn error_400() -> HandlerResult {

View File

@@ -23,22 +23,30 @@ use actix_web::http;
use actix_web::http::StatusCode;
use actix_web::middleware;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Path;
pub use actix_web::web::Data;
use actix_web::App;
use actix_web::Either;
use actix_web::HttpResponse;
use actix_web::HttpServer;
use mercator_db::space::Shape;
use mercator_db::storage::model;
use mercator_db::storage::model::v2::from_properties_by_spaces;
use mercator_db::storage::model::v2::from_spaces_by_properties;
use mercator_db::CoreQueryParameters;
pub use mercator_db::DataBase;
use mercator_db::Properties;
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "static-error-pages")]
pub use helpers_static_pages::*;
use crate::SharedState;
pub use helpers::*;
#[cfg(not(feature = "static-error-pages"))]
pub use helpers_dynamic_pages::*;
pub use helpers::*;
use crate::SharedState;
#[cfg(feature = "static-error-pages")]
pub use helpers_static_pages::*;
pub type HandlerResult = Result<Either<HttpResponse, NamedFile>, Error>;
@@ -47,7 +55,7 @@ pub struct Filters {
filters: Option<String>,
ids_only: Option<bool>,
space: Option<String>, // Output space, None, means each object in its own original space
resolution: Option<Vec<u64>>, // None means automatic selection, based on ViewPort
resolution: Option<Vec<u32>>, // None means automatic selection, based on ViewPort
view_port: Option<(Vec<f64>, Vec<f64>)>,
}
@@ -57,13 +65,10 @@ impl Filters {
}
pub fn ids_only(&self) -> bool {
match self.ids_only {
None => true, // Defaults to true
Some(b) => b,
}
self.ids_only.unwrap_or(true)
}
pub fn space(&self, db: &mercator_db::DataBase) -> Result<Option<String>, HandlerResult> {
pub fn space(&self, db: &DataBase) -> Result<&Option<String>, HandlerResult> {
if let Some(space_id) = &self.space {
if !db.space_keys().contains(&space_id.to_string()) {
return Err(error_422(format!(
@@ -72,18 +77,17 @@ impl Filters {
)));
}
}
Ok(self.space.clone())
Ok(&self.space)
}
pub fn resolution(&self) -> Option<Vec<u64>> {
self.resolution.clone()
pub fn resolution(&self) -> &Option<Vec<u32>> {
&self.resolution
}
pub fn volume(&self) -> Option<f64> {
match &self.view_port {
None => None,
Some(_view) => None, // FIXME: Need to move the Volume functions from mercator_parser to mercator_db.
}
self.view_port.as_ref().map(|(low, high)|
Shape::BoundingBox(low.into(), high.into()).volume()
)
}
}
@@ -107,7 +111,7 @@ impl From<&mercator_db::Core> for Core {
}
// From: https://stackoverflow.com/a/52367953
fn into_static<S>(s: S) -> &'static str
pub fn into_static<S>(s: S) -> &'static str
where
S: Into<String>,
{
@@ -135,10 +139,8 @@ fn config_v1(cfg: &mut web::ServiceConfig) {
}
pub fn config(cfg: &mut web::ServiceConfig) {
let prefix;
match std::env::var("MERCATOR_BASE") {
Ok(val) => prefix = val,
let prefix = match std::env::var("MERCATOR_BASE") {
Ok(val) => val,
Err(val) => {
error!("Could not fetch {} : `{}`", "MERCATOR_BASE", val);
exit(1);
@@ -153,7 +155,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
pub fn get_cors() -> Cors {
// Setup CORS support.
let mut cors = Cors::new();
let mut cors = Cors::default();
match std::env::var("MERCATOR_ALLOWED_ORIGINS") {
Ok(val) => {
@@ -182,61 +184,42 @@ pub fn get_cors() -> Cors {
macro_rules! get_app {
($state:expr) => {
App::new()
.register_data($state.clone())
.app_data($state.clone())
.wrap(middleware::Logger::new(
r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T[s] %D[ms]"#,
))
.wrap(middleware::Compress::default())
.wrap(get_cors())
.configure(config)
.default_service(
web::resource("/")
// 404 for GET request
.route(web::to(page_404)),
)
.default_service(web::to(page_404))
};
}
pub fn run(host: &str, port: u16, state: Data<RwLock<SharedState>>) {
pub async fn run(host: &str, port: u16, state: Data<RwLock<SharedState>>) -> std::io::Result<()> {
info!("Starting http server: {}:{}", host, port);
// Create & run the server.
match HttpServer::new(move || get_app!(state))
.bind(format!("{}:{}", host, port))
.unwrap()
HttpServer::new(move || get_app!(state))
.bind(format!("{}:{}", host, port))?
.run()
{
Ok(_) => info!("Server Stopped!"),
Err(e) => error!("Error running the server: {}", e),
};
.await
}
#[cfg(test)]
mod tests_utils {
use super::*;
//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 use actix_web::test::TestRequest;
pub const CORE_ID: &str = "10k";
pub const CORE_FILE: &str = "10k.index";
pub const CORE_ID: [&str; 1] = ["10k"];
pub const PREFIX: &str = "/spatial-search";
pub const PREFIX: &str = "/spatial-search-test";
pub const CORE: &str = "/10k";
pub const INVALID_CORE: &str = "/INVALID_CORE";
pub const SPACE: &str = "/std";
pub const SPATIAL_OBJECT: &str = "/oid0.44050628835072825";
pub enum Method {
GET,
POST,
PUT,
PATCH,
DELETE,
}
pub fn get_path(path: &str) -> String {
format!("{}{}", PREFIX, path)
@@ -254,113 +237,113 @@ mod tests_utils {
format!("{}{}{}", get_core(CORE), "/spatial_objects", name)
}
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(),
macro_rules! expect_code {
($request:expr, $path:expr, $code:expr) => {
{
std::env::set_var("MERCATOR_BASE", PREFIX);
let db = DataBase::load(&[CORE_FILE]).unwrap();
let app = test::init_service(
get_app!(Data::new(RwLock::new(SharedState::new(db))))).await;
let request = $request.uri(&$path).to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), $code);
// let json = test::read_body(response).await;
// println!("BODY: {:?}", json);
}
};
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);
/// Checks status code OK
pub async fn expect_200(method: TestRequest, path: &str) {
expect_code!(method, path, StatusCode::OK);
}
pub fn expect_400(method: Method, path: &str) {
expect(method, path, http::StatusCode::BAD_REQUEST);
/// Checks status code BAD_REQUEST
pub async fn expect_400(method: TestRequest, path: &str) {
expect_code!(method, path, StatusCode::BAD_REQUEST);
}
pub fn expect_404(method: Method, path: &str) {
expect(method, path, http::StatusCode::NOT_FOUND);
/// Checks status code NOT_FOUND
pub async fn expect_404(method: TestRequest, path: &str) {
expect_code!(method, path, StatusCode::NOT_FOUND);
}
pub fn expect_405(method: Method, path: &str) {
expect(method, path, http::StatusCode::METHOD_NOT_ALLOWED);
/// Checks status code METHOD_NOT_ALLOWED
pub async fn expect_405(method: TestRequest, path: &str) {
expect_code!(method, path, StatusCode::METHOD_NOT_ALLOWED);
}
pub fn expect_422(method: Method, path: &str) {
expect(method, path, http::StatusCode::UNPROCESSABLE_ENTITY);
/// Checks status code UNPROCESSABLE_ENTITY
pub async fn expect_422(method: TestRequest, path: &str) {
expect_code!(method, path, StatusCode::UNPROCESSABLE_ENTITY);
}
pub mod json {
use super::*;
pub fn expect_200(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::OK);
pub async fn expect_200(method: TestRequest, path: &str, _json: String) {
expect_code!(method, path, StatusCode::OK);
}
pub fn expect_404(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::NOT_FOUND);
pub async fn expect_404(method: TestRequest, path: &str, _json: String) {
expect_code!(method, path, StatusCode::NOT_FOUND);
}
pub fn expect_422(method: Method, path: &str, json: String) {
expect(method, path, http::StatusCode::UNPROCESSABLE_ENTITY);
pub async fn expect_422(method: TestRequest, path: &str, _json: String) {
expect_code!(method, path, StatusCode::UNPROCESSABLE_ENTITY);
}
}
}
#[cfg(test)]
mod routing {
use super::tests_utils::*;
use std::panic;
use super::tests_utils::*;
#[test]
fn default_no_path() {
#[ignore] // Don't know how to make work the catch_unwind in an async context
#[actix_web::test]
async 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, "");
//expect_404(TestRequest::get(), "").await;
});
assert!(result.is_err());
}
#[test]
fn default_slash() {
#[actix_web::test]
async 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");
expect_404(TestRequest::get(), "/").await;
expect_404(TestRequest::get(), "//").await;
expect_404(TestRequest::get(), "/%20/").await;
expect_404(TestRequest::get(), "/%20//").await;
expect_404(TestRequest::get(), "//%20").await;
}
#[test]
fn default_invalid_prefix() {
expect_404(Method::GET, "/test");
expect_404(Method::GET, &format!("{}test", PREFIX));
#[actix_web::test]
async fn default_invalid_prefix() {
expect_404(TestRequest::get(), "/test").await;
expect_404(TestRequest::get(), &format!("{}test", PREFIX)).await;
}
#[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);
#[actix_web::test]
async fn default_prefix_no_slash() {
expect_404(TestRequest::put(), PREFIX).await;
expect_404(TestRequest::get(), PREFIX).await;
expect_404(TestRequest::post(), PREFIX).await;
expect_404(TestRequest::patch(), PREFIX).await;
expect_404(TestRequest::delete(), PREFIX).await;
}
#[test]
fn default_prefix_final_slash() {
#[actix_web::test]
async 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);
expect_404(TestRequest::put(), path).await;
expect_404(TestRequest::get(), path).await;
expect_404(TestRequest::post(), path).await;
expect_404(TestRequest::patch(), path).await;
expect_404(TestRequest::delete(), path).await;
}
}

View File

@@ -1,28 +1,28 @@
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::model;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Path;
use super::HandlerResult;
use super::SharedState;
fn put(path: Path<String>) -> HandlerResult {
async fn put(path: Path<String>) -> HandlerResult {
trace!("POST '{:?}'", path);
error_400()
}
fn get((path, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn get((path, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET '{:?}'", path);
let name = path.to_string();
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
match context.db().space(name) {
match context.db().space(&name) {
Ok(space) => {
let space: model::Space = space.into();
ok_200(&space)
@@ -31,12 +31,12 @@ fn get((path, state): (Path<String>, Data<RwLock<SharedState>>)) -> HandlerResul
}
}
fn patch(path: Path<String>) -> HandlerResult {
async fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
fn delete(path: Path<String>) -> HandlerResult {
async fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
@@ -60,35 +60,35 @@ mod routing {
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
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());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_space(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::put(), &get_space(INSTANCE_EXISTS), "".to_string()).await;
json::expect_200(TestRequest::put(), &get_space(INSTANCE_INVALID), "".to_string()).await;
}
#[test]
fn patch() {
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));
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_space(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::patch(), &get_space(INSTANCE_EXISTS), "".to_string()).await;
expect_400(TestRequest::patch(), &get_space(INSTANCE_INVALID)).await;
}
#[test]
fn get() {
expect_200(Method::GET, &get_space(INSTANCE_EXISTS));
expect_404(Method::GET, &get_space(INSTANCE_INVALID));
#[actix_web::test]
async fn get() {
expect_200(TestRequest::get(), &get_space(INSTANCE_EXISTS)).await;
expect_404(TestRequest::get(), &get_space(INSTANCE_INVALID)).await;
}
#[test]
fn delete() {
expect_200(Method::DELETE, &get_space(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_space(INSTANCE_INVALID));
#[actix_web::test]
async fn delete() {
expect_200(TestRequest::delete(), &get_space(INSTANCE_EXISTS)).await;
expect_404(TestRequest::delete(), &get_space(INSTANCE_INVALID)).await;
}
#[test]
fn post() {
expect_405(Method::POST, &get_space(INSTANCE_EXISTS));
expect_405(Method::POST, &get_space(INSTANCE_INVALID));
#[actix_web::test]
async fn post() {
expect_405(TestRequest::post(), &get_space(INSTANCE_EXISTS)).await;
expect_405(TestRequest::post(), &get_space(INSTANCE_INVALID)).await;
}
}

View File

@@ -1,22 +1,23 @@
use std::collections::HashSet;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use crate::model;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_422;
use super::model;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Json;
use super::CoreQueryParameters;
use super::Filters;
use super::HandlerResult;
use super::SharedState;
fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("POST '{:?}'", parameters);
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
let db = context.db();
match parameters.space(db) {
@@ -43,20 +44,26 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
// Retrieve the list of space ids.
let mut results = HashSet::new();
let core_parameters = CoreQueryParameters {
db: context.db(),
output_space: space.as_ref().map(String::as_str),
threshold_volume: parameters.volume(),
view_port: &parameters.view_port,
resolution: parameters.resolution(),
};
let tree = match context.filter(filter) {
Err(e) => return error_422(e),
Ok(bag) => bag,
};
for core in db.core_keys() {
match context.filter(
filter,
core,
space.clone(),
parameters.volume(),
parameters.resolution(),
) {
match context.execute(&tree, core, &core_parameters) {
Err(e) => return error_422(e),
Ok(objects) => {
Ok(v) => {
// We have a list of SpaceObjects, so extract
// the space Ids
for o in objects {
results.insert(o.space_id);
for (space_id, _) in v {
results.insert(space_id);
}
}
}
@@ -82,17 +89,17 @@ fn post((parameters, state): (Json<Filters>, Data<RwLock<SharedState>>)) -> Hand
}
}
fn put() -> HandlerResult {
async fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
fn patch() -> HandlerResult {
async fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
fn delete() -> HandlerResult {
async fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
@@ -113,45 +120,45 @@ mod routing {
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn post() {
expect_200(Method::POST, &get_space(""));
json::expect_200(Method::POST, &get_space(""), "".to_string());
#[actix_web::test]
async fn post() {
expect_200(TestRequest::post(), &get_space("")).await;
json::expect_200(TestRequest::post(), &get_space(""), "".to_string()).await;
json::expect_422(Method::POST, &get_space(""), "".to_string());
json::expect_422(TestRequest::post(), &get_space(""), "".to_string()).await;
expect_400(Method::POST, &get_space(""));
expect_400(TestRequest::post(), &get_space("")).await;
}
#[test]
fn put() {
json::expect_200(Method::PUT, &get_space(""), "".to_string());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_space(""), "".to_string()).await;
json::expect_422(Method::PUT, &get_space(""), "".to_string());
json::expect_422(TestRequest::put(), &get_space(""), "".to_string()).await;
expect_400(Method::PUT, &get_space(""));
expect_400(TestRequest::put(), &get_space("")).await;
}
#[test]
fn patch() {
json::expect_200(Method::PATCH, &get_space(""), "".to_string());
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_space(""), "".to_string()).await;
json::expect_422(Method::PATCH, &get_space(""), "".to_string());
json::expect_422(TestRequest::patch(), &get_space(""), "".to_string()).await;
expect_400(Method::PATCH, &get_space(""));
expect_400(TestRequest::patch(), &get_space("")).await;
}
#[test]
fn delete() {
json::expect_200(Method::DELETE, &get_space(""), "".to_string());
#[actix_web::test]
async fn delete() {
json::expect_200(TestRequest::delete(), &get_space(""), "".to_string()).await;
json::expect_422(Method::DELETE, &get_space(""), "".to_string());
json::expect_422(TestRequest::delete(), &get_space(""), "".to_string()).await;
expect_400(Method::DELETE, &get_space(""));
expect_400(TestRequest::delete(), &get_space("")).await;
}
#[test]
fn get() {
expect_405(Method::GET, &get_space(""));
#[actix_web::test]
async fn get() {
expect_405(TestRequest::get(), &get_space("")).await;
}
}

View File

@@ -1,43 +1,56 @@
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Path;
use mercator_db::CoreQueryParameters;
use crate::model::to_spatial_objects;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::from_properties_by_spaces;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Path;
use super::CoreQueryParameters;
use super::HandlerResult;
use super::Properties;
use super::SharedState;
use mercator_db::{IterObjects, IterObjectsBySpaces};
fn put(path: Path<String>) -> HandlerResult {
async fn put(path: Path<String>) -> HandlerResult {
trace!("PUT '{:?}'", path);
error_400()
}
fn get((path, state): (Path<(String, String)>, Data<RwLock<SharedState>>)) -> HandlerResult {
async fn get((path, state): (Path<(String, String)>, Data<RwLock<SharedState>>)) -> HandlerResult {
trace!("GET '{:?}'", path);
let (core, id) = path.into_inner();
let core = core.to_string();
let id = id.to_string();
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
let db = context.db();
// FIXME: Should we allow setting the resolution/threshold_volume?
let parameters = CoreQueryParameters {
db,
output_space: None,
threshold_volume: Some(0.0), // Empty volume => Highest resolution possible
resolution: None,
// Enforce highest resolution index.
threshold_volume: None,
view_port: &None,
resolution: &Some(vec![0]),
};
match db.core(core) {
match db.core(&core) {
Ok(core) => match core.get_by_id(&parameters, &id) {
Ok(objects) => {
let results = to_spatial_objects(db, objects);
Ok(positions_by_spaces) => {
let value = Properties::Feature(id);
let tmp: IterObjectsBySpaces = positions_by_spaces
.into_iter()
.map(|(space, positions)| {
let objects: IterObjects =
Box::new(positions.map(|position| (position, &value)));
(space, objects)
})
.collect();
let results = from_properties_by_spaces(tmp).collect::<Vec<_>>();
if results.is_empty() {
error_404()
} else {
@@ -50,12 +63,12 @@ fn get((path, state): (Path<(String, String)>, Data<RwLock<SharedState>>)) -> Ha
}
}
fn patch(path: Path<String>) -> HandlerResult {
async fn patch(path: Path<String>) -> HandlerResult {
trace!("PATCH Triggered on {}", path);
error_400()
}
fn delete(path: Path<String>) -> HandlerResult {
async fn delete(path: Path<String>) -> HandlerResult {
trace!("DELETE Triggered on {}", path);
error_400()
}
@@ -79,35 +92,35 @@ mod routing {
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn put() {
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());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_objects(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::put(), &get_objects(INSTANCE_EXISTS), "".to_string()).await;
json::expect_200(TestRequest::put(), &get_objects(INSTANCE_INVALID), "".to_string()).await;
}
#[test]
fn patch() {
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));
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_objects(INSTANCE_EXISTS), "".to_string()).await;
json::expect_422(TestRequest::patch(), &get_objects(INSTANCE_EXISTS), "".to_string()).await;
expect_400(TestRequest::patch(), &get_objects(INSTANCE_INVALID)).await;
}
#[test]
fn get() {
expect_200(Method::GET, &get_objects(INSTANCE_EXISTS));
expect_404(Method::GET, &get_objects(INSTANCE_INVALID));
#[actix_web::test]
async fn get() {
expect_200(TestRequest::get(), &get_objects(INSTANCE_EXISTS)).await;
expect_404(TestRequest::get(), &get_objects(INSTANCE_INVALID)).await;
}
#[test]
fn delete() {
expect_200(Method::DELETE, &get_objects(INSTANCE_EXISTS));
expect_404(Method::DELETE, &get_objects(INSTANCE_INVALID));
#[actix_web::test]
async fn delete() {
expect_200(TestRequest::delete(), &get_objects(INSTANCE_EXISTS)).await;
expect_404(TestRequest::delete(), &get_objects(INSTANCE_INVALID)).await;
}
#[test]
fn post() {
expect_405(Method::POST, &get_objects(INSTANCE_EXISTS));
expect_405(Method::POST, &get_objects(INSTANCE_INVALID));
#[actix_web::test]
async fn post() {
expect_405(TestRequest::post(), &get_objects(INSTANCE_EXISTS)).await;
expect_405(TestRequest::post(), &get_objects(INSTANCE_INVALID)).await;
}
}

View File

@@ -1,104 +1,117 @@
use std::collections::HashSet;
use std::sync::RwLock;
use actix_web::web;
use actix_web::web::Data;
use actix_web::web::Json;
use actix_web::web::Path;
use mercator_db::CoreQueryParameters;
use crate::model::to_spatial_objects;
use crate::shared_state::SharedState;
use super::error_400;
use super::error_404;
use super::error_422;
use super::from_properties_by_spaces;
use super::from_spaces_by_properties;
use super::ok_200;
use super::web;
use super::web::Data;
use super::web::Json;
use super::web::Path;
use super::CoreQueryParameters;
use super::Filters;
use super::HandlerResult;
use super::SharedState;
fn post(
async fn post(
(core_id, parameters, state): (Path<String>, Json<Filters>, Data<RwLock<SharedState>>),
) -> HandlerResult {
trace!("POST '{:?}', {:?}", parameters, core_id);
let core_id = core_id.to_string();
let context = state.read().unwrap();
let context = state
.read()
.unwrap_or_else(|e| panic!("Can't acquire read lock of the database: {}", e));
let db = context.db();
match db.core(core_id.clone()) {
match db.core(&core_id) {
Err(_) => error_404(),
Ok(core) => match parameters.space(db) {
Err(e) => e,
Ok(space) => match parameters.filters() {
None => {
let mut results = HashSet::new();
for property in core.keys().iter() {
results.insert(property.id().clone());
}
if parameters.ids_only() {
ok_200(&results.drain().collect::<Vec<_>>())
// keys() contains unique values only.
let ids = core
.keys()
.iter()
.map(|properties| properties.id())
.collect::<Vec<_>>();
ok_200(&ids)
} else {
let core_parameters = CoreQueryParameters {
db,
output_space: space.as_ref().map(String::as_str),
threshold_volume: parameters.volume(),
view_port: &parameters.view_port,
resolution: parameters.resolution(),
};
let objects = results
.drain()
.flat_map(|id| match core.get_by_id(&core_parameters, id) {
Err(_) => vec![], // FIXME: Return error ?
Ok(r) => r,
})
.collect::<Vec<_>>();
let objects = to_spatial_objects(db, objects);
ok_200(&objects)
let objects_by_spaces =
Box::new(core.keys().iter().filter_map(|property| {
match core.get_by_id(&core_parameters, property.id()) {
Err(_) => None, // FIXME: Return error ?
Ok(positions_by_spaces) => {
Some((property, positions_by_spaces))
}
}
}));
ok_200(&from_spaces_by_properties(objects_by_spaces).collect::<Vec<_>>())
}
}
Some(filter) => {
match context.filter(
filter,
&core_id,
space,
parameters.volume(),
parameters.resolution(),
) {
let core_parameters = CoreQueryParameters {
db,
output_space: space.as_ref().map(String::as_str),
threshold_volume: parameters.volume(),
view_port: &parameters.view_port,
resolution: parameters.resolution(),
};
let tree = match context.filter(filter) {
Err(e) => return error_422(e),
Ok(bag) => bag,
};
let r = match context.execute(&tree, &core_id, &core_parameters) {
Err(e) => error_422(e),
Ok(objects) => {
if parameters.ids_only() {
let mut uniques = HashSet::new();
for o in objects.iter() {
uniques.insert(o.value.id().clone());
for (_, v) in objects {
for (_, properties) in v {
uniques.insert(properties.id());
}
}
ok_200(&uniques.drain().collect::<Vec<_>>())
} else {
let objects = to_spatial_objects(db, objects);
ok_200(&objects)
ok_200(&from_properties_by_spaces(objects).collect::<Vec<_>>())
}
}
}
};
r
}
},
},
}
}
fn put() -> HandlerResult {
async fn put() -> HandlerResult {
trace!("PUT Triggered!");
error_400()
}
fn patch() -> HandlerResult {
async fn patch() -> HandlerResult {
trace!("PATCH Triggered!");
error_400()
}
fn delete() -> HandlerResult {
async fn delete() -> HandlerResult {
trace!("DELETE Triggered!");
error_400()
}
@@ -119,45 +132,45 @@ mod routing {
// FIXME: Add Body to request to see difference between (in)valid bodied requests
#[test]
fn post() {
expect_200(Method::POST, &get_objects(""));
json::expect_200(Method::POST, &get_objects(""), "".to_string());
#[actix_web::test]
async fn post() {
expect_200(TestRequest::post(), &get_objects("")).await;
json::expect_200(TestRequest::post(), &get_objects(""), "".to_string()).await;
json::expect_422(Method::POST, &get_objects(""), "".to_string());
json::expect_422(TestRequest::post(), &get_objects(""), "".to_string()).await;
expect_400(Method::POST, &get_objects(""));
expect_400(TestRequest::post(), &get_objects("")).await;
}
#[test]
fn put() {
json::expect_200(Method::PUT, &get_objects(""), "".to_string());
#[actix_web::test]
async fn put() {
json::expect_200(TestRequest::put(), &get_objects(""), "".to_string()).await;
json::expect_422(Method::PUT, &get_objects(""), "".to_string());
json::expect_422(TestRequest::put(), &get_objects(""), "".to_string()).await;
expect_400(Method::PUT, &get_objects(""));
expect_400(TestRequest::put(), &get_objects("")).await;
}
#[test]
fn patch() {
json::expect_200(Method::PATCH, &get_objects(""), "".to_string());
#[actix_web::test]
async fn patch() {
json::expect_200(TestRequest::patch(), &get_objects(""), "".to_string()).await;
json::expect_422(Method::PATCH, &get_objects(""), "".to_string());
json::expect_422(TestRequest::patch(), &get_objects(""), "".to_string()).await;
expect_400(Method::PATCH, &get_objects(""));
expect_400(TestRequest::patch(), &get_objects("")).await;
}
#[test]
fn delete() {
json::expect_200(Method::DELETE, &get_objects(""), "".to_string());
#[actix_web::test]
async fn delete() {
json::expect_200(TestRequest::delete(), &get_objects(""), "".to_string()).await;
json::expect_422(Method::DELETE, &get_objects(""), "".to_string());
json::expect_422(TestRequest::delete(), &get_objects(""), "".to_string()).await;
expect_400(Method::DELETE, &get_objects(""));
expect_400(TestRequest::delete(), &get_objects("")).await;
}
#[test]
fn get() {
expect_405(Method::GET, &get_objects(""));
#[actix_web::test]
async fn get() {
expect_405(TestRequest::get(), &get_objects("")).await;
}
}

View File

@@ -1,9 +1,11 @@
use mercator_db::CoreQueryParameters;
use mercator_db::DataBase;
use parser::Executor;
use parser::FiltersParser;
use parser::QueryParser;
use parser::Validator;
use mercator_parser::Bag;
use mercator_parser::Executor;
use mercator_parser::FiltersParser;
use mercator_parser::Projection;
use mercator_parser::QueryParser;
use mercator_parser::Validator;
pub struct SharedState {
db: DataBase,
@@ -32,119 +34,82 @@ impl SharedState {
&self.query_parser
}
pub fn filter(
&self,
input: &str,
core: &str,
output_space: Option<String>,
volume: Option<f64>,
resolution: Option<Vec<u64>>,
) -> mercator_db::ResultSet {
pub fn execute<'e, T>(
&'e self,
tree: &'e T, //&'e Bag,
core: &'e str,
parameters: &'e CoreQueryParameters<'e>,
) -> mercator_db::ResultSet<'e>
where
T: Executor<'e, ResultSet = mercator_db::ResultSet<'e>>,
{
// Execute filter.
let execution = {
info_time!("Execution");
// _FIXME: Output space is defined as part of the projection
// and is ignored by projections operators.
tree.execute(core, parameters)
};
match execution {
Err(e) => {
debug!("Execution failed: \n{:?}", e);
Err(e)
}
results @ Ok(_) => results,
}
}
pub fn filter<'q>(&'q self, filter: &'q str) -> Result<Bag, String> {
let parser = self.filter_parser();
let parse;
let parameters = CoreQueryParameters {
db: self.db(),
output_space: output_space.as_ref().map(String::as_str),
threshold_volume: volume,
resolution,
};
// Parse Input
{
info_time!("Parsing");
parse = parser.parse(input);
debug_time!("Parsing");
parse = parser.parse(filter);
}
match parse {
Err(e) => {
debug!("Parsing failed: \n{:?}", e);
Err(format!("{}", e))
}
Ok(tree) => {
let validation;
let execution;
// Check type coherence & validate tree
{
info_time!("Type check");
validation = tree.validate();
}
if validation.is_err() {
debug!("Type check failed");
return Err("Type check failed".to_string());
debug_time!("Type check");
let _ = tree.validate()?;
}
// Execute filter.
{
info_time!("Execution");
execution = tree.execute(core, &parameters);
}
match execution {
Err(e) => {
debug!("Parsing failed: \n{:?}", e);
Err(e.to_string())
}
results @ Ok(_) => results,
}
Ok(tree)
}
}
}
pub fn query(
&self,
input: &str,
core: &str,
volume: Option<f64>,
resolution: Option<Vec<u64>>,
) -> mercator_db::ResultSet {
pub fn query(&self, query: &str) -> Result<Projection, String> {
let parser = self.query_parser();
let parse;
let parameters = CoreQueryParameters {
db: self.db(),
output_space: None,
threshold_volume: volume,
resolution,
};
// Parse Input
{
info_time!("Parsing");
parse = parser.parse(input);
debug_time!("Parsing");
parse = parser.parse(query);
}
match parse {
Err(e) => {
debug!("Parsing failed: \n{:?}", e);
Err(e.to_string())
}
Ok(None) => Ok(vec![]),
Ok(None) => Err("Query is empty!".to_string()),
Ok(Some(tree)) => {
let validation;
let execution;
// Check type coherence & validate tree
{
info_time!("Type check");
validation = tree.validate();
}
if validation.is_err() {
debug!("Type check failed");
return Err("Type check failed".to_string());
debug_time!("Type check");
let _ = tree.validate()?;
}
// Execute filter.
{
info_time!("Execution");
// _FIXME: Output space is defined as part of the projection
// and is ignored by projections operators.
execution = tree.execute(core, &parameters);
}
match execution {
Err(e) => {
debug!("Parsing failed: \n{:?}", e);
Err(e.to_string())
}
results @ Ok(_) => results,
}
Ok(tree)
}
}
}

View File

@@ -115,7 +115,7 @@ paths:
delete:
tags: [Spaces]
summary: >
Delete mulltiple spaces at a time.
Delete multiple spaces at a time.
Each reference space can only be removed if and only if there
are no spatial objects left referencing it.
@@ -249,7 +249,7 @@ paths:
delete:
tags: [Cores]
summary: >
Delete mulltiple Cores at a time. This also removes all the
Delete multiple Cores at a time. This also removes all the
Spatial Objects tied to these cores.
operationId: delete_cores
parameters:
@@ -401,7 +401,7 @@ paths:
delete:
tags: [Spatial Objects]
summary: >
Delete mulltiple spatial objects at a time.
Delete multiple spatial objects at a time.
operationId: delete_spatial_objects
parameters:
- $ref: '#/parameters/SpatialObjectIds'
@@ -682,6 +682,20 @@ parameters:
ids_only:
type: boolean
default: false
space:
type: string
resolution:
type: array
items:
type: number
minimum: 0
format: int32
view_port:
type: array
items:
type: array
items:
type: number
Query:
name: query
@@ -690,7 +704,22 @@ parameters:
description: >
For more about the query syntax, please refer to `FIXME: URL` http://repo/queries.g4.
schema:
type: string
type: object
properties:
query:
type: string
resolution:
type: array
items:
type: number
minimum: 0
format: int32
view_port:
type: array
items:
type: array
items:
type: number
responses:
Space200:

503
static/api/v1.0.yaml Normal file
View File

@@ -0,0 +1,503 @@
openapi: "3.0.0"
info:
title: Spatial Search Backend API Documentation
description: API Documentation for the spatial search backend.
termsOfService: "" #urn:tos FIXME: Describe ToS?
license:
name: The MIT License
url: https://opensource.org/licenses/MIT
version: "1.0"
servers:
- url: http://127.0.0.1:8888/spatial-search
tags:
- name: Actions
description: General Database actions.
- name: Spaces
description: Operations on Reference Spaces.
- name: Cores
description: Operations on Cores.
- name: Spatial Objects
description: Operations on Spatial Objects.
paths:
#--------------------------------------------------------------------
# GENERAL ACTIONS ON THE SYSTEM
#--------------------------------------------------------------------
/health:
get:
tags: [Actions]
summary: >
Health check of the service.
description: >
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: '#/components/responses/Standard200'
default:
$ref: '#/components/responses/Standard405'
/query:
post:
tags: [Actions]
summary: >
Execute an arbitrary query.
#description: >
# This is a POST operation, as it "creates" and execute a query, and it is not idempotent as the same query re-run multiple times might have different results, depending on the state of the database.
operationId: query
requestBody:
$ref: '#/components/requestBodies/Query'
responses:
'200':
$ref: '#/components/responses/Query200'
'422':
$ref: '#/components/responses/Standard422'
default:
$ref: '#/components/responses/Standard405'
#--------------------------------------------------------------------
# SPACES QUERIES
#--------------------------------------------------------------------
/spaces:
post:
tags: [Spaces]
summary: >
Retrieve a list of space definition names.
operationId: post_spaces
requestBody:
$ref: '#/components/requestBodies/Filters'
responses:
'200':
$ref: '#/components/responses/ArrayOfStrings'
'422':
$ref: '#/components/responses/Standard422'
default:
$ref: '#/components/responses/Standard400'
/spaces/{name}:
parameters:
- $ref: '#/components/parameters/SpaceName'
get:
tags: [Spaces]
summary: >
Retrieve the space `name`.
operationId: get_space
responses:
'200':
$ref: '#/components/responses/Space200'
'404':
$ref: '#/components/responses/Standard404'
default:
$ref: '#/components/responses/Standard400'
#--------------------------------------------------------------------
# CORE QUERIES
#--------------------------------------------------------------------
/cores:
post:
tags: [Cores]
summary: >
Retrieve a list of core names.
operationId: post_cores
requestBody:
$ref: '#/components/requestBodies/Filters'
responses:
'200':
$ref: '#/components/responses/ArrayOfStrings'
'422':
$ref: '#/components/responses/Standard422'
default:
$ref: '#/components/responses/Standard400'
/cores/{name}:
parameters:
- $ref: '#/components/parameters/CoreName'
get:
tags: [Cores]
summary: >
Retrieve the core `name` properties. This does not include
the SpatialObjects contained in this Core.
operationId: get_core
responses:
'200':
$ref: '#/components/responses/Core200'
'404':
$ref: '#/components/responses/Standard404'
default:
$ref: '#/components/responses/Standard400'
#--------------------------------------------------------------------
# SPATIAL_OBJECTS QUERIES
#--------------------------------------------------------------------
/cores/{name}/spatial_objects:
parameters:
- $ref: '#/components/parameters/CoreName'
post:
tags: [Spatial Objects]
summary: >
Retrieve a list of spatial object.
operationId: post_spatial_objects
requestBody:
$ref: '#/components/requestBodies/Filters'
responses:
'200':
$ref: '#/components/responses/ArrayOfStrings'
'422':
$ref: '#/components/responses/Standard422'
default:
$ref: '#/components/responses/Standard400'
/cores/{name}/spatial_objects/{id}:
parameters:
- $ref: '#/components/parameters/CoreName'
- $ref: '#/components/parameters/SpatialObjectId'
get:
tags: [Spatial Objects]
summary: >
Retrieve the spatial object `id` of the core `name`.
operationId: get_spatial_object
responses:
'200':
$ref: '#/components/responses/SpatialObject200'
'404':
$ref: '#/components/responses/Standard404'
default:
$ref: '#/components/responses/Standard400'
components:
requestBodies:
Filters:
description: >
Filter string to use to select the data.
For more about the filter syntax, please refer to [filter grammar](https://epfl-dias.github.io/mercator_parser/book/filters.html).
If **ids_only** is true, then a list of **unique identifiers** is returned, instead of the whole, distinct, objects for the selected objects.
required: true
content:
application/json:
schema:
type: object
properties:
filters:
type: string
ids_only:
type: boolean
default: false
space:
type: string
resolution:
type: array
items:
type: number
minimum: 0
format: int32
view_port:
type: array
items:
type: array
items:
type: number
Query:
description: >
For more about the query syntax, please refer to the [query grammar](https://epfl-dias.github.io/mercator_parser/book/queries.html).
required: true
content:
application/json:
schema:
type: object
properties:
query:
type: string
resolution:
type: array
items:
type: number
minimum: 0
format: int32
view_port:
type: array
items:
type: array
items:
type: number
parameters:
SpaceName:
name: name
in: path
required: true
description: >
Name of the reference space
type: string
CoreName:
name: name
in: path
required: true
description: >
Name of the core
type: string
SpatialObjectId:
name: id
in: path
required: true
description: >
Id of the spatial object
type: string
responses:
Space200:
description: >
Reference space definition.
content:
application/json:
schema:
$ref: '#/components/schemas/Space'
SpatialObject200:
description: >
Spatial object.
schema:
$ref: '#/components/schemas/SpatialObject'
Core200:
description: >
Core properties
content:
application/json:
schema:
$ref: '#/components/schemas/Core'
Query200:
description: Arbitrary query.
ArrayOfStrings:
description: >
Array of strings, usually identifiers.
content:
application/json:
schema:
type: array
items:
type: string
Standard422:
description: >
Unprocessable Entity
Standard405:
description: >
Invalid Method
Standard404:
description: >
Object not found
Standard400:
description: >
Invalid or malformed request
Standard200:
description: OK
schemas:
#--------------------------------------------------------------------
# Types returned / accepted by the API
#--------------------------------------------------------------------
Space:
title: Reference Space
description: >
Definition of a space, in which objects are described.
type: object
properties:
name:
description: >
Unique Id for the space, which can also be used to generate a link to the user documentation describing the space, explaining the semantic meaning of the values stored, as well as the definitions of the axes.
type: string
origin:
description: >
Translation vector between the Universe origin to the origin of this reference space. This is expressed in Universe coordinates.
type: array
items:
type: number
axes:
description: >
The order of the axes matter and MUST be kept, as this is also linked to the definition found in the documentation.
Coordinate of a point MUST always be expressed using the same order as defined here.
type: array
items:
$ref: '#/components/schemas/Axis'
SpatialObject:
title: Spatial Object
description: >
Collection of positions in a space, which share a common set of properties.
type: object
properties:
properties:
description: >
Properties tied to a shape, in other words properties valid for the whole content of the shape.
type: object
properties:
type:
description: >
Label defining the kind of the spatial object.
type: string
id:
description: >
Identifier of this spatial object.
type: string
volumes:
description: >
List of volumes, overlapping or not, which define the whole space covered by this spatial object.
The overall volume described here is the **union** of the volumes.
type: array
items:
type: object
properties:
space:
description: >
Name of the reference space the associated shapes are defined in.
type: string
shapes:
description: >
List of shapes.
type: array
items:
type: object
description: >
One of the following fields per instance.
properties:
points:
description: >
List of points.
example: [[1, 2], [2, 3], [6, 1]]
type: array
items:
$ref: '#/components/schemas/Point'
boundingboxes:
description: >
List of bounding boxes.
example: [
[ [1, 2], [2, 3] ],
[ [-1, 0], [20, 10] ],
[ [0, 0], [4, 9] ],
]
type: array
items:
type: array
minItems: 2
maxitems: 2
items:
$ref: '#/components/schemas/Point'
hyperspheres:
description: >
List of hyperspheres, each hypersphere is described as tuple (position, radius)
example: [
[ [1, 2], 3 ],
[ [-1, 0], 4 ],
[ [0, 0], 23 ],
]
type: array
items:
type: array
minItems: 2
maxitems: 2
items:
type: {}
Core:
title: Core
description: >
Collection of Spatial Objects, stored in one or more Reference Spaces.
type: object
properties:
name:
type: string
version:
type: string
scales:
title: Scale Vectors
description: >
Scale factors used to generate less precise, coarser indexes in order to speed up queries over large volumes of the space.
Values are expressed as powers of two, in the range [0;n]. For each scale, a whole vector providing values for each axis MUST be provided.
Values, which are equal, and whose coordinates gets merged are merged as well, to reduce the number of results.
Distinct values whose coordinates are merged are recorded, thus allowing the user to move from one scale factor to another, with a finer resolution smoothly.
type: array
items:
type: array
items:
type: number
minimum: 0
format: int32
#--------------------------------------------------------------------
# Helper types
#--------------------------------------------------------------------
Point:
description: >
One valid value for each axes of the reference space this point
is used in.
type: array
items:
type: number
Axis:
title: Coordinate Axis
description: >
Defines the properties of an axis. The origin and unit vectors or defined within the universe space, but this does NOT imply a linear conversion is possible, this only provide anchoring of the axis as well as its absolute direction.
type: object
properties:
measurement_unit:
description: >
Length unit, as in SI Unit, for the `1.0` value on this axis.
For example [mm], [s], [um].
type: string
graduation:
description: >
Definition of the valid coordinate values which can be used on this axis.
type: object
properties:
set:
description: >
Valid numbers as defined by the usual mathematical sets, for example
* N: Natural numbers,
* Z: Integers,
* Q: Rational numbers,
* R: Real numbers.
type: string
# Decision: For now we leave it at strictly numbers, no
# categories until actually needed.
enum: [N, Z, Q, R]
minimum:
type: number
format: float
maximum:
type: number
format: float
steps:
type: number
format: integer
unit_vector:
description: >
Direction vector, with a norm of 1.0. This is expressed in Universe coordinates and provides the orientation of this axis in the Universe.
type: array
items:
type: number