use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::client::Client;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GraphDirection {
#[default]
Outgoing,
Incoming,
Both,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphNode {
pub id: String,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub labels: Vec<String>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub properties: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphEdge {
pub id: String,
#[serde(default)]
pub from: String,
#[serde(default)]
pub to: String,
#[serde(default)]
pub label: String,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub properties: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphNeighbor {
pub edge_id: String,
pub node_id: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub direction: GraphDirection,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphVisit {
pub id: String,
#[serde(default)]
pub depth: usize,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub labels: Vec<String>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub properties: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphTraversal {
#[serde(default)]
pub start: String,
#[serde(default)]
pub direction: String,
#[serde(default)]
pub max_depth: usize,
#[serde(default)]
pub limit: usize,
#[serde(default)]
pub count: usize,
#[serde(default)]
pub truncated: bool,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub nodes: Vec<GraphVisit>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphPath {
#[serde(default)]
pub found: bool,
#[serde(default)]
pub from: String,
#[serde(default)]
pub to: String,
#[serde(default)]
pub direction: String,
#[serde(default)]
pub hops: usize,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub node_path: Vec<String>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub edge_path: Vec<String>,
#[serde(default)]
pub total_cost: f64,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphNodePage {
#[serde(default)]
pub graph: String,
#[serde(default)]
pub count: usize,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub nodes: Vec<GraphNode>,
#[serde(default)]
pub truncated: bool,
#[serde(default)]
pub total: i64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphEdgePage {
#[serde(default)]
pub graph: String,
#[serde(default)]
pub count: usize,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub edges: Vec<GraphEdge>,
#[serde(default)]
pub truncated: bool,
#[serde(default)]
pub total: i64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GraphRows {
#[serde(default)]
pub graph: String,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub columns: Vec<String>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub rows: Vec<Vec<Value>>,
#[serde(default)]
pub count: usize,
#[serde(default)]
pub truncated: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NeighborOptions {
pub direction: Option<GraphDirection>,
pub label: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TraverseOptions {
pub direction: Option<GraphDirection>,
pub label: Option<String>,
pub max_depth: Option<usize>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PathOptions {
pub direction: Option<GraphDirection>,
pub label: Option<String>,
pub max_depth: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WeightedPathOptions {
pub direction: Option<GraphDirection>,
pub label: Option<String>,
pub weight_property: Option<String>,
}
fn op(body: Value) -> Value {
json!({"Graph": body})
}
fn put(body: &mut Map<String, Value>, key: &str, value: Option<impl Into<Value>>) {
if let Some(value) = value {
body.insert(key.to_string(), value.into());
}
}
fn direction_value(direction: Option<GraphDirection>) -> Option<Value> {
direction.map(|d| serde_json::to_value(d).expect("a direction always encodes"))
}
impl Client {
pub fn graph_create(&mut self, graph: &str) -> Result<()> {
self.send(op(json!({"CreateGraph": {"graph": graph}})))
.map(|_| ())
}
pub fn graph_drop(&mut self, graph: &str) -> Result<()> {
self.send(op(json!({"DropGraph": {"graph": graph}})))
.map(|_| ())
}
pub fn graph_list(&mut self) -> Result<Vec<String>> {
#[derive(Deserialize)]
struct Graphs {
#[serde(default)]
graphs: Vec<String>,
}
let response = self.send(op(Value::String("ListGraphs".into())))?;
let out: Graphs = response.decode("ListGraphs")?;
Ok(out.graphs)
}
pub fn graph_add_node(
&mut self,
graph: &str,
id: &str,
labels: &[&str],
properties: Option<&Map<String, Value>>,
) -> Result<()> {
self.send(op(json!({"AddNode": {
"graph": graph,
"id": id,
"labels": labels,
"properties": properties,
}})))
.map(|_| ())
}
pub fn graph_get_node(&mut self, graph: &str, id: &str) -> Result<Option<GraphNode>> {
let response = self.send(op(json!({"GetNode": {"graph": graph, "id": id}})))?;
if response.expect("Json", "GetNode")?.is_null() {
return Ok(None);
}
response.decode("GetNode").map(Some)
}
pub fn graph_delete_node(&mut self, graph: &str, id: &str) -> Result<()> {
self.send(op(json!({"DeleteNode": {"graph": graph, "id": id}})))
.map(|_| ())
}
pub fn graph_list_nodes(
&mut self,
graph: &str,
limit: Option<usize>,
offset: Option<usize>,
) -> Result<GraphNodePage> {
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
put(&mut body, "limit", limit.filter(|l| *l > 0));
put(&mut body, "offset", offset.filter(|o| *o > 0));
let response = self.send(op(json!({"ListNodes": Value::Object(body)})))?;
response.decode("ListNodes")
}
pub fn graph_add_edge(
&mut self,
graph: &str,
id: &str,
from: &str,
to: &str,
label: &str,
properties: Option<&Map<String, Value>>,
) -> Result<()> {
self.send(op(json!({"AddEdge": {
"graph": graph,
"id": id,
"from": from,
"to": to,
"label": label,
"properties": properties,
}})))
.map(|_| ())
}
pub fn graph_get_edge(&mut self, graph: &str, id: &str) -> Result<Option<GraphEdge>> {
let response = self.send(op(json!({"GetEdge": {"graph": graph, "id": id}})))?;
if response.expect("Json", "GetEdge")?.is_null() {
return Ok(None);
}
response.decode("GetEdge").map(Some)
}
pub fn graph_delete_edge(&mut self, graph: &str, id: &str) -> Result<()> {
self.send(op(json!({"DeleteEdge": {"graph": graph, "id": id}})))
.map(|_| ())
}
pub fn graph_list_edges(
&mut self,
graph: &str,
limit: Option<usize>,
offset: Option<usize>,
) -> Result<GraphEdgePage> {
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
put(&mut body, "limit", limit.filter(|l| *l > 0));
put(&mut body, "offset", offset.filter(|o| *o > 0));
let response = self.send(op(json!({"ListEdges": Value::Object(body)})))?;
response.decode("ListEdges")
}
pub fn graph_neighbors(
&mut self,
graph: &str,
node_id: &str,
options: &NeighborOptions,
) -> Result<Vec<GraphNeighbor>> {
#[derive(Deserialize)]
struct Neighbors {
#[serde(default)]
neighbors: Vec<GraphNeighbor>,
}
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
body.insert("node_id".into(), json!(node_id));
put(&mut body, "direction", direction_value(options.direction));
put(&mut body, "label", options.label.clone());
put(&mut body, "limit", options.limit.filter(|l| *l > 0));
let response = self.send(op(json!({"Neighbors": Value::Object(body)})))?;
let out: Neighbors = response.decode("Neighbors")?;
Ok(out.neighbors)
}
pub fn graph_degree(
&mut self,
graph: &str,
node_id: &str,
direction: Option<GraphDirection>,
) -> Result<i64> {
#[derive(Deserialize)]
struct Degree {
#[serde(default)]
degree: i64,
}
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
body.insert("node_id".into(), json!(node_id));
put(&mut body, "direction", direction_value(direction));
let response = self.send(op(json!({"Degree": Value::Object(body)})))?;
let out: Degree = response.decode("Degree")?;
Ok(out.degree)
}
pub fn graph_traverse(
&mut self,
graph: &str,
start: &str,
options: &TraverseOptions,
) -> Result<GraphTraversal> {
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
body.insert("start".into(), json!(start));
put(&mut body, "direction", direction_value(options.direction));
put(&mut body, "label", options.label.clone());
put(&mut body, "max_depth", options.max_depth.filter(|d| *d > 0));
put(&mut body, "limit", options.limit.filter(|l| *l > 0));
let response = self.send(op(json!({"Traverse": Value::Object(body)})))?;
response.decode("Traverse")
}
pub fn graph_shortest_path(
&mut self,
graph: &str,
from: &str,
to: &str,
options: &PathOptions,
) -> Result<GraphPath> {
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
body.insert("from".into(), json!(from));
body.insert("to".into(), json!(to));
put(&mut body, "direction", direction_value(options.direction));
put(&mut body, "label", options.label.clone());
put(&mut body, "max_depth", options.max_depth.filter(|d| *d > 0));
let response = self.send(op(json!({"ShortestPath": Value::Object(body)})))?;
response.decode("ShortestPath")
}
pub fn graph_weighted_shortest_path(
&mut self,
graph: &str,
from: &str,
to: &str,
options: &WeightedPathOptions,
) -> Result<GraphPath> {
let mut body = Map::new();
body.insert("graph".into(), json!(graph));
body.insert("from".into(), json!(from));
body.insert("to".into(), json!(to));
put(&mut body, "direction", direction_value(options.direction));
put(&mut body, "label", options.label.clone());
put(
&mut body,
"weight_property",
options.weight_property.clone(),
);
let response = self.send(op(json!({"WeightedShortestPath": Value::Object(body)})))?;
response.decode("WeightedShortestPath")
}
pub fn graph_query(&mut self, graph: &str, cypher: &str) -> Result<GraphRows> {
let response = self.send(op(json!({"Query": {"graph": graph, "cypher": cypher}})))?;
response.decode("Query")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn directions_use_the_servers_spelling() {
assert_eq!(
serde_json::to_value(GraphDirection::Outgoing).unwrap(),
"outgoing"
);
assert_eq!(
serde_json::to_value(GraphDirection::Incoming).unwrap(),
"incoming"
);
assert_eq!(serde_json::to_value(GraphDirection::Both).unwrap(), "both");
assert_eq!(GraphDirection::default(), GraphDirection::Outgoing);
}
#[test]
fn an_unset_option_is_left_out_rather_than_sent_empty() {
let mut body = Map::new();
put(&mut body, "label", None::<String>);
put(&mut body, "limit", None::<usize>);
assert!(body.is_empty());
put(&mut body, "label", Some("FOLLOWS".to_string()));
put(&mut body, "limit", Some(10usize));
assert_eq!(body["label"], "FOLLOWS");
assert_eq!(body["limit"], 10);
}
#[test]
fn a_zero_bound_is_the_servers_default_not_a_bound_of_zero() {
let options = TraverseOptions {
max_depth: Some(0),
..Default::default()
};
let mut body = Map::new();
put(&mut body, "max_depth", options.max_depth.filter(|d| *d > 0));
assert!(body.is_empty());
}
#[test]
fn a_path_that_was_not_found_still_decodes() {
let path: GraphPath =
serde_json::from_value(json!({"found": false, "message": "no path within 10 hops"}))
.unwrap();
assert!(!path.found);
assert_eq!(path.hops, 0);
assert!(path.node_path.is_empty());
}
}