use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Request {
CreateNode {
labels: Vec<String>,
properties: serde_json::Value,
#[serde(default)]
embedding: Option<Vec<f32>>,
},
CreateEdge {
source: u64,
target: u64,
edge_type: String,
#[serde(default = "default_properties")]
properties: serde_json::Value,
#[serde(default = "default_weight")]
weight: f64,
#[serde(default)]
valid_from: Option<i64>,
#[serde(default)]
valid_to: Option<i64>,
},
GetNode { id: u64 },
GetEdge { id: u64 },
UpdateNode {
id: u64,
properties: serde_json::Value,
},
UpdateEdge {
id: u64,
properties: serde_json::Value,
},
DeleteNode { id: u64 },
DeleteEdge { id: u64 },
Neighbors {
id: u64,
direction: String, #[serde(default)]
edge_type: Option<String>,
},
Bfs {
start: u64,
#[serde(default = "default_max_depth")]
max_depth: usize,
},
ShortestPath {
from: u64,
to: u64,
#[serde(default)]
weighted: bool,
},
VectorSearch {
query: Vec<f32>,
#[serde(default = "default_k")]
k: usize,
},
HybridSearch {
anchor: u64,
query: Vec<f32>,
#[serde(default = "default_max_depth")]
max_hops: usize,
#[serde(default = "default_k")]
k: usize,
#[serde(default = "default_alpha")]
alpha: f32,
},
SemanticNeighbors {
id: u64,
concept: Vec<f32>,
#[serde(default = "default_direction")]
direction: String,
#[serde(default = "default_k")]
k: usize,
},
SemanticWalk {
start: u64,
concept: Vec<f32>,
#[serde(default = "default_max_depth")]
max_hops: usize,
},
Query { gql: String },
ExtractSubgraph {
center: u64,
#[serde(default = "default_max_depth")]
hops: usize,
#[serde(default = "default_max_context_nodes")]
max_nodes: usize,
#[serde(default = "default_text_format")]
format: String,
},
GraphRag {
question: String,
#[serde(default)]
question_embedding: Option<Vec<f32>>,
#[serde(default)]
anchor: Option<u64>,
#[serde(default = "default_max_depth")]
hops: usize,
#[serde(default = "default_max_context_nodes")]
max_nodes: usize,
#[serde(default = "default_text_format")]
format: String,
},
NeighborsAt {
id: u64,
direction: String,
timestamp: i64,
#[serde(default)]
edge_type: Option<String>,
},
BfsAt {
start: u64,
#[serde(default = "default_max_depth")]
max_depth: usize,
timestamp: i64,
},
ShortestPathAt {
from: u64,
to: u64,
timestamp: i64,
#[serde(default)]
weighted: bool,
},
Dfs {
start: u64,
#[serde(default = "default_max_depth")]
max_depth: usize,
},
DfsAt {
start: u64,
#[serde(default = "default_max_depth")]
max_depth: usize,
timestamp: i64,
},
FindByLabel { label: String },
DeleteByLabel { label: String },
FindEdgeByType { edge_type: String },
RunPageRank {
#[serde(default)]
nodes: Option<Vec<u64>>,
#[serde(default = "default_damping")]
damping: f64,
#[serde(default = "default_max_iterations")]
max_iterations: usize,
#[serde(default = "default_tolerance")]
tolerance: f64,
},
RunLouvain {
#[serde(default)]
nodes: Option<Vec<u64>>,
},
RunConnectedComponents {
#[serde(default)]
nodes: Option<Vec<u64>>,
#[serde(default)]
strong: bool,
},
RunDegreeCentrality {
#[serde(default)]
nodes: Option<Vec<u64>>,
#[serde(default = "default_direction")]
direction: String,
},
RunBetweennessCentrality {
#[serde(default)]
nodes: Option<Vec<u64>>,
},
GraphStats,
GetSubgraph {
center: u64,
#[serde(default = "default_max_depth")]
hops: usize,
#[serde(default = "default_max_context_nodes")]
max_nodes: usize,
},
Ping,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum Response {
#[serde(rename = "ok")]
Ok { data: serde_json::Value },
#[serde(rename = "error")]
Error { message: String },
}
impl Response {
pub fn ok(data: impl Serialize) -> Self {
Self::Ok {
data: serde_json::to_value(data).unwrap_or(serde_json::Value::Null),
}
}
pub fn error(msg: impl Into<String>) -> Self {
Self::Error {
message: msg.into(),
}
}
}
fn default_properties() -> serde_json::Value {
serde_json::json!({})
}
fn default_weight() -> f64 {
1.0
}
fn default_max_depth() -> usize {
3
}
fn default_k() -> usize {
10
}
fn default_alpha() -> f32 {
0.5
}
fn default_direction() -> String {
"outgoing".to_string()
}
fn default_max_context_nodes() -> usize {
50
}
fn default_damping() -> f64 {
0.85
}
fn default_max_iterations() -> usize {
100
}
fn default_tolerance() -> f64 {
1e-6
}
fn default_text_format() -> String {
"structured".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_create_node_request() {
let req = Request::CreateNode {
labels: vec!["Person".into()],
properties: serde_json::json!({"name": "Alice"}),
embedding: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("CreateNode"));
assert!(json.contains("Alice"));
}
#[test]
fn deserialize_create_node_request() {
let json = r#"{"type":"CreateNode","labels":["Person"],"properties":{"name":"Bob"}}"#;
let req: Request = serde_json::from_str(json).unwrap();
match req {
Request::CreateNode {
labels, properties, ..
} => {
assert_eq!(labels, vec!["Person"]);
assert_eq!(properties["name"], "Bob");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn response_ok() {
let resp = Response::ok(serde_json::json!({"id": 42}));
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("ok"));
assert!(json.contains("42"));
}
#[test]
fn response_error() {
let resp = Response::error("not found");
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("error"));
assert!(json.contains("not found"));
}
#[test]
fn deserialize_ping() {
let json = r#"{"type":"Ping"}"#;
let req: Request = serde_json::from_str(json).unwrap();
assert!(matches!(req, Request::Ping));
}
}