use crate::error::{Result, TqlError};
use serde_json::Value as JsonValue;
use std::net::IpAddr;
use std::str::FromStr;
pub fn get_field<'a>(record: &'a JsonValue, field_path: &str) -> Result<Option<&'a JsonValue>> {
let parts: Vec<&str> = field_path.split('.').collect();
let mut current = record;
for part in parts {
match current {
JsonValue::Object(map) => {
match map.get(part) {
Some(value) => current = value,
None => return Ok(None), }
}
JsonValue::Array(arr) => {
if let Ok(index) = part.parse::<usize>() {
match arr.get(index) {
Some(value) => current = value,
None => return Ok(None), }
} else {
return Ok(None);
}
}
_ => {
return Ok(None);
}
}
}
Ok(Some(current))
}
pub fn field_exists(record: &JsonValue, field_path: &str) -> Result<bool> {
Ok(get_field(record, field_path)?.is_some())
}
pub fn get_field_as_string(record: &JsonValue, field_path: &str) -> Result<Option<String>> {
match get_field(record, field_path)? {
Some(JsonValue::String(s)) => Ok(Some(s.clone())),
Some(JsonValue::Number(n)) => Ok(Some(n.to_string())),
Some(JsonValue::Bool(b)) => Ok(Some(b.to_string())),
Some(JsonValue::Null) => Ok(Some("null".to_string())),
Some(_) => Ok(None), None => Ok(None),
}
}
pub fn get_field_as_i64(record: &JsonValue, field_path: &str) -> Result<Option<i64>> {
match get_field(record, field_path)? {
Some(JsonValue::Number(n)) => Ok(n.as_i64()),
Some(JsonValue::String(s)) => Ok(s.parse::<i64>().ok()),
Some(_) => Ok(None),
None => Ok(None),
}
}
pub fn get_field_as_f64(record: &JsonValue, field_path: &str) -> Result<Option<f64>> {
match get_field(record, field_path)? {
Some(JsonValue::Number(n)) => Ok(n.as_f64()),
Some(JsonValue::String(s)) => Ok(s.parse::<f64>().ok()),
Some(_) => Ok(None),
None => Ok(None),
}
}
pub fn get_field_as_bool(record: &JsonValue, field_path: &str) -> Result<Option<bool>> {
match get_field(record, field_path)? {
Some(JsonValue::Bool(b)) => Ok(Some(*b)),
Some(JsonValue::String(s)) => {
let lower = s.to_lowercase();
match lower.as_str() {
"true" | "yes" | "1" => Ok(Some(true)),
"false" | "no" | "0" => Ok(Some(false)),
_ => Ok(None),
}
}
Some(JsonValue::Number(n)) => {
if let Some(i) = n.as_i64() {
Ok(Some(i != 0))
} else {
Ok(None)
}
}
Some(_) => Ok(None),
None => Ok(None),
}
}
pub fn get_field_as_array<'a>(
record: &'a JsonValue,
field_path: &str,
) -> Result<Option<&'a Vec<JsonValue>>> {
match get_field(record, field_path)? {
Some(JsonValue::Array(arr)) => Ok(Some(arr)),
Some(_) => Ok(None),
None => Ok(None),
}
}
pub fn set_field(record: &mut JsonValue, field_path: &str, value: JsonValue) -> Result<()> {
let parts: Vec<&str> = field_path.split('.').collect();
if parts.is_empty() {
return Err(TqlError::FieldError(format!(
"Empty field path: {}",
field_path
)));
}
let mut current = record;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
match current {
JsonValue::Object(map) => {
map.insert(part.to_string(), value);
return Ok(());
}
_ => {
return Err(TqlError::FieldError(format!(
"Cannot set field '{}' on non-object",
field_path
)));
}
}
} else {
match current {
JsonValue::Object(map) => {
current = map
.entry(part.to_string())
.or_insert_with(|| JsonValue::Object(serde_json::Map::new()));
}
_ => {
return Err(TqlError::FieldError(format!(
"Cannot navigate through non-object at '{}' in path '{}'",
part, field_path
)));
}
}
}
}
Ok(())
}
pub const COERCING_TYPE_HINTS: &[&str] = &[
"bool", "boolean", "decimal", "double", "float", "int", "integer", "ip", "number", "str",
"string",
];
pub const STRUCTURAL_TYPE_HINTS: &[(&str, &str)] = &[
(
"array",
"the comparators already iterate array-valued fields",
),
(
"date",
"TQL has no date comparison semantics to coerce into",
),
(
"geo",
"selects a geo-shaped field; no scalar conversion applies",
),
("list", "alias of `array`"),
(
"object",
"selects an object-shaped field; no scalar conversion applies",
),
];
pub fn known_type_hints() -> Vec<&'static str> {
let mut all: Vec<&'static str> = COERCING_TYPE_HINTS.to_vec();
all.extend(STRUCTURAL_TYPE_HINTS.iter().map(|(name, _)| *name));
all.sort_unstable();
all
}
fn py_str(value: &JsonValue) -> String {
match value {
JsonValue::Null => "None".to_string(),
JsonValue::Bool(true) => "True".to_string(),
JsonValue::Bool(false) => "False".to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => s.clone(),
JsonValue::Array(_) | JsonValue::Object(_) => py_repr(value),
}
}
fn py_repr(value: &JsonValue) -> String {
match value {
JsonValue::String(s) => {
let (quote, escape_quote) = if s.contains('\'') && !s.contains('"') {
('"', false)
} else {
('\'', true)
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\'' if escape_quote => out.push_str("\\'"),
other => out.push(other),
}
}
out.push(quote);
out
}
JsonValue::Array(items) => {
let inner: Vec<String> = items.iter().map(py_repr).collect();
format!("[{}]", inner.join(", "))
}
JsonValue::Object(map) => {
let inner: Vec<String> = map
.iter()
.map(|(k, v)| format!("{}: {}", py_repr(&JsonValue::String(k.clone())), py_repr(v)))
.collect();
format!("{{{}}}", inner.join(", "))
}
other => py_str(other),
}
}
fn unconvertible(what: &str, field_name: &str, value: &JsonValue) -> TqlError {
TqlError::TypeHintCoercion(format!(
"Cannot convert value to {} for field '{}': {}",
what,
field_name,
py_str(value)
))
}
fn to_i64(value: &JsonValue) -> Option<i64> {
match value {
JsonValue::Number(n) => n.as_i64().or_else(|| n.as_f64().map(|f| f.trunc() as i64)),
JsonValue::Bool(b) => Some(i64::from(*b)),
JsonValue::String(s) => s.trim().parse::<i64>().ok(),
_ => None,
}
}
fn to_f64(value: &JsonValue) -> Option<f64> {
match value {
JsonValue::Number(n) => n.as_f64(),
JsonValue::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
JsonValue::String(s) => s.trim().parse::<f64>().ok(),
_ => None,
}
}
fn is_ip_network(s: &str) -> bool {
let Some((addr, prefix)) = s.split_once('/') else {
return addr_ok(s);
};
let Ok(ip) = IpAddr::from_str(addr) else {
return false;
};
let Ok(bits) = prefix.parse::<u8>() else {
return false;
};
match ip {
IpAddr::V4(_) => bits <= 32,
IpAddr::V6(_) => bits <= 128,
}
}
fn addr_ok(s: &str) -> bool {
IpAddr::from_str(s).is_ok()
}
pub fn apply_type_hint(
value: &JsonValue,
type_hint: &str,
field_name: &str,
operator: &str,
) -> Result<JsonValue> {
if value.is_null() {
return Ok(value.clone());
}
if STRUCTURAL_TYPE_HINTS
.iter()
.any(|(name, _)| *name == type_hint)
{
return Ok(value.clone());
}
match type_hint {
"ip" => {
let s = py_str(value);
if addr_ok(&s) {
return Ok(JsonValue::String(s));
}
if (operator == "cidr" || operator == "not_cidr") && is_ip_network(&s) {
return Ok(JsonValue::String(s));
}
Err(TqlError::TypeHintCoercion(format!(
"Invalid IP address format for field '{}': {}",
field_name, s
)))
}
"integer" | "int" => to_i64(value)
.map(|i| JsonValue::Number(i.into()))
.ok_or_else(|| unconvertible("integer", field_name, value)),
"float" | "double" | "decimal" => {
let f = to_f64(value).ok_or_else(|| unconvertible("float", field_name, value))?;
number_from_f64(f).ok_or_else(|| unconvertible("float", field_name, value))
}
"number" => {
let f = to_f64(value).ok_or_else(|| unconvertible("number", field_name, value))?;
let integral = f.is_finite() && f.fract() == 0.0 && !py_str(value).contains('.');
let representable = f >= (i64::MIN as f64) && f <= (i64::MAX as f64);
if integral && representable {
Ok(JsonValue::Number((f as i64).into()))
} else {
number_from_f64(f).ok_or_else(|| unconvertible("number", field_name, value))
}
}
"boolean" | "bool" => match value {
JsonValue::Bool(b) => Ok(JsonValue::Bool(*b)),
JsonValue::String(s) => {
let lower = s.to_lowercase();
if lower == "true" || s == "1" {
Ok(JsonValue::Bool(true))
} else if lower == "false" || s == "0" {
Ok(JsonValue::Bool(false))
} else {
Err(unconvertible("boolean", field_name, value))
}
}
_ => Err(unconvertible("boolean", field_name, value)),
},
"string" | "str" => Ok(JsonValue::String(hint_str(value))),
_ => {
Err(unknown_type_hint_error(type_hint, field_name))
}
}
}
pub fn unknown_type_hint_error(type_hint: &str, field_name: &str) -> TqlError {
TqlError::TypeError(format!(
"Unknown type hint '{}' for field '{}'. Known hints: {}",
type_hint,
field_name,
known_type_hints().join(", ")
))
}
fn hint_str(value: &JsonValue) -> String {
match value {
JsonValue::Bool(true) => "true".to_string(),
JsonValue::Bool(false) => "false".to_string(),
other => py_str(other),
}
}
fn number_from_f64(f: f64) -> Option<JsonValue> {
serde_json::Number::from_f64(f).map(JsonValue::Number)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_get_field_simple() {
let record = json!({
"name": "John",
"age": 30
});
let name = get_field(&record, "name").unwrap();
assert_eq!(name, Some(&json!("John")));
let age = get_field(&record, "age").unwrap();
assert_eq!(age, Some(&json!(30)));
}
#[test]
fn test_get_field_nested() {
let record = json!({
"user": {
"profile": {
"name": "John",
"age": 30
}
}
});
let name = get_field(&record, "user.profile.name").unwrap();
assert_eq!(name, Some(&json!("John")));
}
#[test]
fn test_get_field_nonexistent() {
let record = json!({
"name": "John"
});
let result = get_field(&record, "nonexistent").unwrap();
assert_eq!(result, None);
let result = get_field(&record, "user.profile.name").unwrap();
assert_eq!(result, None);
}
#[test]
fn test_get_field_array_index() {
let record = json!({
"tags": ["rust", "tql", "parser"]
});
let tag = get_field(&record, "tags.1").unwrap();
assert_eq!(tag, Some(&json!("tql")));
}
#[test]
fn test_field_exists() {
let record = json!({
"user": {
"name": "John"
}
});
assert!(field_exists(&record, "user.name").unwrap());
assert!(!field_exists(&record, "user.age").unwrap());
}
#[test]
fn test_get_field_as_string() {
let record = json!({
"name": "John",
"age": 30,
"active": true
});
assert_eq!(
get_field_as_string(&record, "name").unwrap(),
Some("John".to_string())
);
assert_eq!(
get_field_as_string(&record, "age").unwrap(),
Some("30".to_string())
);
assert_eq!(
get_field_as_string(&record, "active").unwrap(),
Some("true".to_string())
);
}
#[test]
fn test_get_field_as_i64() {
let record = json!({
"age": 30,
"count": "42"
});
assert_eq!(get_field_as_i64(&record, "age").unwrap(), Some(30));
assert_eq!(get_field_as_i64(&record, "count").unwrap(), Some(42));
}
#[test]
fn test_get_field_as_bool() {
let record = json!({
"active": true,
"enabled": "yes",
"disabled": "no"
});
assert_eq!(get_field_as_bool(&record, "active").unwrap(), Some(true));
assert_eq!(get_field_as_bool(&record, "enabled").unwrap(), Some(true));
assert_eq!(get_field_as_bool(&record, "disabled").unwrap(), Some(false));
}
#[test]
fn test_set_field() {
let mut record = json!({});
set_field(&mut record, "name", json!("John")).unwrap();
assert_eq!(get_field(&record, "name").unwrap(), Some(&json!("John")));
set_field(&mut record, "user.profile.age", json!(30)).unwrap();
assert_eq!(
get_field(&record, "user.profile.age").unwrap(),
Some(&json!(30))
);
}
}