use super::{SnapshotError, SnapshotResult};
#[derive(Debug, Clone)]
#[must_use = "JsonField is a borrowed view; call as_u64 / as_i64 / etc. to extract"]
#[non_exhaustive]
pub enum JsonField<'a> {
Value(&'a serde_json::Value),
Missing(SnapshotError),
}
impl<'a> JsonField<'a> {
pub fn is_present(&self) -> bool {
!matches!(self, JsonField::Missing(_))
}
pub fn raw(&self) -> Option<&'a serde_json::Value> {
match self {
JsonField::Value(v) => Some(*v),
JsonField::Missing(_) => None,
}
}
pub fn error(&self) -> Option<&SnapshotError> {
match self {
JsonField::Missing(err) => Some(err),
_ => None,
}
}
pub fn get(&self, path: &str) -> JsonField<'a> {
match self {
JsonField::Value(v) => walk_json_path(v, path),
JsonField::Missing(err) => JsonField::Missing(err.clone()),
}
}
pub fn as_u64(&self) -> SnapshotResult<u64> {
match self {
JsonField::Value(v) => json_to_u64(v),
JsonField::Missing(err) => Err(err.clone()),
}
}
pub fn as_i64(&self) -> SnapshotResult<i64> {
match self {
JsonField::Value(v) => json_to_i64(v),
JsonField::Missing(err) => Err(err.clone()),
}
}
pub fn as_f64(&self) -> SnapshotResult<f64> {
match self {
JsonField::Value(v) => json_to_f64(v),
JsonField::Missing(err) => Err(err.clone()),
}
}
pub fn as_bool(&self) -> SnapshotResult<bool> {
match self {
JsonField::Value(serde_json::Value::Bool(b)) => Ok(*b),
JsonField::Value(other) => Err(SnapshotError::TypeMismatch {
expected: "bool".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
JsonField::Missing(err) => Err(err.clone()),
}
}
pub fn as_str(&self) -> SnapshotResult<&'a str> {
match self {
JsonField::Value(serde_json::Value::String(s)) => Ok(s.as_str()),
JsonField::Value(other) => Err(SnapshotError::TypeMismatch {
expected: "str".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
JsonField::Missing(err) => Err(err.clone()),
}
}
pub fn as_u64_array(&self) -> SnapshotResult<Vec<u64>> {
json_to_typed_array(self, json_to_u64, "u64")
}
pub fn as_u32_array(&self) -> SnapshotResult<Vec<u32>> {
json_to_typed_array(
self,
|v| {
json_to_u64(v).and_then(|x| {
u32::try_from(x).map_err(|_| SnapshotError::TypeMismatch {
expected: "u32".to_string(),
actual: "Uint(>u32::MAX)".to_string(),
requested: String::new(),
})
})
},
"u32",
)
}
pub fn as_i64_array(&self) -> SnapshotResult<Vec<i64>> {
json_to_typed_array(self, json_to_i64, "i64")
}
pub fn as_f64_array(&self) -> SnapshotResult<Vec<f64>> {
json_to_typed_array(self, json_to_f64, "f64")
}
pub fn as_bool_array(&self) -> SnapshotResult<Vec<bool>> {
json_to_typed_array(
self,
|v| match v {
serde_json::Value::Bool(b) => Ok(*b),
other => Err(SnapshotError::TypeMismatch {
expected: "bool".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
},
"bool",
)
}
pub fn iter_members(&self) -> impl Iterator<Item = JsonField<'a>> + '_ {
let elements: &[serde_json::Value] = match self {
JsonField::Value(serde_json::Value::Array(a)) => a.as_slice(),
_ => &[],
};
elements.iter().map(JsonField::Value)
}
}
fn json_to_typed_array<T, F>(
field: &JsonField<'_>,
coerce: F,
type_name: &'static str,
) -> SnapshotResult<Vec<T>>
where
F: Fn(&serde_json::Value) -> SnapshotResult<T>,
{
let value = match field {
JsonField::Value(v) => *v,
JsonField::Missing(err) => return Err(err.clone()),
};
let elements = match value {
serde_json::Value::Array(a) => a.as_slice(),
other => {
return Err(SnapshotError::TypeMismatch {
expected: format!("[{type_name}]"),
actual: describe_json_kind(other),
requested: String::new(),
});
}
};
let mut out = Vec::with_capacity(elements.len());
for element in elements {
out.push(coerce(element)?);
}
Ok(out)
}
pub fn stats_path<'a>(value: &'a serde_json::Value, path: &str) -> JsonField<'a> {
walk_json_path(value, path)
}
fn walk_json_path<'a>(root: &'a serde_json::Value, path: &str) -> JsonField<'a> {
if path.is_empty() {
return JsonField::Value(root);
}
let mut cursor: &serde_json::Value = root;
let mut walked = String::new();
for component in path.split('.') {
if component.is_empty() {
return JsonField::Missing(SnapshotError::EmptyPathComponent {
requested: path.to_string(),
});
}
match cursor {
serde_json::Value::Object(map) => {
let Some(next) = map.get(component) else {
let mut available: Vec<String> = map.keys().cloned().collect();
available.sort();
return JsonField::Missing(SnapshotError::FieldNotFound {
requested: path.to_string(),
walked: walked.clone(),
component: component.to_string(),
available,
});
};
cursor = next;
}
other => {
return JsonField::Missing(SnapshotError::NotAStruct {
requested: path.to_string(),
walked: walked.clone(),
component: component.to_string(),
kind: describe_json_kind(other),
});
}
}
if !walked.is_empty() {
walked.push('.');
}
walked.push_str(component);
}
JsonField::Value(cursor)
}
fn describe_json_kind(v: &serde_json::Value) -> String {
match v {
serde_json::Value::Null => "Null",
serde_json::Value::Bool(_) => "Bool",
serde_json::Value::Number(_) => "Number",
serde_json::Value::String(_) => "String",
serde_json::Value::Array(_) => "Array",
serde_json::Value::Object(_) => "Object",
}
.to_string()
}
fn json_to_u64(v: &serde_json::Value) -> SnapshotResult<u64> {
match v {
serde_json::Value::Number(n) => {
if let Some(u) = n.as_u64() {
Ok(u)
} else if let Some(i) = n.as_i64() {
if i < 0 {
Err(SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: "Int(negative)".to_string(),
requested: String::new(),
})
} else {
Ok(i as u64)
}
} else if let Some(f) = n.as_f64() {
if !f.is_finite() || f < 0.0 {
Err(SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: "Float(non-coercible)".to_string(),
requested: String::new(),
})
} else if f.fract() != 0.0 {
Err(SnapshotError::TypeMismatch {
expected: "integer".to_string(),
actual: "non-integer float".to_string(),
requested: String::new(),
})
} else {
Ok(f as u64)
}
} else {
Err(SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: "Number(unrepresentable)".to_string(),
requested: String::new(),
})
}
}
serde_json::Value::Bool(b) => Ok(u64::from(*b)),
serde_json::Value::String(s) => s.parse::<u64>().map_err(|_| SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: "String(non-numeric)".to_string(),
requested: String::new(),
}),
other => Err(SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
}
}
fn json_to_i64(v: &serde_json::Value) -> SnapshotResult<i64> {
match v {
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(i)
} else if let Some(u) = n.as_u64() {
if u > i64::MAX as u64 {
Err(SnapshotError::TypeMismatch {
expected: "i64".to_string(),
actual: "Uint(>i64::MAX)".to_string(),
requested: String::new(),
})
} else {
Ok(u as i64)
}
} else if let Some(f) = n.as_f64() {
if !f.is_finite() {
Err(SnapshotError::TypeMismatch {
expected: "i64".to_string(),
actual: "Float(non-finite)".to_string(),
requested: String::new(),
})
} else if f.fract() != 0.0 {
Err(SnapshotError::TypeMismatch {
expected: "integer".to_string(),
actual: "non-integer float".to_string(),
requested: String::new(),
})
} else {
Ok(f as i64)
}
} else {
Err(SnapshotError::TypeMismatch {
expected: "i64".to_string(),
actual: "Number(unrepresentable)".to_string(),
requested: String::new(),
})
}
}
serde_json::Value::Bool(b) => Ok(i64::from(*b)),
serde_json::Value::String(s) => s.parse::<i64>().map_err(|_| SnapshotError::TypeMismatch {
expected: "i64".to_string(),
actual: "String(non-numeric)".to_string(),
requested: String::new(),
}),
other => Err(SnapshotError::TypeMismatch {
expected: "i64".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
}
}
fn json_to_f64(v: &serde_json::Value) -> SnapshotResult<f64> {
match v {
serde_json::Value::Number(n) => n.as_f64().ok_or(SnapshotError::TypeMismatch {
expected: "f64".to_string(),
actual: "Number(unrepresentable)".to_string(),
requested: String::new(),
}),
serde_json::Value::String(s) => s.parse::<f64>().map_err(|_| SnapshotError::TypeMismatch {
expected: "f64".to_string(),
actual: "String(non-numeric)".to_string(),
requested: String::new(),
}),
other => Err(SnapshotError::TypeMismatch {
expected: "f64".to_string(),
actual: describe_json_kind(other),
requested: String::new(),
}),
}
}