reifydb_export/render/
value.rs1use reifydb_value::value::Value;
5
6use crate::error::RenderError;
7
8pub fn render_value(value: &Value) -> Result<String, RenderError> {
9 match value {
10 Value::None {
11 ..
12 } => Ok("none".to_string()),
13 Value::Boolean(b) => Ok(if *b {
14 "true"
15 } else {
16 "false"
17 }
18 .to_string()),
19 Value::Float4(f) => {
20 if f.value().is_finite() {
21 Ok(f.to_string())
22 } else {
23 Err(RenderError::NonFiniteFloat)
24 }
25 }
26 Value::Float8(f) => {
27 if f.value().is_finite() {
28 Ok(f.to_string())
29 } else {
30 Err(RenderError::NonFiniteFloat)
31 }
32 }
33 Value::Int1(v) => Ok(v.to_string()),
34 Value::Int2(v) => Ok(v.to_string()),
35 Value::Int4(v) => Ok(v.to_string()),
36 Value::Int8(v) => Ok(v.to_string()),
37 Value::Int16(v) => Ok(v.to_string()),
38 Value::Uint1(v) => Ok(v.to_string()),
39 Value::Uint2(v) => Ok(v.to_string()),
40 Value::Uint4(v) => Ok(v.to_string()),
41 Value::Uint8(v) => Ok(v.to_string()),
42 Value::Uint16(v) => Ok(v.to_string()),
43 Value::Int(v) => Ok(v.to_string()),
44 Value::Uint(v) => Ok(v.to_string()),
45 Value::Decimal(d) => Ok(format!("'{}'", d)),
46 Value::Utf8(s) => render_text(s),
47 Value::Date(d) => Ok(format!("@{}", d)),
48 Value::Time(t) => Ok(format!("@{}", t)),
49 Value::DateTime(dt) => Ok(format!("@{}", dt)),
50 Value::Duration(dur) => Ok(format!("@{}", dur.to_iso_string())),
51 Value::Blob(b) => {
52 let display = b.to_string();
53 let hex = display.strip_prefix("0x").unwrap_or(&display);
54 Ok(format!("blob::hex('{}')", hex))
55 }
56 Value::Uuid4(u) => Ok(format!("'{}'", u)),
57 Value::Uuid7(u) => Ok(format!("'{}'", u)),
58 Value::IdentityId(id) => Ok(format!("'{}'", id)),
59 Value::DictionaryId(_) => Err(RenderError::Unsupported("DictionaryId")),
60 Value::Any(_) => Err(RenderError::Unsupported("Any")),
61 Value::Type(_) => Err(RenderError::Unsupported("Type")),
62 Value::List(_) => Err(RenderError::Unsupported("List")),
63 Value::Record(_) => Err(RenderError::Unsupported("Record")),
64 Value::Tuple(_) => Err(RenderError::Unsupported("Tuple")),
65 }
66}
67
68pub fn render_text(s: &str) -> Result<String, RenderError> {
69 let has_single = s.contains('\'');
70 let has_double = s.contains('"');
71 if has_single && has_double {
72 return Err(RenderError::UnrepresentableText);
73 }
74 if has_single {
75 Ok(format!("\"{}\"", s))
76 } else {
77 Ok(format!("'{}'", s))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use reifydb_value::value::{
84 blob::Blob, date::Date, datetime::DateTime, duration::Duration, time::Time, value_type::ValueType,
85 };
86
87 use super::*;
88
89 #[test]
90 fn none_renders_as_none() {
91 assert_eq!(render_value(&Value::none()).unwrap(), "none");
92 }
93
94 #[test]
95 fn booleans_render_unquoted() {
96 assert_eq!(render_value(&Value::Boolean(true)).unwrap(), "true");
97 assert_eq!(render_value(&Value::Boolean(false)).unwrap(), "false");
98 }
99
100 #[test]
101 fn integers_render_as_decimal_including_negatives() {
102 assert_eq!(render_value(&Value::Int4(-100)).unwrap(), "-100");
103 assert_eq!(render_value(&Value::Int1(i8::MIN)).unwrap(), "-128");
104 assert_eq!(render_value(&Value::Uint8(u64::MAX)).unwrap(), "18446744073709551615");
105 }
106
107 #[test]
108 fn finite_floats_render_and_non_finite_fails_loud() {
109 assert_eq!(render_value(&Value::float8(0.0)).unwrap(), "0");
110 assert_eq!(render_value(&Value::float8(-2.5)).unwrap(), "-2.5");
111 assert_eq!(render_value(&Value::float8(f64::INFINITY)), Err(RenderError::NonFiniteFloat));
112 assert_eq!(render_value(&Value::float4(f32::NEG_INFINITY)), Err(RenderError::NonFiniteFloat));
113 }
114
115 #[test]
116 fn text_quote_selection_and_unrepresentable() {
117 assert_eq!(render_value(&Value::Utf8("Alice".to_string())).unwrap(), "'Alice'");
118 assert_eq!(render_value(&Value::Utf8(String::new())).unwrap(), "''");
119 assert_eq!(render_value(&Value::Utf8("it's".to_string())).unwrap(), "\"it's\"");
120 assert_eq!(render_value(&Value::Utf8("say \"hi\"".to_string())).unwrap(), "'say \"hi\"'");
121 assert_eq!(
122 render_value(&Value::Utf8("both ' and \"".to_string())),
123 Err(RenderError::UnrepresentableText)
124 );
125 }
126
127 #[test]
128 fn temporals_render_with_at_prefix() {
129 assert_eq!(render_value(&Value::Date(Date::from_ymd(2024, 3, 15).unwrap())).unwrap(), "@2024-03-15");
130 assert_eq!(
131 render_value(&Value::Time(Time::from_hms_nano(14, 30, 15, 123_456_789).unwrap())).unwrap(),
132 "@14:30:15.123456789"
133 );
134 let dt = render_value(&Value::DateTime(DateTime::from_epoch_secs(1_700_000_000).unwrap())).unwrap();
135 assert!(dt.starts_with("@") && dt.contains('T') && dt.ends_with('Z'), "got {dt}");
136 }
137
138 #[test]
139 fn durations_render_as_iso_with_at_prefix() {
140 assert_eq!(render_value(&Value::Duration(Duration::from_seconds(90).unwrap())).unwrap(), "@PT1M30S");
141 assert_eq!(
142 render_value(&Value::Duration(Duration::new(14, 3, 3_661_000_000_000).unwrap())).unwrap(),
143 "@P1Y2M3DT1H1M1S"
144 );
145 assert_eq!(render_value(&Value::Duration(Duration::zero()).clone()).unwrap(), "@PT0S");
146 }
147
148 #[test]
149 fn blob_renders_as_hex_constructor() {
150 assert_eq!(
151 render_value(&Value::Blob(Blob::from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]))).unwrap(),
152 "blob::hex('deadbeef')"
153 );
154 assert_eq!(render_value(&Value::Blob(Blob::empty())).unwrap(), "blob::hex('')");
155 }
156
157 #[test]
158 fn dictionary_id_value_is_rejected_since_scans_decode() {
159 let _ = ValueType::DictionaryId;
160 assert!(matches!(
161 render_value(&Value::DictionaryId(Default::default())),
162 Err(RenderError::Unsupported(_))
163 ));
164 }
165}