This application collects data from an external database (neo4j) on start-up and then provides one HTTP end-point where one can get a filtered and sorted response based on some JSON that's passed along in the body of the request.
main.rs:
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rusted_cypher;
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate serde;
extern crate time;
use std::collections::HashMap;
use rusted_cypher::GraphClient;
use rocket_contrib::{JSON, Value};
use rocket::State;
fn get_year_of_release_date(release_date: i64) -> u16 {
time::at_utc(time::Timespec {
nsec: 0,
sec: release_date,
})
.tm_year as u16 + 1900
}
fn load_data() -> Vec<Media> {
// TODO: get neo4j server url from environment
let graph = GraphClient::connect("http://serveraddress:7474/db/data").unwrap();
// query is a simple match of nodes that returns an id, rating, release date for collected nodes and sorts them by rating in descending order
let results = graph.exec(query).unwrap();
let mut data: Vec<Media> = Vec::new();
for row in results.rows() {
data.push(Media {
id: row.get("id").unwrap(),
rating: row.get("rating").unwrap(),
year: get_year_of_release_date(row.get("releaseDate").unwrap_or(0)),
});
}
data
}
#[derive(Debug, Serialize, Deserialize)]
struct Media {
id: String,
rating: u16,
year: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct RecsysObject {
#[serde(rename = "pageSize")]
page_size: Option<usize>,
page: Option<usize>,
filters: Option<HashMap<String, SimpleYearFilter>>,
sort: Option<Vec<HashMap<String, String>>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct SimpleYearFilter {
#[serde(rename = "$eq")]
eq: Option<u16>,
#[serde(rename = "$and")]
and: Option<Vec<LogicFilter>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
enum LogicFilter {
#[serde(rename = "$gt")]
GT(u16),
#[serde(rename = "$lt")]
LT(u16),
}
impl RecsysObject {
fn construct_filter(self: &Self, movie: &Media) -> bool {
match self.filters.clone() {
None => true,
Some(f) => {
match f.get("year") {
None => true,
Some(yf) => {
match yf.eq {
Some(year) => movie.year == year,
None => {
match yf.and.clone() {
None => true,
Some(rangefilter) => {
for rf in rangefilter {
match rf {
LogicFilter::LT(val) => {
if movie.year >= val {
return false;
}
}
LogicFilter::GT(val) => {
if movie.year <= val {
return false;
}
}
}
}
true
}
}
}
}
}
}
}
}
}
}
#[post("/", data = "<object>")]
fn one_point_of_success(allmovies: State<Vec<Media>>, object: JSON<RecsysObject>) -> JSON<Value> {
let mut results = allmovies
.iter()
.filter(|m| object.construct_filter(m))
.collect::<Vec<_>>();
match object.sort.clone() {
None => (),
Some(v) => {
match v.get(0) {
None => (),
Some(hmap) => {
match hmap.get("property") {
None => {
if hmap.get("type").unwrap() == "popularity" {
match hmap.get("order") {
Some(order) => {
if order == "ascending" {
results.sort_by(|a, b| a.rating.cmp(&b.rating));
}
}
_ => {
() // already sorted
}
}
}
}
_ => {
if hmap.get("property").unwrap() == "metadata.releaseDate" {
match hmap.get("order") {
Some(order) => {
if order == "ascending" {
results.sort_by(|a, b| a.year.cmp(&b.year));
} else {
results.sort_by(|a, b| b.year.cmp(&a.year));
}
}
_ => {
results.sort_by(|a, b| b.year.cmp(&a.year));
}
}
} else {
()
}
}
}
}
}
}
};
JSON(json!(results.iter().skip(object.page.unwrap_or(0) * object.page_size.unwrap_or(10)) // first page is page 0
.take(object.page_size.unwrap_or(10)).collect::<Vec<_>>()))
}
fn main() {
rocket::ignite()
.manage(load_data())
.mount("/", routes![one_point_of_success])
.launch();
}
These types of filters/sorts are available to be sent as a JSON body into the POST request:
Single year filter
{"filters":{"year":{"$eq":1984}},"pageSize":10,"page":0}
Year range filter
{"filters":{"year":{"$and":[{"$gt":1982},{"$lt":1989}]}},"pageSize":10,"page":0}
Year range filter + year sort
{"filters":{"year":{"$and":[{"$gt":1900},{"$lt":2007}]}},"sort":[{"type":"property","property":"metadata.releaseDate","order":"descending"}],"pageSize":100,"page":310}
Year range filter + popularity sort
{"filters":{"year":{"$and":[{"$gt":1900},{"$lt":2018}]}},"sort":[{"type":"popularity","order":"descending"}],"pageSize":10,"page":0}
Popularity sort
{"sort":[{"type":"popularity","order":"descending"}],"pageSize":10,"page":0}
Year sort
{"sort":[{"type":"property","property":"metadata.releaseDate","order":"ascending"}],"pageSize":10,"page":0}
While putting everything into a vector might not be the best choice in terms of filtering and sorting, the performance is good enough. I'm primarily concerned on how ugly I've implemented the filtering and sorting, so I'm looking for feedback to improve on that, preferably with some concrete examples.
For completeness, this is built on nightly Rust with these dependencies:
rusted_cypher = "1.1.0"
rocket = { git = "https://github.com/SergioBenitez/Rocket" }
rocket_codegen = { git = "https://github.com/SergioBenitez/Rocket" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket" }
serde = "1.0.2"
serde_json = "1.0.1"
serde_derive = "1.0.2"
time = "0.1"
query
in this scope inload_data()
\$\endgroup\$