use indexmap::IndexMap;
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub struct PlaceholderValue {
pub(crate) path: String,
pub(crate) optional: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum HoconValue {
Object(IndexMap<String, HoconValue>),
Array(Vec<HoconValue>),
Scalar(ScalarValue),
#[doc(hidden)]
Placeholder(PlaceholderValue),
}
impl HoconValue {
pub fn as_str(&self) -> Option<&str> {
match self {
HoconValue::Scalar(sv) if sv.value_type == ScalarType::String => Some(&sv.raw),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
HoconValue::Scalar(sv) => scalar_as_i64(sv),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
HoconValue::Scalar(sv) => sv.raw.parse::<f64>().ok(),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
HoconValue::Scalar(sv) => match sv.raw.to_lowercase().as_str() {
"true" | "yes" | "on" => Some(true),
"false" | "no" | "off" => Some(false),
_ => None,
},
_ => None,
}
}
pub fn as_object(&self) -> Option<&IndexMap<String, HoconValue>> {
match self {
HoconValue::Object(map) => Some(map),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[HoconValue]> {
match self {
HoconValue::Array(items) => Some(items),
_ => None,
}
}
pub fn is_object(&self) -> bool {
matches!(self, HoconValue::Object(_))
}
pub fn is_array(&self) -> bool {
matches!(self, HoconValue::Array(_))
}
pub fn is_scalar(&self) -> bool {
matches!(self, HoconValue::Scalar(_))
}
pub fn is_null(&self) -> bool {
matches!(
self,
HoconValue::Scalar(sv) if sv.value_type == ScalarType::Null
)
}
}
fn scalar_as_i64(sv: &ScalarValue) -> Option<i64> {
sv.raw
.parse::<i64>()
.ok()
.or_else(|| whole_float_to_i64(&sv.raw))
}
pub(crate) fn whole_float_to_i64(raw: &str) -> Option<i64> {
if !raw.contains(['.', 'e', 'E']) {
return None;
}
let s = raw.trim();
let (neg, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
let (mantissa, exp) = match body.split_once(['e', 'E']) {
Some((m, e)) => (m, e.parse::<i32>().ok()?),
None => (body, 0),
};
let (int_part, frac_part) = match mantissa.split_once('.') {
Some((i, f)) => (i, f),
None => (mantissa, ""),
};
if int_part.is_empty() && frac_part.is_empty() {
return None;
}
if !int_part.bytes().all(|b| b.is_ascii_digit())
|| !frac_part.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let combined = format!("{int_part}{frac_part}");
let digits = combined.trim_start_matches('0');
if digits.is_empty() {
return Some(0);
}
let r = frac_part.len() as i64 - exp as i64;
let mag_str: String = if r <= 0 {
let zeros = (-r) as usize;
if digits.len().saturating_add(zeros) > 19 {
return None;
}
let mut t = String::with_capacity(digits.len() + zeros);
t.push_str(digits);
t.extend(std::iter::repeat_n('0', zeros));
t
} else {
let r = r as usize;
if r >= digits.len() {
return None;
}
let (head, tail) = digits.split_at(digits.len() - r);
if !tail.bytes().all(|b| b == b'0') {
return None;
}
head.to_string()
};
let mag: u64 = mag_str.parse().ok()?; if neg {
match mag.cmp(&((i64::MAX as u64) + 1)) {
std::cmp::Ordering::Less => Some(-(mag as i64)),
std::cmp::Ordering::Equal => Some(i64::MIN),
std::cmp::Ordering::Greater => None,
}
} else {
i64::try_from(mag).ok()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarType {
String,
Number,
Boolean,
Null,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct ScalarValue {
pub raw: String,
pub value_type: ScalarType,
}
impl ScalarValue {
pub fn new(raw: String, value_type: ScalarType) -> Self {
Self { raw, value_type }
}
pub fn string(raw: String) -> Self {
Self {
raw,
value_type: ScalarType::String,
}
}
pub fn null() -> Self {
Self {
raw: "null".to_string(),
value_type: ScalarType::Null,
}
}
pub fn boolean(value: bool) -> Self {
Self {
raw: if value { "true" } else { "false" }.to_string(),
value_type: ScalarType::Boolean,
}
}
pub fn number(raw: String) -> Self {
Self {
raw,
value_type: ScalarType::Number,
}
}
}