use serde_json::{Number, Value};
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq)]
pub struct GhaValue {
pub json: Value,
pub origin: Option<String>,
}
impl GhaValue {
pub fn new(json: Value) -> Self {
Self { json, origin: None }
}
pub fn with_origin(json: Value, origin: impl Into<String>) -> Self {
Self {
json,
origin: Some(origin.into()),
}
}
pub fn missing() -> Self {
Self::new(Value::String(String::new()))
}
pub fn truthy(&self) -> bool {
match &self.json {
Value::Null => false,
Value::Bool(value) => *value,
Value::Number(number) => number.as_f64().is_some_and(|value| value != 0.0),
Value::String(value) => !value.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
}
fn string_coerce(value: &Value) -> Option<String> {
match value {
Value::Null => Some(String::new()),
Value::Bool(value) => Some(value.to_string()),
Value::Number(value) => Some(value.to_string()),
Value::String(value) => Some(value.clone()),
Value::Array(_) | Value::Object(_) => None,
}
}
pub fn string_for_render(value: &Value) -> String {
string_coerce(value).unwrap_or_else(|| serde_json::to_string(value).unwrap_or_default())
}
pub fn loose_equal(left: &GhaValue, right: &GhaValue) -> bool {
if same_scalar_type(&left.json, &right.json) {
return match (&left.json, &right.json) {
(Value::String(left), Value::String(right)) => left.eq_ignore_ascii_case(right),
(Value::Number(left), Value::Number(right)) => left.as_f64() == right.as_f64(),
_ => left.json == right.json,
};
}
if matches!(left.json, Value::Array(_) | Value::Object(_))
|| matches!(right.json, Value::Array(_) | Value::Object(_))
{
return left.origin.is_some() && left.origin == right.origin;
}
let left = number_coerce(&left.json);
let right = number_coerce(&right.json);
matches!((left, right), (Some(left), Some(right)) if left == right)
}
pub fn loose_compare(left: &GhaValue, right: &GhaValue) -> Option<Ordering> {
let left = number_coerce(&left.json)?;
let right = number_coerce(&right.json)?;
left.partial_cmp(&right)
}
pub fn number_coerce(value: &Value) -> Option<f64> {
match value {
Value::Null => Some(0.0),
Value::Bool(true) => Some(1.0),
Value::Bool(false) => Some(0.0),
Value::Number(value) => value.as_f64(),
Value::String(value) => {
if value.is_empty() {
Some(0.0)
} else {
serde_json::from_str::<Number>(value)
.ok()
.and_then(|number| number.as_f64())
}
}
Value::Array(_) | Value::Object(_) => None,
}
}
fn same_scalar_type(left: &Value, right: &Value) -> bool {
matches!(
(left, right),
(Value::Null, Value::Null)
| (Value::Bool(_), Value::Bool(_))
| (Value::Number(_), Value::Number(_))
| (Value::String(_), Value::String(_))
)
}