diff --git a/src/backend/backend.rs b/src/backend/backend.rs index 628b306..3d7846f 100644 --- a/src/backend/backend.rs +++ b/src/backend/backend.rs @@ -1,21 +1,56 @@ -use neo4rs::Graph; -use rocket::{get, Route, State}; -use rocket::response::status::NotFound; -use rocket_okapi::{openapi, openapi_get_routes}; +use crate::backend::types::{City, Road, ShortestPath}; +use crate::graph::graph::{all_cities, create_city, create_road, shortest_path}; +use neo4rs::{Graph}; +use rocket::response::status::{BadRequest}; use rocket::serde::json::Json; +use rocket::{get, post, Route, State}; +use rocket_okapi::{openapi, openapi_get_routes}; - +/// configure available api nodes pub fn build_api() -> Vec { - openapi_get_routes![ - append - ] + openapi_get_routes![addcity, addroad, getshortestpath, getallcities] } -/// get general build-server stats -#[openapi(tag = "stats")] -#[get("/append")] -pub async fn append(graph: &State) -> Result> { - let graph = graph as &Graph; - - Ok("worked".to_string()) +/// endpoint to add new city to graph +#[openapi] +#[post("/addcity", data = "")] +pub async fn addcity(graph: &State, msg: Json) -> Result> { + match create_city(graph, msg.into_inner()).await { + Ok(_) => Ok("ok".to_string()), + Err(e) => Err(BadRequest(e.to_string())), + } +} + +/// endpoint to add new road between two cities +#[openapi] +#[post("/road", data = "")] +pub async fn addroad(graph: &State, msg: Json) -> Result> { + match create_road(graph, msg.into_inner()).await { + Ok(_) => Ok("ok".to_string()), + Err(e) => Err(BadRequest(e.to_string())), + } +} + +/// endpoint to get all available cities with optional limit +#[openapi] +#[get("/cities?")] +pub async fn getallcities(graph: &State, limit: Option) -> Result>, BadRequest> { + match all_cities(graph, limit).await { + Ok(v) => Ok(Json::from(v)), + Err(e) => Err(BadRequest(e.to_string())), + } +} + +/// endpoint to get shortest path between two cities with the resulting length +#[openapi] +#[get("/shortestpath?&")] +pub async fn getshortestpath( + graph: &State, + city1: String, + city2: String, +) -> Result, BadRequest> { + match shortest_path(graph, city1, city2).await { + Ok(v) => Ok(Json::from(v)), + Err(e) => Err(BadRequest(e.to_string())), + } } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d882b4c..e59217a 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1 +1,2 @@ -pub mod backend; \ No newline at end of file +pub mod backend; +pub mod types; diff --git a/src/backend/types.rs b/src/backend/types.rs new file mode 100644 index 0000000..190a4ae --- /dev/null +++ b/src/backend/types.rs @@ -0,0 +1,24 @@ +use rocket::serde::{Deserialize, Serialize}; +use rocket_okapi::okapi::schemars; +use rocket_okapi::JsonSchema; +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(crate = "rocket::serde")] +pub struct City { + pub name: String, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(crate = "rocket::serde")] +pub struct Road { + pub city1name: String, + pub city2name: String, + pub distance: i32, + pub oneway: bool, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(crate = "rocket::serde")] +pub struct ShortestPath { + pub(crate) path: Vec, + pub(crate) distance: i32, +} \ No newline at end of file diff --git a/src/graph/graph.rs b/src/graph/graph.rs new file mode 100644 index 0000000..27cd3b2 --- /dev/null +++ b/src/graph/graph.rs @@ -0,0 +1,90 @@ +use crate::backend::types::{City, Road, ShortestPath}; +use anyhow::anyhow; +use neo4rs::{query, Graph, Node, Path}; + + +pub async fn shortest_path( + graph: &Graph, + city1: String, + city2: String, +) -> anyhow::Result { + if city1 == city2 { + return Err(anyhow!("Two cities must be different")); + } + + let mut result = graph.execute( + query("MATCH (c1:City {name: $c1}), (c2:City {name: $c2}) +WITH c1, c2 +MATCH path = (c1)-[:ROAD*]->(c2) +WITH path, reduce(distance = 0, rel in relationships(path) | distance + rel.distance) AS totalDistance +ORDER BY totalDistance ASC +LIMIT 1 +RETURN path, totalDistance AS distance") + .param("c1", city1) + .param("c2", city2) + ).await?; + + return if let Ok(Some(row)) = result.next().await { + let path: Path = row.get("path")?; + let distance: i32 = row.get("distance")?; + + let city_names: Vec = path + .nodes() + .iter() + .map(|node| node.get::("name").unwrap()) + .collect(); + Ok(ShortestPath { + path: city_names, + distance, + }) + } else { + Err(anyhow!("couldn't find a valid path")) + }; +} + +pub async fn all_cities(graph: &Graph, limit: Option) -> anyhow::Result> { + let mut cities = vec![]; + + let query = match limit { + None => query("MATCH (c:City) RETURN c"), + Some(limit) => query("MATCH (c:City) RETURN c LIMIT $limit").param("limit", limit) + }; + + let mut result = graph + .execute(query) + .await?; + while let Ok(Some(row)) = result.next().await { + let node: Node = row.get("c")?; + let name: String = node.get("name")?; + cities.push(name); + } + Ok(cities) +} + +pub async fn create_road(graph: &Graph, msg: Road) -> anyhow::Result<()> { + let mut txn = graph.start_txn().await?; + txn.run_queries([ + query("MATCH (c1:City{name: $city1}) WITH c1 MATCH (c2:City{name: $city2}) WITH c1,c2 CREATE (c1)-[:ROAD{distance: $distance}]->(c2)") + .param("city1", msg.city1name.clone()) + .param("city2", msg.city2name.clone()) + .param("distance", msg.distance.clone()), + ]).await?; + if !msg.oneway { + txn.run_queries([ + query("MATCH (c1:City{name: $city1}) WITH c1 MATCH (c2:City{name: $city2}) WITH c1,c2 CREATE (c1)<-[:ROAD{distance: $distance}]-(c2)") + .param("city1", msg.city1name.clone()) + .param("city2", msg.city2name.clone()) + .param("distance", msg.distance.clone()) + ]).await?; + } + txn.commit().await?; + Ok(()) +} + +pub async fn create_city(graph: &Graph, msg: City) -> anyhow::Result<()> { + let mut txn = graph.start_txn().await?; + txn.run_queries([query("CREATE (c:City {name: $name})").param("name", msg.name.clone())]) + .await?; + txn.commit().await?; + Ok(()) +} diff --git a/src/graph/mod.rs b/src/graph/mod.rs new file mode 100644 index 0000000..6f94350 --- /dev/null +++ b/src/graph/mod.rs @@ -0,0 +1 @@ +pub mod graph; diff --git a/src/main.rs b/src/main.rs index cf0a137..e78307e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,51 +1,29 @@ -use neo4rs::{Graph, Node, query}; +use crate::backend::backend::build_api; +use neo4rs::Graph; use rocket::Config; use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; -use crate::backend::backend::build_api; mod backend; +mod graph; fn main() { - println!("Hello, world!"); + let tokio = tokio::runtime::Runtime::new().expect("Failed to spawn tokio runtime"); - let t = tokio::runtime::Runtime::new().unwrap(); - - let tt = Box::new("hello".to_string()); - - let iii = 1; - - - - - - - for i in 1..5 { - - if tt == "hello" { - println!("{tt}"); - } else { - println!("not"); - } - - } - - println!("{tt}"); - - t.block_on(async move { - // concurrent queries + tokio.block_on(async move { let uri: String = "127.0.0.1:7687".to_string(); - let user = "neo4j"; - let pass = "neo"; + let user = ""; + let pass = ""; + // connect to neo4j database let graph = Graph::new(uri, user, pass).await.unwrap(); - //println!("{uri}"); - + // configure api port and interface let config = Config { address: "0.0.0.0".parse().unwrap(), port: 8081, ..Default::default() }; + // configure api and swagger ui let rock = rocket::custom(config) .manage(graph) .mount("/api/", build_api())