use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum QueryType {
Gremlin = 1,
Gql = 2,
}
impl QueryType {
pub fn from_flags(flags: u16) -> Option<Self> {
match flags {
1 => Some(QueryType::Gremlin),
2 => Some(QueryType::Gql),
_ => None,
}
}
pub fn to_flags(self) -> u16 {
self as u16
}
}
impl std::fmt::Display for QueryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QueryType::Gremlin => write!(f, "Gremlin"),
QueryType::Gql => write!(f, "GQL"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ParameterType {
Any = 0xFF,
Null = 0x00,
Boolean = 0x01,
Integer = 0x02,
Float = 0x03,
String = 0x04,
List = 0x05,
Map = 0x06,
VertexId = 0x07,
EdgeId = 0x08,
}
impl ParameterType {
pub fn from_discriminant(d: u8) -> Self {
match d {
0x00 => ParameterType::Null,
0x01 => ParameterType::Boolean,
0x02 => ParameterType::Integer,
0x03 => ParameterType::Float,
0x04 => ParameterType::String,
0x05 => ParameterType::List,
0x06 => ParameterType::Map,
0x07 => ParameterType::VertexId,
0x08 => ParameterType::EdgeId,
_ => ParameterType::Any,
}
}
pub fn to_discriminant(self) -> u8 {
self as u8
}
pub fn matches(&self, value: &Value) -> bool {
match self {
ParameterType::Any => true,
ParameterType::Null => matches!(value, Value::Null),
ParameterType::Boolean => matches!(value, Value::Bool(_)),
ParameterType::Integer => matches!(value, Value::Int(_)),
ParameterType::Float => matches!(value, Value::Float(_)),
ParameterType::String => matches!(value, Value::String(_)),
ParameterType::List => matches!(value, Value::List(_)),
ParameterType::Map => matches!(value, Value::Map(_)),
ParameterType::VertexId => matches!(value, Value::Vertex(_)),
ParameterType::EdgeId => matches!(value, Value::Edge(_)),
}
}
pub fn type_name(&self) -> &'static str {
match self {
ParameterType::Any => "any",
ParameterType::Null => "null",
ParameterType::Boolean => "boolean",
ParameterType::Integer => "integer",
ParameterType::Float => "float",
ParameterType::String => "string",
ParameterType::List => "list",
ParameterType::Map => "map",
ParameterType::VertexId => "vertex_id",
ParameterType::EdgeId => "edge_id",
}
}
}
impl std::fmt::Display for ParameterType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.type_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryParameter {
pub name: String,
pub param_type: ParameterType,
}
impl QueryParameter {
pub fn new(name: impl Into<String>, param_type: ParameterType) -> Self {
Self {
name: name.into(),
param_type,
}
}
pub fn any(name: impl Into<String>) -> Self {
Self::new(name, ParameterType::Any)
}
}
impl std::fmt::Display for QueryParameter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "${}: {}", self.name, self.param_type)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedQuery {
pub id: u32,
pub name: String,
pub query_type: QueryType,
pub description: String,
pub query: String,
pub parameters: Vec<QueryParameter>,
}
impl SavedQuery {
pub fn new(
id: u32,
name: impl Into<String>,
query_type: QueryType,
description: impl Into<String>,
query: impl Into<String>,
parameters: Vec<QueryParameter>,
) -> Self {
Self {
id,
name: name.into(),
query_type,
description: description.into(),
query: query.into(),
parameters,
}
}
pub fn parameter_names(&self) -> std::collections::HashSet<&str> {
self.parameters.iter().map(|p| p.name.as_str()).collect()
}
pub fn get_parameter(&self, name: &str) -> Option<&QueryParameter> {
self.parameters.iter().find(|p| p.name == name)
}
pub fn has_parameters(&self) -> bool {
!self.parameters.is_empty()
}
}
impl std::fmt::Display for SavedQuery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}] {} ({}): {}",
self.id, self.name, self.query_type, self.description
)
}
}
pub type QueryParams = HashMap<String, Value>;
pub const MAX_QUERY_NAME_LENGTH: usize = 64;
pub fn validate_query_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("query name cannot be empty".to_string());
}
if name.len() > MAX_QUERY_NAME_LENGTH {
return Err(format!(
"query name too long: {} characters (max {})",
name.len(),
MAX_QUERY_NAME_LENGTH
));
}
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
Some(c) => {
return Err(format!(
"query name must start with letter or underscore, got '{}'",
c
))
}
None => return Err("query name cannot be empty".to_string()),
}
for c in chars {
if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
return Err(format!("invalid character in query name: '{}'", c));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_type_conversion() {
assert_eq!(QueryType::from_flags(1), Some(QueryType::Gremlin));
assert_eq!(QueryType::from_flags(2), Some(QueryType::Gql));
assert_eq!(QueryType::from_flags(0), None);
assert_eq!(QueryType::from_flags(3), None);
assert_eq!(QueryType::Gremlin.to_flags(), 1);
assert_eq!(QueryType::Gql.to_flags(), 2);
}
#[test]
fn test_query_type_display() {
assert_eq!(format!("{}", QueryType::Gremlin), "Gremlin");
assert_eq!(format!("{}", QueryType::Gql), "GQL");
}
#[test]
fn test_parameter_type_conversion() {
assert_eq!(ParameterType::from_discriminant(0xFF), ParameterType::Any);
assert_eq!(ParameterType::from_discriminant(0x00), ParameterType::Null);
assert_eq!(
ParameterType::from_discriminant(0x02),
ParameterType::Integer
);
assert_eq!(
ParameterType::from_discriminant(0x04),
ParameterType::String
);
assert_eq!(ParameterType::Any.to_discriminant(), 0xFF);
assert_eq!(ParameterType::Integer.to_discriminant(), 0x02);
assert_eq!(ParameterType::String.to_discriminant(), 0x04);
}
#[test]
fn test_parameter_type_matches() {
assert!(ParameterType::Any.matches(&Value::Int(42)));
assert!(ParameterType::Any.matches(&Value::String("test".to_string())));
assert!(ParameterType::Integer.matches(&Value::Int(42)));
assert!(!ParameterType::Integer.matches(&Value::String("test".to_string())));
assert!(ParameterType::String.matches(&Value::String("test".to_string())));
assert!(!ParameterType::String.matches(&Value::Int(42)));
assert!(ParameterType::Boolean.matches(&Value::Bool(true)));
assert!(!ParameterType::Boolean.matches(&Value::Int(1)));
}
#[test]
fn test_parameter_type_display() {
assert_eq!(format!("{}", ParameterType::Any), "any");
assert_eq!(format!("{}", ParameterType::String), "string");
assert_eq!(format!("{}", ParameterType::Integer), "integer");
}
#[test]
fn test_query_parameter() {
let param = QueryParameter::new("name", ParameterType::String);
assert_eq!(param.name, "name");
assert_eq!(param.param_type, ParameterType::String);
assert_eq!(format!("{}", param), "$name: string");
let any_param = QueryParameter::any("value");
assert_eq!(any_param.param_type, ParameterType::Any);
}
#[test]
fn test_saved_query() {
let query = SavedQuery::new(
1,
"test_query",
QueryType::Gremlin,
"A test query",
"g.V().has('name', $name)",
vec![QueryParameter::new("name", ParameterType::String)],
);
assert_eq!(query.id, 1);
assert_eq!(query.name, "test_query");
assert_eq!(query.query_type, QueryType::Gremlin);
assert!(query.has_parameters());
assert!(query.parameter_names().contains("name"));
assert!(query.get_parameter("name").is_some());
assert!(query.get_parameter("unknown").is_none());
}
#[test]
fn test_validate_query_name_valid() {
assert!(validate_query_name("test").is_ok());
assert!(validate_query_name("test_query").is_ok());
assert!(validate_query_name("test-query").is_ok());
assert!(validate_query_name("_private").is_ok());
assert!(validate_query_name("query123").is_ok());
assert!(validate_query_name("Query_123_test").is_ok());
}
#[test]
fn test_validate_query_name_invalid() {
assert!(validate_query_name("").is_err());
assert!(validate_query_name("123test").is_err());
assert!(validate_query_name("-test").is_err());
assert!(validate_query_name("test query").is_err());
assert!(validate_query_name("test.query").is_err());
assert!(validate_query_name("test@query").is_err());
let long_name = "a".repeat(MAX_QUERY_NAME_LENGTH + 1);
assert!(validate_query_name(&long_name).is_err());
let max_name = "a".repeat(MAX_QUERY_NAME_LENGTH);
assert!(validate_query_name(&max_name).is_ok());
}
}