use std::os::raw::c_int;
use std::ptr::{self, NonNull};
use crate::{Format, SerializeOptions};
use crate::error::Error;
use crate::ffi;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
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 {
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 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> {
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();
let ffi_options: ffi::FigSerializeOptions = options.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)
}
}
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 s = value.serialize(format)?;
if s.ends_with('\n') {
s.pop();
}
Ok(s)
}
pub(crate) fn number_from_raw(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));
}
}
parse_yaml_float(raw)
.map(Value::Float)
.ok_or_else(|| Error::Number(raw.to_owned()))
}
pub(crate) fn parse_yaml_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(),
}
}
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 s = f.to_string();
if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
format!("{s}.0")
} else {
s
}
}
#[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::Uint(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::Uint)
.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:\n- 1\n- 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]
#[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:\n- 1\n- 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"
);
}
}