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,
},
}
impl fmt::Display for FindExpression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FindExpression::Variable(var) => write!(f, "{}", var),
FindExpression::Aggregation {
func,
var,
distinct,
} => {
let func_name = match func {
AggregationFunction::Count => "COUNT",
AggregationFunction::Sum => "SUM",
AggregationFunction::Avg => "AVG",
AggregationFunction::Min => "MIN",
AggregationFunction::Max => "MAX",
};
if *distinct {
write!(f, "{}(DISTINCT {})", func_name, var)
} else {
write!(f, "{}({})", func_name, var)
}
}
}
}
}
#[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()
}
}
impl fmt::Display for DotPathVar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.path.is_empty() {
write!(f, "?{}", self.var)
} else {
write!(f, "?{}.{}", self.var, self.path.join("."))
}
}
}
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,
pub aggregation: Option<AggregationFunction>,
}
impl OrderByCondition {
pub fn is_aggregation(&self) -> bool {
self.aggregation.is_some()
}
}
#[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>),
Update(UpdateStatement),
Merge(MergeStatement),
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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expect_version: Option<u64>,
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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expect_version: Option<u64>,
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 struct UpdateStatement {
pub target: String,
pub set_attributes: Option<Vec<(String, UpdateValue)>>,
pub set_metadata: Option<Vec<(String, UpdateValue)>>,
pub where_clauses: Vec<WhereClause>,
pub limit: Option<usize>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum UpdateValue {
Json(Json),
Expr(UpdateExpr),
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum UpdateFunction {
Add,
Mul,
Clamp,
Coalesce,
}
impl UpdateFunction {
pub fn calculate(&self, args: &[Json]) -> Json {
fn binary_number_op(
a: &Json,
b: &Json,
int_op: impl Fn(i128, i128) -> Option<i128>,
float_op: impl Fn(f64, f64) -> f64,
) -> Json {
match (as_i128(a), as_i128(b)) {
(Some(x), Some(y)) => int_op(x, y)
.and_then(i128_to_number)
.map(Json::Number)
.unwrap_or_else(|| float_number(float_op(x as f64, y as f64))),
_ => match (a.as_f64(), b.as_f64()) {
(Some(x), Some(y)) => float_number(float_op(x, y)),
_ => Json::Null,
},
}
}
match self {
UpdateFunction::Add => match args {
[a, b] => binary_number_op(a, b, |x, y| x.checked_add(y), |x, y| x + y),
_ => Json::Null,
},
UpdateFunction::Mul => match args {
[a, b] => binary_number_op(a, b, |x, y| x.checked_mul(y), |x, y| x * y),
_ => Json::Null,
},
UpdateFunction::Clamp => match args {
[x, lo, hi] => match (as_i128(x), as_i128(lo), as_i128(hi)) {
(Some(x), Some(lo), Some(hi)) if lo <= hi => i128_to_number(x.clamp(lo, hi))
.map(Json::Number)
.unwrap_or(Json::Null),
_ => match (x.as_f64(), lo.as_f64(), hi.as_f64()) {
(Some(x), Some(lo), Some(hi)) if lo <= hi => float_number(x.clamp(lo, hi)),
_ => Json::Null,
},
},
_ => Json::Null,
},
UpdateFunction::Coalesce => match args {
[x, default] => {
if x.is_null() {
default.clone()
} else {
x.clone()
}
}
_ => Json::Null,
},
}
}
}
fn as_i128(value: &Json) -> Option<i128> {
match value {
Json::Number(n) => {
if let Some(i) = n.as_i64() {
Some(i as i128)
} else {
n.as_u64().map(|u| u as i128)
}
}
_ => None,
}
}
fn i128_to_number(value: i128) -> Option<Number> {
if let Ok(i) = i64::try_from(value) {
Some(Number::from(i))
} else {
u64::try_from(value).ok().map(Number::from)
}
}
fn float_number(value: f64) -> Json {
Number::from_f64(value)
.map(Json::Number)
.unwrap_or_default()
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum UpdateExpr {
Number(Number),
Variable(DotPathVar),
Function {
func: UpdateFunction,
args: Vec<UpdateExpr>,
},
}
impl UpdateExpr {
pub fn evaluate<F>(&self, resolve: &F) -> Json
where
F: Fn(&DotPathVar) -> Json,
{
match self {
UpdateExpr::Number(n) => Json::Number(n.clone()),
UpdateExpr::Variable(path) => resolve(path),
UpdateExpr::Function { func, args } => {
let args: Vec<Json> = args.iter().map(|arg| arg.evaluate(resolve)).collect();
func.calculate(&args)
}
}
}
pub fn referenced_paths(&self) -> Vec<&DotPathVar> {
match self {
UpdateExpr::Number(_) => vec![],
UpdateExpr::Variable(path) => vec![path],
UpdateExpr::Function { args, .. } => {
args.iter().flat_map(|arg| arg.referenced_paths()).collect()
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct MergeStatement {
pub source: String,
pub target: String,
pub where_clauses: Vec<WhereClause>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum MetaCommand {
Describe(DescribeTarget),
Search(SearchCommand),
Export(ExportCommand),
}
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<SearchMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub threshold: Option<Number>,
pub limit: Option<usize>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum SearchMode {
Keyword,
Semantic,
Hybrid,
}
impl fmt::Display for SearchMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SearchMode::Keyword => write!(f, "keyword"),
SearchMode::Semantic => write!(f, "semantic"),
SearchMode::Hybrid => write!(f, "hybrid"),
}
}
}
impl FromStr for SearchMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"keyword" => Ok(SearchMode::Keyword),
"semantic" => Ok(SearchMode::Semantic),
"hybrid" => Ok(SearchMode::Hybrid),
_ => Err(format!(
"Invalid SEARCH mode: {s:?}, expected \"keyword\", \"semantic\", or \"hybrid\""
)),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum SearchTarget {
Concept,
Proposition,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct ExportCommand {
pub target: String,
pub where_clauses: Vec<WhereClause>,
pub limit: Option<usize>,
}
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,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::{cmp::Ordering, str::FromStr};
#[test]
fn find_expression_display_variable() {
let expr = FindExpression::Variable(DotPathVar {
var: "drug".to_string(),
path: vec!["attributes".to_string(), "risk_level".to_string()],
});
assert_eq!(expr.to_string(), "?drug.attributes.risk_level");
}
#[test]
fn find_expression_display_aggregation_without_distinct() {
let expr = FindExpression::Aggregation {
func: AggregationFunction::Count,
var: DotPathVar {
var: "drug".to_string(),
path: vec![],
},
distinct: false,
};
assert_eq!(expr.to_string(), "COUNT(?drug)");
}
#[test]
fn find_expression_display_aggregation_with_distinct() {
let expr = FindExpression::Aggregation {
func: AggregationFunction::Sum,
var: DotPathVar {
var: "drug".to_string(),
path: vec!["score".to_string()],
},
distinct: true,
};
assert_eq!(expr.to_string(), "SUM(DISTINCT ?drug.score)");
}
#[test]
fn value_conversions_display_and_accessors_cover_all_variants() {
assert_eq!(Value::Null.to_string(), "null");
assert_eq!(Value::Bool(true).to_string(), "true");
assert_eq!(Value::Number(Number::from(7)).to_string(), "7");
assert_eq!(Value::String("a\"b".to_string()).to_string(), r#""a\"b""#);
assert_eq!(Json::from(Value::Null), Json::Null);
assert_eq!(Json::from(Value::Bool(false)), Json::Bool(false));
assert_eq!(Json::from(Value::Number(Number::from(3))), json!(3));
assert_eq!(Json::from(Value::String("x".to_string())), json!("x"));
assert_eq!(Value::from("borrowed"), Value::String("borrowed".into()));
assert_eq!(
Value::from("owned".to_string()),
Value::String("owned".into())
);
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(
Value::from(Number::from(42)),
Value::Number(Number::from(42))
);
assert_eq!(Value::try_from(Json::Null).unwrap(), Value::Null);
assert_eq!(Value::try_from(json!(true)).unwrap(), Value::Bool(true));
assert_eq!(
Value::try_from(json!(11)).unwrap(),
Value::Number(Number::from(11))
);
assert_eq!(
Value::try_from(json!("text")).unwrap(),
Value::String("text".into())
);
assert!(
Value::try_from(json!([1]))
.unwrap_err()
.contains("Unsupported")
);
assert_eq!(
Value::String("s".into()).into_opt_string().unwrap(),
Some("s".into())
);
assert_eq!(Value::Null.into_opt_string().unwrap(), None);
assert!(Value::Bool(true).into_opt_string().is_err());
assert_eq!(
Value::Number(Number::from(5)).into_opt_number().unwrap(),
Some(Number::from(5))
);
assert_eq!(Value::Null.into_opt_number().unwrap(), None);
assert!(Value::String("bad".into()).into_opt_number().is_err());
assert_eq!(Value::Bool(false).into_opt_bool().unwrap(), Some(false));
assert_eq!(Value::Null.into_opt_bool().unwrap(), None);
assert!(Value::Number(Number::from(1)).into_opt_bool().is_err());
assert_eq!(Value::String("s".into()).as_string(), Some("s".into()));
assert_eq!(Value::Null.as_string(), None);
assert_eq!(
Value::Number(Number::from(9)).as_number(),
Some(Number::from(9))
);
assert_eq!(Value::Null.as_number(), None);
assert_eq!(Value::Bool(true).as_bool(), Some(true));
assert_eq!(Value::Null.as_bool(), None);
assert!(Value::String("s".into()).is_string());
assert!(Value::Number(Number::from(1)).is_number());
assert!(Value::Bool(true).is_bool());
assert!(Value::Null.is_null());
}
#[test]
fn command_type_display_parse_serde_and_from_command() {
assert_eq!(CommandType::Kql.to_string(), "KQL");
assert_eq!(CommandType::Kml.to_string(), "KML");
assert_eq!(CommandType::Meta.to_string(), "META");
assert_eq!(CommandType::Unknown.to_string(), "UNKNOWN");
assert_eq!(CommandType::from_str("kql").unwrap(), CommandType::Kql);
assert_eq!(CommandType::from_str("KML").unwrap(), CommandType::Kml);
assert_eq!(CommandType::from_str("meta").unwrap(), CommandType::Meta);
assert_eq!(
CommandType::from_str("other").unwrap(),
CommandType::Unknown
);
let serialized = serde_json::to_string(&CommandType::Kml).unwrap();
assert_eq!(serialized, r#""KML""#);
assert_eq!(
serde_json::from_str::<CommandType>(&serialized).unwrap(),
CommandType::Kml
);
let kql = Command::Kql(KqlQuery {
find_clause: FindClause {
expressions: vec![],
},
where_clauses: vec![],
order_by: None,
limit: None,
cursor: None,
});
let kml = Command::Kml(KmlStatement::Upsert(vec![]));
let meta = Command::Meta(MetaCommand::Describe(DescribeTarget::Primer));
assert_eq!(CommandType::from(&kql), CommandType::Kql);
assert_eq!(CommandType::from(&kml), CommandType::Kml);
assert_eq!(CommandType::from(&meta), CommandType::Meta);
}
#[test]
fn concept_matcher_dot_path_and_comparison_helpers_cover_edges() {
assert_eq!(
ConceptMatcher::ID("id1".into()).to_string(),
r#"{id: "id1"}"#
);
assert_eq!(
ConceptMatcher::Type("Drug".into()).to_string(),
r#"{type: "Drug"}"#
);
assert_eq!(
ConceptMatcher::Name("Aspirin".into()).to_string(),
r#"{name: "Aspirin"}"#
);
assert_eq!(
ConceptMatcher::Object {
r#type: "Drug".into(),
name: "Aspirin".into(),
}
.to_string(),
r#"{type: "Drug", name: "Aspirin"}"#
);
assert!(ConceptMatcher::ID("id1".into()).is_unique());
assert!(
ConceptMatcher::Object {
r#type: "Drug".into(),
name: "Aspirin".into(),
}
.is_unique()
);
assert!(!ConceptMatcher::Type("Drug".into()).is_unique());
let invalid = ConceptMatcher::try_from(vec![
KeyValue {
key: "id".into(),
value: "id1".into(),
},
KeyValue {
key: "type".into(),
value: "Drug".into(),
},
])
.unwrap_err();
assert!(invalid.contains("cannot have both id"));
assert!(
ConceptMatcher::try_from(vec![KeyValue {
key: "name".into(),
value: Value::Null,
}])
.unwrap_err()
.contains("must have at least one")
);
let escaped = DotPathVar {
var: "node".into(),
path: vec!["a/b".into(), "c~d".into()],
};
assert_eq!(escaped.to_pointer(), "/a~1b/c~0d");
assert_eq!(escaped.to_pointer_or("ignored"), "/a~1b/c~0d");
let whole_doc = DotPathVar {
var: "node".into(),
path: vec![],
};
assert_eq!(whole_doc.to_pointer(), "");
assert_eq!(whole_doc.to_pointer_or("a/b"), "/a~1b");
let value = json!(2);
assert!(ComparisonOperator::Equal.compare(&value, &json!(2)));
assert!(ComparisonOperator::GreaterEqual.compare(&value, &json!(2)));
assert!(!ComparisonOperator::GreaterEqual.compare(&json!(1), &json!(2)));
assert!(!ComparisonOperator::LessThan.compare(&json!("x"), &json!(2)));
}
#[test]
fn update_function_calculate_covers_numeric_and_null_semantics() {
assert_eq!(
UpdateFunction::Add.calculate(&[json!(5), json!(1)]),
json!(6)
);
assert_eq!(
UpdateFunction::Add.calculate(&[json!(5), json!(-2)]),
json!(3)
);
assert_eq!(
UpdateFunction::Mul.calculate(&[json!(4), json!(3)]),
json!(12)
);
assert_eq!(
UpdateFunction::Mul.calculate(&[json!(0.5), json!(4)]),
json!(2.0)
);
assert_eq!(
UpdateFunction::Clamp.calculate(&[json!(15), json!(0), json!(10)]),
json!(10)
);
assert_eq!(
UpdateFunction::Clamp.calculate(&[json!(1.2), json!(0.0), json!(1.0)]),
json!(1.0)
);
assert_eq!(
UpdateFunction::Coalesce.calculate(&[Json::Null, json!(0)]),
json!(0)
);
assert_eq!(
UpdateFunction::Coalesce.calculate(&[json!(7), json!(0)]),
json!(7)
);
assert_eq!(
UpdateFunction::Add.calculate(&[Json::Null, json!(1)]),
Json::Null
);
assert_eq!(
UpdateFunction::Mul.calculate(&[json!("text"), json!(2)]),
Json::Null
);
assert_eq!(
UpdateFunction::Clamp.calculate(&[json!(1), json!(10), json!(0)]),
Json::Null );
}
#[test]
fn update_expr_evaluate_resolves_target_paths() {
let expr = UpdateExpr::Function {
func: UpdateFunction::Add,
args: vec![
UpdateExpr::Function {
func: UpdateFunction::Coalesce,
args: vec![
UpdateExpr::Variable(DotPathVar {
var: "t".to_string(),
path: vec!["attributes".to_string(), "count".to_string()],
}),
UpdateExpr::Number(Number::from(0)),
],
},
UpdateExpr::Number(Number::from(1)),
],
};
assert_eq!(expr.evaluate(&|_| Json::Null), json!(1));
assert_eq!(expr.evaluate(&|_| json!(41)), json!(42));
assert_eq!(expr.evaluate(&|_| json!("not a number")), Json::Null);
assert_eq!(
expr.referenced_paths()
.into_iter()
.map(|p| p.to_string())
.collect::<Vec<_>>(),
vec!["?t.attributes.count".to_string()]
);
let decay = UpdateExpr::Function {
func: UpdateFunction::Clamp,
args: vec![
UpdateExpr::Function {
func: UpdateFunction::Mul,
args: vec![
UpdateExpr::Variable(DotPathVar {
var: "t".to_string(),
path: vec!["metadata".to_string(), "confidence".to_string()],
}),
UpdateExpr::Number(Number::from_f64(0.9).unwrap()),
],
},
UpdateExpr::Number(Number::from_f64(0.0).unwrap()),
UpdateExpr::Number(Number::from_f64(1.0).unwrap()),
],
};
assert_eq!(decay.evaluate(&|_| json!(0.5)), json!(0.45));
assert_eq!(decay.evaluate(&|_| json!(2.0)), json!(1.0));
assert_eq!(decay.evaluate(&|_| Json::Null), Json::Null);
}
#[test]
fn search_mode_display_and_from_str_roundtrip() {
for (mode, s) in [
(SearchMode::Keyword, "keyword"),
(SearchMode::Semantic, "semantic"),
(SearchMode::Hybrid, "hybrid"),
] {
assert_eq!(mode.to_string(), s);
assert_eq!(SearchMode::from_str(s).unwrap(), mode);
assert_eq!(SearchMode::from_str(&s.to_ascii_uppercase()).unwrap(), mode);
}
assert!(SearchMode::from_str("fuzzy").is_err());
}
#[test]
fn aggregation_display_and_json_comparison_cover_remaining_branches() {
let var = DotPathVar {
var: "drug".into(),
path: vec![],
};
for (func, expected) in [
(AggregationFunction::Avg, "AVG(?drug)"),
(AggregationFunction::Min, "MIN(?drug)"),
(AggregationFunction::Max, "MAX(?drug)"),
] {
let expr = FindExpression::Aggregation {
func,
var: var.clone(),
distinct: false,
};
assert_eq!(expr.to_string(), expected);
}
let values = vec![json!(1), json!(2), json!(2), json!("skip")];
assert_eq!(
AggregationFunction::Count.calculate(&values, true),
json!(3)
);
assert_eq!(
AggregationFunction::Avg.calculate(&values, false),
json!(5.0 / 3.0)
);
assert_eq!(
AggregationFunction::Min.calculate(&values, false),
json!(1.0)
);
assert_eq!(
AggregationFunction::Max.calculate(&values, false),
json!(2.0)
);
assert_eq!(
AggregationFunction::Avg.calculate(&vec![json!("x")], false),
Json::Null
);
assert_eq!(
compare_json(&json!(false), &json!(true)),
Some(Ordering::Less)
);
assert_eq!(
compare_json(&Json::Null, &Json::Null),
Some(Ordering::Equal)
);
assert_eq!(
compare_json(&json!("9"), &json!("10")),
Some(Ordering::Less)
);
assert_eq!(
compare_json(
&json!("2025-01-01T00:00:00Z"),
&json!("2025-01-02T00:00:00Z")
),
Some(Ordering::Less)
);
assert_eq!(
compare_json(
&json!("Tue, 1 Jul 2003 10:52:37 +0200"),
&json!("Tue, 1 Jul 2003 10:53:37 +0200")
),
Some(Ordering::Less)
);
assert_eq!(
compare_json(&json!("abc"), &json!("abd")),
Some(Ordering::Less)
);
assert_eq!(compare_json(&json!("abc"), &json!(1)), None);
}
}