use std::os::raw::c_int;
use std::ptr::{self, NonNull};
use crate::error::Error;
use crate::ffi;
use crate::{Format, SerializeOptions};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtKind {
OffsetDateTime,
LocalDateTime,
LocalDate,
LocalTime,
EnumLiteral,
CharLiteral,
NumberSpecial,
}
impl ExtKind {
fn to_c(self) -> c_int {
match self {
ExtKind::OffsetDateTime => 0,
ExtKind::LocalDateTime => 1,
ExtKind::LocalDate => 2,
ExtKind::LocalTime => 3,
ExtKind::EnumLiteral => 4,
ExtKind::CharLiteral => 5,
ExtKind::NumberSpecial => 6,
}
}
pub(crate) fn from_c(kind: c_int) -> Option<Self> {
Some(match kind {
0 => ExtKind::OffsetDateTime,
1 => ExtKind::LocalDateTime,
2 => ExtKind::LocalDate,
3 => ExtKind::LocalTime,
4 => ExtKind::EnumLiteral,
5 => ExtKind::CharLiteral,
6 => ExtKind::NumberSpecial,
_ => return None,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Uint(u64),
Float(f64),
Str(String),
Extended {
kind: ExtKind,
text: String,
},
Seq(Vec<Value>),
Map(Vec<(Value, Value)>),
}
impl From<bool> for Value {
fn from(v: bool) -> Self {
Value::Bool(v)
}
}
impl From<i64> for Value {
fn from(v: i64) -> Self {
Value::Int(v)
}
}
impl From<i32> for Value {
fn from(v: i32) -> Self {
Value::Int(v as i64)
}
}
impl From<u64> for Value {
fn from(v: u64) -> Self {
match i64::try_from(v) {
Ok(i) => Value::Int(i),
Err(_) => Value::Uint(v),
}
}
}
impl From<f64> for Value {
fn from(v: f64) -> Self {
Value::Float(v)
}
}
impl From<&str> for Value {
fn from(v: &str) -> Self {
Value::Str(v.to_owned())
}
}
impl From<String> for Value {
fn from(v: String) -> Self {
Value::Str(v)
}
}
impl From<Vec<Value>> for Value {
fn from(v: Vec<Value>) -> Self {
Value::Seq(v)
}
}
impl From<&Value> for Value {
fn from(v: &Value) -> Self {
v.clone()
}
}
impl Value {
pub fn serialize(&self, format: Format) -> Result<String, Error> {
self.serialize_with(format, SerializeOptions::default())
}
pub fn serialize_with(
&self,
format: Format,
options: SerializeOptions,
) -> Result<String, Error> {
self.serialize_ffi(format, options.into())
}
pub(crate) fn serialize_ffi(
&self,
format: Format,
ffi_options: ffi::FigSerializeOptions,
) -> Result<String, Error> {
let mut raw = ptr::null_mut();
Error::from_status(unsafe { ffi::fig_value_create(&mut raw) })?;
NonNull::new(raw).ok_or(Error::Internal)?;
let guard = ValueGuard(raw);
let root = build(guard.0, self)?;
let mut ptr_out: *const u8 = ptr::null();
let mut len: usize = 0;
let ffi_format: ffi::FigFormat = format.into();
Error::from_status(unsafe {
ffi::fig_value_serialize_opts(
guard.0,
root,
ffi_format as i32,
&ffi_options,
&mut ptr_out,
&mut len,
)
})?;
let bytes = if len == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(ptr_out, len) }
};
Ok(std::str::from_utf8(bytes)
.map_err(|_| Error::Utf8)?
.to_owned())
}
pub fn diagnose(
&self,
format: Format,
options: SerializeOptions,
) -> Result<Vec<crate::Warning>, Error> {
let mut raw = ptr::null_mut();
Error::from_status(unsafe { ffi::fig_value_create(&mut raw) })?;
NonNull::new(raw).ok_or(Error::Internal)?;
let guard = ValueGuard(raw);
let root = build(guard.0, self)?;
let ffi_format: ffi::FigFormat = format.into();
let ffi_options: ffi::FigSerializeOptions = options.into();
let mut count: usize = 0;
Error::from_status(unsafe {
ffi::fig_value_diagnose(guard.0, root, ffi_format as c_int, &ffi_options, &mut count)
})?;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let mut w = ffi::FigWarning::new();
Error::from_status(unsafe { ffi::fig_value_warning(guard.0, i, &mut w) })?;
out.push(unsafe { crate::Warning::from_ffi(&w) });
}
Ok(out)
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn is_bool(&self) -> bool {
matches!(self, Value::Bool(_))
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn is_i64(&self) -> bool {
self.as_i64().is_some()
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Int(i) => Some(*i),
Value::Uint(u) => i64::try_from(*u).ok(),
_ => None,
}
}
pub fn is_u64(&self) -> bool {
self.as_u64().is_some()
}
pub fn as_u64(&self) -> Option<u64> {
match self {
Value::Uint(u) => Some(*u),
Value::Int(i) => u64::try_from(*i).ok(),
_ => None,
}
}
pub fn is_f64(&self) -> bool {
matches!(self, Value::Float(_))
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Float(f) => Some(*f),
Value::Int(i) => Some(*i as f64),
Value::Uint(u) => Some(*u as f64),
_ => None,
}
}
pub fn is_str(&self) -> bool {
matches!(self, Value::Str(_))
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::Str(s) => Some(s),
_ => None,
}
}
pub fn is_extended(&self) -> bool {
matches!(self, Value::Extended { .. })
}
pub fn as_extended(&self) -> Option<(ExtKind, &str)> {
match self {
Value::Extended { kind, text } => Some((*kind, text.as_str())),
_ => None,
}
}
pub fn is_seq(&self) -> bool {
matches!(self, Value::Seq(_))
}
pub fn as_seq(&self) -> Option<&[Value]> {
match self {
Value::Seq(items) => Some(items),
_ => None,
}
}
pub fn is_mapping(&self) -> bool {
matches!(self, Value::Map(_))
}
pub fn as_mapping(&self) -> Option<&[(Value, Value)]> {
match self {
Value::Map(entries) => Some(entries),
_ => None,
}
}
pub fn eq_canonical(&self, other: &Value) -> bool {
match (self, other) {
(Value::Float(a), Value::Float(b)) => a.to_bits() == b.to_bits(),
(Value::Int(i), Value::Uint(u)) | (Value::Uint(u), Value::Int(i)) => {
u64::try_from(*i).is_ok_and(|i| i == *u)
}
(Value::Seq(a), Value::Seq(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_canonical(y))
}
(Value::Map(a), Value::Map(b)) => {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|((ak, av), (bk, bv))| ak.eq_canonical(bk) && av.eq_canonical(bv))
}
_ => self == other,
}
}
pub fn parse_number(raw: &str, is_float: bool) -> Result<Value, Error> {
if !is_float {
if let Ok(i) = raw.parse::<i64>() {
return Ok(Value::Int(i));
}
if let Ok(u) = raw.parse::<u64>() {
return Ok(Value::Uint(u));
}
}
Value::parse_float(raw)
.map(Value::Float)
.ok_or_else(|| Error::Number(raw.to_owned()))
}
pub fn parse_float(raw: &str) -> Option<f64> {
match raw {
".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
"-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
_ => raw.parse::<f64>().ok(),
}
}
pub fn get<I: Index>(&self, index: I) -> Option<&Value> {
index.index_into(self)
}
}
pub(crate) fn map_get<'a>(entries: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
entries.iter().rev().find_map(|(k, v)| match k {
Value::Str(s) if s == key => Some(v),
_ => None,
})
}
pub trait Index: sealed::Sealed {
#[doc(hidden)]
fn index_into<'v>(&self, value: &'v Value) -> Option<&'v Value>;
}
impl Index for str {
fn index_into<'v>(&self, value: &'v Value) -> Option<&'v Value> {
match value {
Value::Map(entries) => map_get(entries, self),
_ => None,
}
}
}
impl Index for String {
fn index_into<'v>(&self, value: &'v Value) -> Option<&'v Value> {
self.as_str().index_into(value)
}
}
impl Index for usize {
fn index_into<'v>(&self, value: &'v Value) -> Option<&'v Value> {
match value {
Value::Seq(items) => items.get(*self),
_ => None,
}
}
}
impl<T: ?Sized + Index> Index for &T {
fn index_into<'v>(&self, value: &'v Value) -> Option<&'v Value> {
(**self).index_into(value)
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for str {}
impl Sealed for String {}
impl Sealed for usize {}
impl<T: ?Sized + Sealed> Sealed for &T {}
}
impl<I: Index> std::ops::Index<I> for Value {
type Output = Value;
fn index(&self, index: I) -> &Value {
static NULL: Value = Value::Null;
self.get(index).unwrap_or(&NULL)
}
}
struct ValueGuard(*mut ffi::FigValue);
impl Drop for ValueGuard {
fn drop(&mut self) {
unsafe { ffi::fig_value_destroy(self.0) };
}
}
fn build(handle: *mut ffi::FigValue, value: &Value) -> Result<ffi::FigNodeId, Error> {
let mut id: ffi::FigNodeId = 0;
let status = unsafe {
match value {
Value::Null => ffi::fig_value_null(handle, &mut id),
Value::Bool(b) => ffi::fig_value_bool(handle, *b, &mut id),
Value::Int(n) => ffi::fig_value_int(handle, *n, &mut id),
Value::Uint(n) => ffi::fig_value_uint(handle, *n, &mut id),
Value::Float(f) => {
let text = format_float(*f);
ffi::fig_value_number(handle, text.as_ptr(), text.len(), true, &mut id)
}
Value::Str(s) => ffi::fig_value_string(handle, s.as_ptr(), s.len(), &mut id),
Value::Extended { kind, text } => {
ffi::fig_value_extended(handle, kind.to_c(), text.as_ptr(), text.len(), &mut id)
}
Value::Seq(items) => {
let ids = items
.iter()
.map(|it| build(handle, it))
.collect::<Result<Vec<_>, _>>()?;
ffi::fig_value_seq(handle, ids.as_ptr(), ids.len(), &mut id)
}
Value::Map(entries) => {
let kvs = entries
.iter()
.map(|(k, v)| {
Ok(ffi::FigKeyValue {
key: build(handle, k)?,
value: build(handle, v)?,
})
})
.collect::<Result<Vec<_>, Error>>()?;
ffi::fig_value_map(handle, kvs.as_ptr(), kvs.len(), &mut id)
}
}
};
Error::from_status(status)?;
Ok(id)
}
pub(crate) fn value_text(value: &Value, format: Format) -> Result<String, Error> {
let mut ffi_options: crate::ffi::FigSerializeOptions = SerializeOptions::default().into();
if format == Format::Fig {
ffi_options.flow = 1;
}
let mut s = value.serialize_ffi(format, ffi_options)?;
if s.ends_with('\n') {
s.pop();
}
Ok(s)
}
pub(crate) fn value_text_with(
value: &Value,
format: Format,
options: SerializeOptions,
) -> Result<String, Error> {
let ffi_options: crate::ffi::FigSerializeOptions = options.into(); let mut s = value.serialize_ffi(format, ffi_options)?;
if s.ends_with('\n') {
s.pop();
}
Ok(s)
}
fn format_float(f: f64) -> String {
if f.is_nan() {
return ".nan".to_string();
}
if f.is_infinite() {
return if f < 0.0 { "-.inf" } else { ".inf" }.to_string();
}
let sci = format!("{f:e}");
let (mantissa, exponent) = sci.split_once('e').expect("LowerExp always writes an `e`");
let e: i32 = exponent
.parse()
.expect("LowerExp writes a decimal exponent");
let kk = e + 1;
if (-4..=16).contains(&kk) {
let s = f.to_string();
if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
format!("{s}.0")
} else {
s
}
} else if mantissa.contains('.') {
sci
} else {
format!("{mantissa}.0e{exponent}")
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Value {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::Deserialize;
use serde::de::{MapAccess, SeqAccess, Visitor};
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("any fig value")
}
fn visit_bool<E>(self, v: bool) -> Result<Value, E> {
Ok(Value::Bool(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Value, E> {
Ok(Value::Int(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Value, E> {
Ok(Value::from(v))
}
fn visit_i128<E>(self, v: i128) -> Result<Value, E> {
Ok(i64::try_from(v)
.map(Value::Int)
.unwrap_or(Value::Float(v as f64)))
}
fn visit_u128<E>(self, v: u128) -> Result<Value, E> {
Ok(u64::try_from(v)
.map(Value::from)
.unwrap_or(Value::Float(v as f64)))
}
fn visit_f64<E>(self, v: f64) -> Result<Value, E> {
Ok(Value::Float(v))
}
fn visit_str<E>(self, v: &str) -> Result<Value, E> {
Ok(Value::Str(v.to_owned()))
}
fn visit_string<E>(self, v: String) -> Result<Value, E> {
Ok(Value::Str(v))
}
fn visit_unit<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_none<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_some<D: serde::Deserializer<'de>>(self, d: D) -> Result<Value, D::Error> {
Value::deserialize(d)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
let mut items = Vec::new();
while let Some(e) = seq.next_element::<Value>()? {
items.push(e);
}
Ok(Value::Seq(items))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
let mut entries = Vec::new();
while let Some((k, v)) = map.next_entry::<Value, Value>()? {
entries.push((k, v));
}
Ok(Value::Map(entries))
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_and_serializes_to_multiple_formats() {
let v = Value::Map(vec![
(Value::Str("name".into()), Value::Str("fig".into())),
(
Value::Str("nums".into()),
Value::Seq(vec![Value::Int(1), Value::Int(2)]),
),
]);
assert_eq!(
v.serialize(Format::Yaml).unwrap(),
"name: fig\nnums: [1, 2]\n"
);
assert_eq!(
v.serialize(Format::Json).unwrap(),
"{\n \"name\": \"fig\",\n \"nums\": [\n 1,\n 2\n ]\n}\n",
);
}
#[test]
fn quotes_and_round_trip_safe_strings() {
assert_eq!(
Value::Str("a: b".into()).serialize(Format::Yaml).unwrap(),
"'a: b'\n"
);
let v = Value::Map(vec![(
Value::Str("s".into()),
Value::Str("multi\nline".into()),
)]);
assert_eq!(
v.serialize(Format::Yaml).unwrap(),
"s: |-\n multi\n line\n"
);
}
#[test]
fn float_text_switches_to_scientific_outside_the_readable_band() {
assert_eq!(format_float(1e300), "1.0e300");
assert_eq!(format_float(-1e300), "-1.0e300");
assert_eq!(format_float(1e-7), "1.0e-7");
assert_eq!(format_float(1.5e-7), "1.5e-7");
assert_eq!(format_float(f64::MAX), "1.7976931348623157e308");
assert_eq!(format_float(5e-324), "5.0e-324");
assert_eq!(format_float(0.0), "0.0");
assert_eq!(format_float(-0.0), "-0.0");
assert_eq!(format_float(1.0), "1.0");
assert_eq!(format_float(0.1), "0.1");
assert_eq!(format_float(1e-5), "0.00001");
assert_eq!(format_float(1e-6), "1.0e-6");
assert_eq!(format_float(1e15), "1000000000000000.0");
assert_eq!(format_float(1e16), "1.0e16");
assert_eq!(format_float(f64::NAN), ".nan");
assert_eq!(format_float(f64::INFINITY), ".inf");
assert_eq!(format_float(f64::NEG_INFINITY), "-.inf");
}
#[test]
fn float_text_round_trips_bit_exactly() {
for f in [
1e300,
-1e300,
1e-7,
1.5e-7,
f64::MAX,
5e-324,
0.0,
-0.0,
0.1,
1e-6,
1e15,
1e16,
123456789012345680000.0,
] {
let text = format_float(f);
let back = Value::parse_float(&text)
.unwrap_or_else(|| panic!("`{text}` must parse back as a float"));
assert_eq!(back.to_bits(), f.to_bits(), "round trip of `{text}`");
}
}
#[test]
#[cfg(feature = "toml")]
fn null_value_is_unsupported_in_toml() {
let v = Value::Map(vec![(Value::Str("k".into()), Value::Null)]);
assert!(matches!(
v.serialize(Format::Toml),
Err(Error::UnsupportedFormat)
));
}
#[test]
fn document_reads_into_value() {
use crate::{Document, Format};
let doc =
Document::parse(b"title: Hi\nnums:\n- 1\n- 2\nratio: 1.5\n", Format::Yaml).unwrap();
let v = doc.to_value().unwrap();
assert_eq!(
v,
Value::Map(vec![
("title".into(), "Hi".into()),
("nums".into(), Value::Seq(vec![1i64.into(), 2i64.into()])),
("ratio".into(), 1.5.into()),
]),
);
assert_eq!(
v.serialize(Format::Yaml).unwrap(),
"title: Hi\nnums: [1, 2]\nratio: 1.5\n"
);
}
#[test]
#[cfg(feature = "toml")]
fn toml_datetimes_round_trip_as_extended() {
use crate::{Document, ExtKind, Format};
let src = "d = 2026-06-18\nt = 07:32:00\n";
let v = Document::parse(src.as_bytes(), Format::Toml)
.unwrap()
.to_value()
.unwrap();
assert_eq!(
v,
Value::Map(vec![
(
"d".into(),
Value::Extended {
kind: ExtKind::LocalDate,
text: "2026-06-18".into()
}
),
(
"t".into(),
Value::Extended {
kind: ExtKind::LocalTime,
text: "07:32:00".into()
}
),
])
);
assert_eq!(v.serialize(Format::Toml).unwrap(), src);
}
#[test]
#[cfg(feature = "zon")]
fn zon_literals_round_trip_as_extended() {
use crate::{Document, ExtKind, Format};
let v = Document::parse(b".{ .mode = .fast, .c = 'a' }", Format::Zon)
.unwrap()
.to_value()
.unwrap();
assert_eq!(
v,
Value::Map(vec![
(
"mode".into(),
Value::Extended {
kind: ExtKind::EnumLiteral,
text: "fast".into()
}
),
(
"c".into(),
Value::Extended {
kind: ExtKind::CharLiteral,
text: "97".into()
}
),
])
);
}
#[test]
fn value_diagnose_reports_degraded_datetime() {
use crate::{Format, SerializeOptions, WarningCode};
let v = Value::Map(vec![(
"when".into(),
Value::Extended {
kind: ExtKind::OffsetDateTime,
text: "1979-05-27T07:32:00Z".into(),
},
)]);
let warns = v
.diagnose(Format::Json, SerializeOptions::default())
.unwrap();
assert_eq!(warns.len(), 1);
assert_eq!(warns[0].code, WarningCode::TypeDegraded);
assert_eq!(warns[0].path, "when");
assert_eq!(warns[0].note, "string");
}
#[test]
#[cfg(feature = "toml")]
fn constructed_extended_serializes() {
let v = Value::Map(vec![(
"when".into(),
Value::Extended {
kind: ExtKind::OffsetDateTime,
text: "1979-05-27T07:32:00Z".into(),
},
)]);
assert_eq!(
v.serialize(Format::Toml).unwrap(),
"when = 1979-05-27T07:32:00Z\n"
);
}
#[test]
fn accessors_match_kind() {
assert!(Value::Null.is_null());
assert!(!Value::Bool(false).is_null());
assert!(Value::Bool(true).is_bool());
assert_eq!(Value::Bool(true).as_bool(), Some(true));
assert_eq!(Value::Int(1).as_bool(), None);
assert!(Value::Str("hi".into()).is_str());
assert_eq!(Value::Str("hi".into()).as_str(), Some("hi"));
assert_eq!(Value::Int(1).as_str(), None);
let ext = Value::Extended {
kind: ExtKind::LocalDate,
text: "2026-06-18".into(),
};
assert!(ext.is_extended());
assert_eq!(ext.as_extended(), Some((ExtKind::LocalDate, "2026-06-18")));
assert_eq!(Value::Null.as_extended(), None);
let seq = Value::Seq(vec![1i64.into(), 2i64.into()]);
assert!(seq.is_seq());
assert_eq!(seq.as_seq(), Some(&[Value::Int(1), Value::Int(2)][..]));
assert_eq!(Value::Null.as_seq(), None);
let map = Value::Map(vec![("a".into(), 1i64.into())]);
assert!(map.is_mapping());
assert_eq!(
map.as_mapping(),
Some(&[(Value::Str("a".into()), Value::Int(1))][..])
);
assert_eq!(Value::Null.as_mapping(), None);
}
#[test]
fn numeric_accessors_widen_between_int_and_uint() {
assert!(Value::Int(5).is_i64());
assert_eq!(Value::Int(5).as_i64(), Some(5));
assert!(Value::Int(5).is_u64());
assert_eq!(Value::Int(5).as_u64(), Some(5));
assert!(!Value::Int(-5).is_u64());
assert_eq!(Value::Int(-5).as_u64(), None);
assert!(Value::Uint(u64::MAX).is_u64());
assert_eq!(Value::Uint(u64::MAX).as_u64(), Some(u64::MAX));
assert!(!Value::Uint(u64::MAX).is_i64());
assert_eq!(Value::Uint(u64::MAX).as_i64(), None);
assert!(Value::Uint(5).is_i64());
assert_eq!(Value::Uint(5).as_i64(), Some(5));
assert!(Value::Float(1.5).is_f64());
assert_eq!(Value::Float(1.5).as_f64(), Some(1.5));
assert!(!Value::Int(5).is_f64());
assert_eq!(Value::Int(5).as_f64(), Some(5.0));
assert_eq!(Value::Uint(5).as_f64(), Some(5.0));
assert_eq!(Value::Bool(true).as_f64(), None);
}
#[test]
fn float_specials_round_trip_through_the_public_parser() {
for (text, expect_nan) in [(".inf", false), ("-.inf", false), (".nan", true)] {
let v = Value::parse_number(text, true).unwrap();
assert!(
v.is_f64(),
"`{text}` must stay a float, not become a string"
);
let f = v.as_f64().unwrap();
assert_eq!(f.is_nan(), expect_nan);
assert_eq!(format_float(f), text);
}
assert!(".inf".parse::<f64>().is_err());
assert!(Value::parse_float(".inf").unwrap().is_infinite());
assert!(Value::parse_float("inf").unwrap().is_infinite());
assert!(Value::parse_float("nope").is_none());
}
#[test]
fn parse_number_classifies_by_kind() {
assert_eq!(Value::parse_number("3", false).unwrap(), Value::Int(3));
assert_eq!(Value::parse_number("-3", false).unwrap(), Value::Int(-3));
assert_eq!(Value::parse_number("3", true).unwrap(), Value::Float(3.0));
assert_eq!(
Value::parse_number("18446744073709551615", false).unwrap(),
Value::Uint(u64::MAX)
);
assert!(matches!(
Value::parse_number("184467440737095516150", false).unwrap(),
Value::Float(_)
));
assert!(matches!(
Value::parse_number("zero", false),
Err(Error::Number(_))
));
}
#[test]
fn small_unsigned_integers_construct_as_int() {
assert_eq!(Value::from(3u64), Value::Int(3));
assert_eq!(Value::from(3u64), Value::from(3i64));
assert_eq!(Value::from(i64::MAX as u64), Value::Int(i64::MAX));
let past = i64::MAX as u64 + 1;
assert_eq!(Value::from(past), Value::Uint(past));
assert_eq!(Value::from(u64::MAX), Value::Uint(u64::MAX));
assert_eq!(Value::Uint(3).as_i64(), Some(3));
assert!(Value::Int(3).eq_canonical(&Value::Uint(3)));
}
#[test]
fn eq_canonical_is_reflexive_over_nan() {
let nan = Value::Float(f64::NAN);
assert!(nan != nan);
assert!(nan.eq_canonical(&nan));
let doc = |f: f64| {
Value::Map(vec![(
"xs".into(),
Value::Seq(vec![Value::Float(f), Value::Int(1)]),
)])
};
assert!(doc(f64::NAN) != doc(f64::NAN));
assert!(doc(f64::NAN).eq_canonical(&doc(f64::NAN)));
assert!(doc(f64::INFINITY).eq_canonical(&doc(f64::INFINITY)));
assert!(!doc(f64::NAN).eq_canonical(&doc(1.0)));
assert!(Value::Float(0.0) == Value::Float(-0.0));
assert!(!Value::Float(0.0).eq_canonical(&Value::Float(-0.0)));
assert!(Value::Str("a".into()).eq_canonical(&Value::Str("a".into())));
assert!(!Value::Str("a".into()).eq_canonical(&Value::Int(1)));
assert!(!Value::Seq(vec![Value::Int(1)]).eq_canonical(&Value::Seq(vec![])));
let m = |a: &str, b: &str| {
Value::Map(vec![(a.into(), Value::Int(1)), (b.into(), Value::Int(2))])
};
assert!(m("a", "b").eq_canonical(&m("a", "b")));
assert!(!m("a", "b").eq_canonical(&m("b", "a")));
}
#[test]
#[cfg(feature = "derive")]
fn unsigned_to_value_impls_are_canonical_too() {
use crate::ToValue;
assert_eq!(3u8.to_value(), Value::Int(3));
assert_eq!(3u32.to_value(), Value::Int(3));
assert_eq!(3usize.to_value(), Value::Int(3));
assert_eq!(3u64.to_value(), 3i64.to_value());
assert_eq!(u64::MAX.to_value(), Value::Uint(u64::MAX));
}
#[test]
#[cfg(feature = "yaml")]
fn parsed_and_constructed_integers_agree() {
use crate::{Document, Format};
let parsed = Document::parse(b"n: 3\n", Format::Yaml)
.unwrap()
.to_value()
.unwrap();
assert_eq!(parsed, Value::Map(vec![("n".into(), Value::from(3u64))]));
}
#[test]
fn get_looks_up_by_string_key_or_seq_index() {
let map = Value::Map(vec![
("title".into(), "Hi".into()),
("count".into(), 42i64.into()),
("title".into(), "Overridden".into()),
("nums".into(), Value::Seq(vec![1i64.into(), 2i64.into()])),
]);
assert_eq!(map.get("title"), Some(&Value::Str("Overridden".into())));
assert_eq!(map.get("count"), Some(&Value::Int(42)));
assert_eq!(map.get("missing"), None);
assert_eq!(Value::Seq(vec![]).get("title"), None);
assert_eq!(map.get(String::from("count")), Some(&Value::Int(42)));
assert_eq!(map.get("nums").and_then(|v| v.get(1)), Some(&Value::Int(2)));
assert_eq!(map.get("nums").and_then(|v| v.get(9)), None);
assert_eq!(Value::Null.get(0), None);
}
#[test]
fn index_operator_returns_null_instead_of_panicking() {
let map = Value::Map(vec![(
"nums".into(),
Value::Seq(vec![1i64.into(), 2i64.into()]),
)]);
assert_eq!(map["nums"][0], Value::Int(1));
assert_eq!(map["nums"][9], Value::Null);
assert_eq!(map["missing"], Value::Null);
assert_eq!(Value::Null["a"], Value::Null);
assert_eq!(Value::Null[0], Value::Null);
}
}