use crate::error::{Result, XervError};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Value(pub JsonValue);
impl Value {
pub fn null() -> Self {
Self(JsonValue::Null)
}
pub fn bool(v: bool) -> Self {
Self(JsonValue::Bool(v))
}
pub fn int(v: i64) -> Self {
Self(JsonValue::Number(v.into()))
}
pub fn float(v: f64) -> Self {
Self(serde_json::Number::from_f64(v).map_or(JsonValue::Null, JsonValue::Number))
}
pub fn string(v: impl Into<String>) -> Self {
Self(JsonValue::String(v.into()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() {
return Ok(Self::null());
}
serde_json::from_slice(bytes)
.map(Self)
.map_err(|e| XervError::Serialization(format!("Failed to parse value: {}", e)))
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
serde_json::to_vec(&self.0)
.map_err(|e| XervError::Serialization(format!("Failed to serialize value: {}", e)))
}
pub fn is_null(&self) -> bool {
self.0.is_null()
}
pub fn get_field(&self, path: &str) -> Option<Value> {
let path = path.strip_prefix("$.").unwrap_or(path);
let mut current = &self.0;
for part in path.split('.') {
if let Some((field, idx_str)) = part.split_once('[') {
current = current.get(field)?;
let idx_str = idx_str.strip_suffix(']')?;
let idx: usize = idx_str.parse().ok()?;
current = current.get(idx)?;
} else {
current = current.get(part)?;
}
}
Some(Value(current.clone()))
}
pub fn get_string(&self, path: &str) -> Option<String> {
self.get_field(path).and_then(|v| v.as_string())
}
pub fn get_f64(&self, path: &str) -> Option<f64> {
self.get_field(path).and_then(|v| v.as_f64())
}
pub fn get_bool(&self, path: &str) -> Option<bool> {
self.get_field(path).and_then(|v| v.as_bool())
}
pub fn as_string(&self) -> Option<String> {
match &self.0 {
JsonValue::String(s) => Some(s.clone()),
JsonValue::Number(n) => Some(n.to_string()),
JsonValue::Bool(b) => Some(b.to_string()),
JsonValue::Null => None,
_ => Some(self.0.to_string()),
}
}
pub fn as_f64(&self) -> Option<f64> {
match &self.0 {
JsonValue::Number(n) => n.as_f64(),
JsonValue::String(s) => s.parse().ok(),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match &self.0 {
JsonValue::Bool(b) => Some(*b),
JsonValue::String(s) => match s.to_lowercase().as_str() {
"true" | "1" | "yes" => Some(true),
"false" | "0" | "no" => Some(false),
_ => None,
},
JsonValue::Number(n) => Some(n.as_f64().map_or(false, |v| v != 0.0)),
JsonValue::Null => Some(false),
_ => None,
}
}
pub fn equals_str(&self, other: &str) -> bool {
self.as_string().map_or(false, |s| s == other)
}
pub fn field_equals(&self, path: &str, value: &str) -> bool {
self.get_field(path).map_or(false, |v| v.equals_str(value))
}
pub fn field_greater_than(&self, path: &str, threshold: f64) -> bool {
self.get_f64(path).map_or(false, |v| v > threshold)
}
pub fn field_less_than(&self, path: &str, threshold: f64) -> bool {
self.get_f64(path).map_or(false, |v| v < threshold)
}
pub fn field_matches(&self, path: &str, pattern: &str) -> bool {
let Some(field_value) = self.get_string(path) else {
return false;
};
regex::Regex::new(pattern)
.map(|re| re.is_match(&field_value))
.unwrap_or(false)
}
pub fn field_is_true(&self, path: &str) -> bool {
self.get_bool(path).unwrap_or(false)
}
pub fn field_is_false(&self, path: &str) -> bool {
self.get_bool(path).map_or(false, |b| !b)
}
pub fn inner(&self) -> &JsonValue {
&self.0
}
pub fn into_inner(self) -> JsonValue {
self.0
}
}
impl Default for Value {
fn default() -> Self {
Self::null()
}
}
impl From<JsonValue> for Value {
fn from(v: JsonValue) -> Self {
Self(v)
}
}
impl From<Value> for JsonValue {
fn from(v: Value) -> Self {
v.0
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Self::string(s)
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Self::string(s)
}
}
impl From<i64> for Value {
fn from(v: i64) -> Self {
Self::int(v)
}
}
impl From<f64> for Value {
fn from(v: f64) -> Self {
Self::float(v)
}
}
impl From<bool> for Value {
fn from(v: bool) -> Self {
Self::bool(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn value_from_bytes() {
let bytes = br#"{"name": "test", "score": 0.95}"#;
let value = Value::from_bytes(bytes).unwrap();
assert_eq!(value.get_string("name"), Some("test".to_string()));
assert_eq!(value.get_f64("score"), Some(0.95));
}
#[test]
fn value_nested_field_access() {
let value = Value(json!({
"result": {
"status": "success",
"data": {
"count": 42
}
}
}));
assert_eq!(
value.get_string("result.status"),
Some("success".to_string())
);
assert_eq!(value.get_f64("result.data.count"), Some(42.0));
}
#[test]
fn value_jsonpath_prefix() {
let value = Value(json!({"score": 0.9}));
assert_eq!(value.get_f64("score"), Some(0.9));
assert_eq!(value.get_f64("$.score"), Some(0.9));
}
#[test]
fn value_array_access() {
let value = Value(json!({
"items": [
{"name": "first"},
{"name": "second"}
]
}));
assert_eq!(value.get_string("items[0].name"), Some("first".to_string()));
assert_eq!(
value.get_string("items[1].name"),
Some("second".to_string())
);
}
#[test]
fn field_equals() {
let value = Value(json!({"status": "active"}));
assert!(value.field_equals("status", "active"));
assert!(!value.field_equals("status", "inactive"));
}
#[test]
fn field_greater_than() {
let value = Value(json!({"score": 0.85}));
assert!(value.field_greater_than("score", 0.8));
assert!(!value.field_greater_than("score", 0.9));
}
#[test]
fn field_less_than() {
let value = Value(json!({"temperature": 25.5}));
assert!(value.field_less_than("temperature", 30.0));
assert!(!value.field_less_than("temperature", 20.0));
}
#[test]
fn field_matches() {
let value = Value(json!({"email": "user@example.com"}));
assert!(value.field_matches("email", r"^[\w.+-]+@[\w.-]+\.\w+$"));
assert!(!value.field_matches("email", r"^invalid"));
}
#[test]
fn field_bool_checks() {
let value = Value(json!({"success": true, "failed": false}));
assert!(value.field_is_true("success"));
assert!(!value.field_is_true("failed"));
assert!(value.field_is_false("failed"));
assert!(!value.field_is_false("success"));
}
#[test]
fn empty_bytes_returns_null() {
let value = Value::from_bytes(&[]).unwrap();
assert!(value.is_null());
}
#[test]
fn missing_field_returns_none() {
let value = Value(json!({"a": 1}));
assert!(value.get_field("missing").is_none());
assert!(value.get_f64("missing").is_none());
}
}