use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, collections::HashSet, fmt, str::FromStr};
pub use serde_json::{Map, Number};
pub type Json = serde_json::Value;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)]
pub enum Value {
#[default]
Null,
Bool(bool),
Number(Number),
String(String),
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Bool(b) => write!(f, "{b}"),
Value::Number(n) => write!(f, "{n}"),
Value::String(s) => write!(f, "{}", Json::String(s.clone())),
}
}
}
impl From<Value> for Json {
fn from(value: Value) -> Self {
match value {
Value::Null => Json::Null,
Value::Bool(b) => Json::Bool(b),
Value::Number(n) => Json::Number(n),
Value::String(s) => Json::String(s),
}
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::String(s.to_string())
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::String(s)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
impl From<Number> for Value {
fn from(value: Number) -> Self {
Value::Number(value)
}
}
impl TryFrom<Json> for Value {
type Error = String;
fn try_from(value: Json) -> Result<Self, Self::Error> {
match value {
Json::Null => Ok(Value::Null),
Json::Bool(b) => Ok(Value::Bool(b)),
Json::Number(n) => Ok(Value::Number(n)),
Json::String(s) => Ok(Value::String(s)),
_ => Err(format!("Unsupported JSON type: {value:?}")),
}
}
}
impl Value {
pub fn into_opt_string(self) -> Result<Option<String>, String> {
match self {
Value::String(s) => Ok(Some(s)),
Value::Null => Ok(None),
v => Err(format!("Expected a string or null, found: {v:?}")),
}
}
pub fn into_opt_number(self) -> Result<Option<Number>, String> {
match self {
Value::Number(n) => Ok(Some(n)),
Value::Null => Ok(None),
v => Err(format!("Expected a number or null, found: {v:?}")),
}
}
pub fn into_opt_bool(self) -> Result<Option<bool>, String> {
match self {
Value::Bool(b) => Ok(Some(b)),
Value::Null => Ok(None),
v => Err(format!("Expected a boolean or null, found: {v:?}")),
}
}
pub fn as_string(self) -> Option<String> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_number(self) -> Option<Number> {
match self {
Value::Number(n) => Some(n),
_ => None,
}
}
pub fn as_bool(self) -> Option<bool> {
match self {
Value::Bool(b) => Some(b),
_ => None,
}
}
pub fn is_string(&self) -> bool {
matches!(self, Value::String(_))
}
pub fn is_number(&self) -> bool {
matches!(self, Value::Number(_))
}
pub fn is_bool(&self) -> bool {
matches!(self, Value::Bool(_))
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandType {
Kql,
Kml,
Meta,
Unknown,
}
impl CommandType {
pub fn from(val: &Command) -> CommandType {
match val {
Command::Kql(_) => CommandType::Kql,
Command::Kml(_) => CommandType::Kml,
Command::Meta(_) => CommandType::Meta,
}
}
}
impl fmt::Display for CommandType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CommandType::Kql => write!(f, "KQL"),
CommandType::Kml => write!(f, "KML"),
CommandType::Meta => write!(f, "META"),
CommandType::Unknown => write!(f, "UNKNOWN"),
}
}
}
impl FromStr for CommandType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"KQL" => Ok(CommandType::Kql),
"KML" => Ok(CommandType::Kml),
"META" => Ok(CommandType::Meta),
_ => Ok(CommandType::Unknown),
}
}
}
impl Serialize for CommandType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
struct CommandTypeVisitor;
impl serde::de::Visitor<'_> for CommandTypeVisitor {
type Value = CommandType;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
CommandType::from_str(s).map_err(|err| E::custom(err))
}
}
impl<'de> Deserialize<'de> for CommandType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(CommandTypeVisitor)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum Command {
Kql(KqlQuery),
Kml(KmlStatement),
Meta(MetaCommand),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct KeyValue {
pub key: String,
pub value: Value,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct ConceptClause {
pub matcher: ConceptMatcher,
pub variable: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum ConceptMatcher {
ID(String),
Type(String),
Name(String),
Object { r#type: String, name: String },
}
impl fmt::Display for ConceptMatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConceptMatcher::ID(val) => write!(f, "{{id: {val:?}}}"),
ConceptMatcher::Type(val) => write!(f, "{{type: {val:?}}}"),
ConceptMatcher::Name(val) => write!(f, "{{name: {val:?}}}"),
ConceptMatcher::Object {
r#type: val_type,
name: val_name,
} => {
write!(f, "{{type: {val_type:?}, name: {val_name:?}}}")
}
}
}
}
impl TryFrom<Vec<KeyValue>> for ConceptMatcher {
type Error = String;
fn try_from(values: Vec<KeyValue>) -> Result<Self, Self::Error> {
let mut id: Option<String> = None;
let mut r#type: Option<String> = None;
let mut name: Option<String> = None;
for val in values {
match val.key.as_str() {
"id" => id = val.value.into_opt_string()?,
"type" => r#type = val.value.into_opt_string()?,
"name" => name = val.value.into_opt_string()?,
key => {
return Err(format!("Invalid key in Concept clause: {}", key));
}
}
}
match (id, r#type, name) {
(Some(id_val), None, None) => Ok(ConceptMatcher::ID(id_val)),
(None, Some(type_val), None) => Ok(ConceptMatcher::Type(type_val)),
(None, None, Some(name_val)) => Ok(ConceptMatcher::Name(name_val)),
(None, Some(type_val), Some(name_val)) => Ok(ConceptMatcher::Object {
r#type: type_val,
name: name_val,
}),
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
Err("ConceptMatcher cannot have both id and other attributes".to_string())
}
(None, None, None) => Err(
"ConceptMatcher must have at least one identifying attribute: id, type, or name"
.to_string(),
),
}
}
}
impl ConceptMatcher {
pub fn is_unique(&self) -> bool {
matches!(self, ConceptMatcher::ID(_) | ConceptMatcher::Object { .. })
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct PropositionClause {
pub matcher: PropositionMatcher,
pub variable: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum PropositionMatcher {
ID(String),
Object {
subject: TargetTerm,
predicate: PredTerm,
object: TargetTerm,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum TargetTerm {
Variable(String),
Concept(ConceptMatcher),
Proposition(Box<PropositionMatcher>),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum PredTerm {
Variable(String),
Literal(String),
Alternative(Vec<String>),
MultiHop {
predicate: String,
min: u16,
max: Option<u16>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct KqlQuery {
pub find_clause: FindClause,
pub where_clauses: Vec<WhereClause>,
pub order_by: Option<Vec<OrderByCondition>>,
pub limit: Option<usize>,
pub cursor: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct FindClause {
pub expressions: Vec<FindExpression>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum FindExpression {
Variable(DotPathVar),
Aggregation {
func: AggregationFunction,
var: DotPathVar,
distinct: bool,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct DotPathVar {
pub var: String,
pub path: Vec<String>,
}
impl DotPathVar {
pub fn to_pointer(&self) -> String {
if self.path.is_empty() {
return "".to_string(); }
let mut pointer = String::new();
for component in &self.path {
pointer.push('/');
pointer.push_str(&escape_json_pointer_token(component));
}
pointer
}
pub fn to_pointer_or(&self, field: &str) -> String {
if self.path.is_empty() {
return format!("/{}", escape_json_pointer_token(field));
}
self.to_pointer()
}
}
fn escape_json_pointer_token(token: &str) -> String {
token
.replace('~', "~0") .replace('/', "~1") }
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum AggregationFunction {
Count,
Sum,
Avg,
Min,
Max,
}
impl AggregationFunction {
pub fn calculate(&self, values: &Vec<Json>, distinct: bool) -> Json {
match self {
AggregationFunction::Count => {
if distinct {
let vals: HashSet<&Json> = HashSet::from_iter(values);
vals.len().into()
} else {
values.len().into()
}
}
AggregationFunction::Sum => {
let sum: f64 = values.iter().filter_map(|v| v.as_f64()).sum();
Number::from_f64(sum).map(|v| v.into()).unwrap_or_default()
}
AggregationFunction::Avg => {
let nums: Vec<f64> = values.iter().filter_map(|v| v.as_f64()).collect();
if nums.is_empty() {
Json::Null
} else {
let avg = nums.iter().sum::<f64>() / nums.len() as f64;
Number::from_f64(avg).map(|v| v.into()).unwrap_or_default()
}
}
AggregationFunction::Min => values
.iter()
.filter_map(|v| v.as_f64())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.map(|min| Number::from_f64(min).map(|v| v.into()).unwrap_or_default())
.unwrap_or(Json::Null),
AggregationFunction::Max => values
.iter()
.filter_map(|v| v.as_f64())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.map(|max| Number::from_f64(max).map(|v| v.into()).unwrap_or_default())
.unwrap_or(Json::Null),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum WhereClause {
Concept(ConceptClause),
Proposition(PropositionClause),
Filter(FilterClause),
Not(Vec<WhereClause>),
Optional(Vec<WhereClause>),
Union(Vec<WhereClause>),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct FilterClause {
pub expression: FilterExpression,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum FilterExpression {
Comparison {
left: FilterOperand,
operator: ComparisonOperator,
right: FilterOperand,
},
Logical {
left: Box<FilterExpression>,
operator: LogicalOperator,
right: Box<FilterExpression>,
},
Not(Box<FilterExpression>),
Function {
func: FilterFunction,
args: Vec<FilterOperand>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum FilterOperand {
Variable(DotPathVar),
Literal(Value),
List(Vec<Value>),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum ComparisonOperator {
Equal,
NotEqual,
LessThan,
GreaterThan,
LessEqual,
GreaterEqual,
}
impl ComparisonOperator {
pub fn compare(&self, left: &Json, right: &Json) -> bool {
match self {
ComparisonOperator::Equal => left == right,
ComparisonOperator::NotEqual => left != right,
ComparisonOperator::LessThan => compare_json(left, right)
.map(|o| o == Ordering::Less)
.unwrap_or(false),
ComparisonOperator::GreaterThan => compare_json(left, right)
.map(|o| o == Ordering::Greater)
.unwrap_or(false),
ComparisonOperator::LessEqual => compare_json(left, right)
.map(|o| o != Ordering::Greater)
.unwrap_or(false),
ComparisonOperator::GreaterEqual => compare_json(left, right)
.map(|o| o != Ordering::Less)
.unwrap_or(false),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum LogicalOperator {
And,
Or,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum FilterFunction {
Contains,
StartsWith,
EndsWith,
Regex,
In,
IsNull,
IsNotNull,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct OrderByCondition {
pub variable: DotPathVar,
pub direction: OrderDirection,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum OrderDirection {
Asc,
Desc,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum KmlStatement {
Upsert(Vec<UpsertBlock>),
Delete(DeleteStatement),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct UpsertBlock {
pub items: Vec<UpsertItem>,
pub metadata: Option<Map<String, Json>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum UpsertItem {
Concept(ConceptBlock),
Proposition(PropositionBlock),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct ConceptBlock {
pub handle: Option<String>,
pub concept: ConceptMatcher,
pub set_attributes: Option<Map<String, Json>>,
pub set_propositions: Option<Vec<SetProposition>>,
pub metadata: Option<Map<String, Json>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SetProposition {
pub predicate: String,
pub object: TargetTerm,
pub metadata: Option<Map<String, Json>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct PropositionBlock {
pub handle: Option<String>,
pub proposition: PropositionMatcher,
pub set_attributes: Option<Map<String, Json>>,
pub metadata: Option<Map<String, Json>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum DeleteStatement {
DeleteAttributes {
attributes: Vec<String>,
target: String,
where_clauses: Vec<WhereClause>,
},
DeleteMetadata {
keys: Vec<String>,
target: String,
where_clauses: Vec<WhereClause>,
},
DeletePropositions {
target: String,
where_clauses: Vec<WhereClause>,
},
DeleteConcept {
target: String,
where_clauses: Vec<WhereClause>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum MetaCommand {
Describe(DescribeTarget),
Search(SearchCommand),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum DescribeTarget {
Primer,
Domains,
ConceptTypes {
limit: Option<usize>,
cursor: Option<String>,
},
ConceptType(String),
PropositionTypes {
limit: Option<usize>,
cursor: Option<String>,
},
PropositionType(String),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SearchCommand {
pub target: SearchTarget,
pub term: String,
pub in_type: Option<String>,
pub limit: Option<usize>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum SearchTarget {
Concept,
Proposition,
}
pub fn compare_json(left: &Json, right: &Json) -> Option<Ordering> {
match (left, right) {
(Json::Number(a), Json::Number(b)) => a
.as_f64()
.unwrap_or(0.0)
.partial_cmp(&b.as_f64().unwrap_or(0.0)),
(Json::Bool(a), Json::Bool(b)) => Some(a.cmp(b)),
(Json::Null, Json::Null) => Some(Ordering::Equal),
(Json::String(a), Json::String(b)) => {
if let Ok(a) = Number::from_str(a)
&& let Ok(b) = Number::from_str(b)
{
return a
.as_f64()
.unwrap_or(0.0)
.partial_cmp(&b.as_f64().unwrap_or(0.0));
}
if let Ok(a) = DateTime::parse_from_rfc3339(a)
&& let Ok(b) = DateTime::parse_from_rfc3339(b)
{
return Some(a.cmp(&b));
}
if let Ok(a) = DateTime::parse_from_rfc2822(a)
&& let Ok(b) = DateTime::parse_from_rfc2822(b)
{
return Some(a.cmp(&b));
}
Some(a.cmp(b))
}
_ => None,
}
}