1use base64::Engine;
13
14use crate::descriptor::{Strategy, TypeBody};
15use crate::error::{PanicKind, SoilError};
16use crate::value::Value;
17use crate::Runtime;
18
19const BIGINT_SAFE_MAX: i64 = (1 << 53) - 1;
22
23pub fn encode(runtime: &Runtime, value: &Value) -> Result<String, SoilError> {
26 let mut out = String::new();
27 encode_into(runtime, value, &mut out)?;
28 Ok(out)
29}
30
31fn encode_into(runtime: &Runtime, value: &Value, out: &mut String) -> Result<(), SoilError> {
32 match value {
33 Value::I64(n) => push_int(*n, out),
34 Value::U64(n) => push_int(*n, out),
35 Value::I32(n) => push_int(*n, out),
36 Value::U32(n) => push_int(*n, out),
37 Value::I16(n) => push_int(*n, out),
38 Value::U16(n) => push_int(*n, out),
39 Value::I8(n) => push_int(*n, out),
40 Value::U8(n) => push_int(*n, out),
41 Value::F64(x) => push_f64(*x, out),
42 Value::BigInt(b) => {
43 match i64::try_from(&**b) {
44 Ok(n) if n.abs() <= BIGINT_SAFE_MAX => push_int(n, out),
45 _ => {
46 out.push('"');
49 out.push_str(&b.to_string());
50 out.push('"');
51 }
52 }
53 }
54 Value::Utf8(s) => push_string(s, out),
55 Value::Bytes(b) => {
56 out.push('"');
57 out.push_str(&base64::engine::general_purpose::STANDARD.encode(&**b));
58 out.push('"');
59 }
60 Value::Unit => out.push_str("null"),
61 Value::Record(record) => {
62 let desc = runtime.registry.get(record.type_id)?;
63 if desc.strategy == Strategy::Opaque {
64 out.push_str("\"<handle>\"");
65 return Ok(());
66 }
67 let TypeBody::Record(fields) = &desc.body else {
68 return Err(SoilError::new(
69 PanicKind::TypeError,
70 format!("value is a record but type {} is not", desc.name),
71 ));
72 };
73 if fields.len() != record.fields.len() {
74 return Err(SoilError::new(
75 PanicKind::TypeError,
76 format!("record of type {} has wrong field count", desc.name),
77 ));
78 }
79 out.push('{');
80 let mut first = true;
81 for (field_desc, field_value) in fields.iter().zip(record.fields.iter()) {
82 if field_desc.ignored.is_some() {
83 continue;
84 }
85 if !first {
86 out.push(',');
87 }
88 first = false;
89 push_string(&field_desc.name, out);
90 out.push(':');
91 encode_into(runtime, field_value, out)?;
92 }
93 out.push('}');
94 }
95 Value::Sum(sum) => {
96 if sum.type_id == runtime.registry.bool_id() {
97 out.push_str(if sum.variant == 0 { "true" } else { "false" });
99 return Ok(());
100 }
101 let desc = runtime.registry.get(sum.type_id)?;
102 if desc.strategy == Strategy::Opaque {
103 out.push_str("\"<handle>\"");
104 return Ok(());
105 }
106 let TypeBody::Sum(variants) = &desc.body else {
107 return Err(SoilError::new(
108 PanicKind::TypeError,
109 format!("value is a sum but type {} is not", desc.name),
110 ));
111 };
112 let variant = variants.get(sum.variant as usize).ok_or_else(|| {
113 SoilError::new(
114 PanicKind::TypeError,
115 format!("type {} has no variant index {}", desc.name, sum.variant),
116 )
117 })?;
118 out.push_str("{\"tag\":");
119 push_string(&variant.name, out);
120 match (&variant.payload, &sum.payload) {
121 (Some(_), Some(payload)) => {
122 out.push_str(",\"value\":");
123 encode_into(runtime, payload, out)?;
124 }
125 (None, None) => {}
126 _ => {
127 return Err(SoilError::new(
128 PanicKind::TypeError,
129 format!("variant {}::{} payload mismatch", desc.name, variant.name),
130 ));
131 }
132 }
133 out.push('}');
134 }
135 Value::List(items) => {
136 out.push('[');
137 for (i, item) in items.iter().enumerate() {
138 if i > 0 {
139 out.push(',');
140 }
141 encode_into(runtime, item, out)?;
142 }
143 out.push(']');
144 }
145 Value::Map(map) => {
146 out.push('[');
147 for (i, (key, val)) in map.entries().iter().enumerate() {
148 if i > 0 {
149 out.push(',');
150 }
151 out.push_str("{\"key\":");
152 encode_into(runtime, key, out)?;
153 out.push_str(",\"value\":");
154 encode_into(runtime, val, out)?;
155 out.push('}');
156 }
157 out.push(']');
158 }
159 Value::Closure(_) => {
160 return Err(SoilError::new(
161 PanicKind::DerivationError,
162 "functions have no JSON encoding",
163 ));
164 }
165 Value::Opaque(_) => out.push_str("\"<handle>\""),
166 }
167 Ok(())
168}
169
170fn push_int<T: itoa::Integer>(n: T, out: &mut String) {
171 let mut buffer = itoa::Buffer::new();
172 out.push_str(buffer.format(n));
173}
174
175fn push_f64(x: f64, out: &mut String) {
176 if x.is_nan() {
177 out.push_str("\"NaN\"");
178 } else if x == f64::INFINITY {
179 out.push_str("\"Inf\"");
180 } else if x == f64::NEG_INFINITY {
181 out.push_str("\"-Inf\"");
182 } else {
183 let mut buffer = ryu::Buffer::new();
184 out.push_str(buffer.format(x));
185 }
186}
187
188pub(crate) fn push_string(s: &str, out: &mut String) {
192 out.push('"');
193 for c in s.chars() {
194 match c {
195 '"' => out.push_str("\\\""),
196 '\\' => out.push_str("\\\\"),
197 '\n' => out.push_str("\\n"),
198 '\r' => out.push_str("\\r"),
199 '\t' => out.push_str("\\t"),
200 '\u{0008}' => out.push_str("\\b"),
201 '\u{000c}' => out.push_str("\\f"),
202 c if (c as u32) < 0x20 => {
203 out.push_str(&format!("\\u{:04x}", c as u32));
204 }
205 c => out.push(c),
206 }
207 }
208 out.push('"');
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::descriptor::{FieldDesc, Strategy, TypeBody, TypeDesc, TypeShape, VariantDesc};
215 use crate::value::{Closure, MapOrder, MapVal, OpaqueVal, Record, Ref, SumVal};
216 use num_bigint::BigInt;
217
218 fn rt() -> Runtime {
219 Runtime::new()
220 }
221
222 fn enc(runtime: &Runtime, v: &Value) -> String {
223 encode(runtime, v).unwrap()
224 }
225
226 #[test]
227 fn scalars() {
228 let rt = rt();
229 assert_eq!(enc(&rt, &Value::I64(-42)), "-42");
230 assert_eq!(enc(&rt, &Value::U64(u64::MAX)), "18446744073709551615");
231 assert_eq!(enc(&rt, &Value::U8(255)), "255");
232 assert_eq!(enc(&rt, &Value::Unit), "null");
233 }
234
235 #[test]
236 fn floats() {
237 let rt = rt();
238 assert_eq!(enc(&rt, &Value::F64(1.0)), "1.0");
239 assert_eq!(enc(&rt, &Value::F64(-0.0)), "-0.0");
240 assert_eq!(enc(&rt, &Value::F64(0.0)), "0.0");
241 assert_eq!(enc(&rt, &Value::F64(1.5e300)), "1.5e300");
242 assert_eq!(enc(&rt, &Value::F64(f64::NAN)), "\"NaN\"");
243 assert_eq!(enc(&rt, &Value::F64(f64::INFINITY)), "\"Inf\"");
244 assert_eq!(enc(&rt, &Value::F64(f64::NEG_INFINITY)), "\"-Inf\"");
245 }
246
247 #[test]
248 fn bigint_hybrid_by_range() {
249 let rt = rt();
250 let safe = BigInt::from((1i64 << 53) - 1);
251 let beyond = BigInt::from(1i64 << 53);
252 assert_eq!(enc(&rt, &Value::BigInt(Ref::new(safe))), "9007199254740991");
253 assert_eq!(
254 enc(&rt, &Value::BigInt(Ref::new(beyond))),
255 "\"9007199254740992\""
256 );
257 let negative = BigInt::from(-(1i64 << 53));
258 assert_eq!(
259 enc(&rt, &Value::BigInt(Ref::new(negative))),
260 "\"-9007199254740992\""
261 );
262 }
263
264 #[test]
265 fn string_escapes() {
266 let rt = rt();
267 let s: Ref<str> = Ref::from("a\"b\\c\nd\te\u{1}f — ok");
268 assert_eq!(
269 enc(&rt, &Value::Utf8(s)),
270 "\"a\\\"b\\\\c\\nd\\te\\u0001f — ok\""
271 );
272 }
273
274 #[test]
275 fn bytes_base64() {
276 let rt = rt();
277 let b: Ref<[u8]> = Ref::from(&b"hello"[..]);
278 assert_eq!(enc(&rt, &Value::Bytes(b)), "\"aGVsbG8=\"");
279 }
280
281 #[test]
282 fn bool_is_json_booleans() {
283 let rt = rt();
284 let t = Value::Sum(Ref::new(SumVal {
285 type_id: rt.registry.bool_id(),
286 variant: 0,
287 payload: None,
288 }));
289 let f = Value::Sum(Ref::new(SumVal {
290 type_id: rt.registry.bool_id(),
291 variant: 1,
292 payload: None,
293 }));
294 assert_eq!(enc(&rt, &t), "true");
295 assert_eq!(enc(&rt, &f), "false");
296 }
297
298 #[test]
299 fn record_declaration_order_and_ignored_omitted() {
300 let mut rt = rt();
301 let id = rt
302 .registry
303 .register(TypeDesc {
304 name: "Doc".to_string(),
305 strategy: Strategy::Structural,
306 body: TypeBody::Record(vec![
307 FieldDesc {
308 name: "text".to_string(),
309 shape: TypeShape::Utf8,
310 ignored: None,
311 },
312 FieldDesc {
313 name: "cached_word_count".to_string(),
314 shape: TypeShape::U64,
315 ignored: Some(crate::descriptor::IgnoredDefault::Native(Closure::native(
316 |_| Ok(Value::U64(0)),
317 ))),
318 },
319 FieldDesc {
320 name: "id".to_string(),
321 shape: TypeShape::I64,
322 ignored: None,
323 },
324 ]),
325 })
326 .unwrap();
327 let value = Value::Record(Ref::new(Record {
328 type_id: id,
329 fields: Box::new([Value::Utf8(Ref::from("hi")), Value::U64(2), Value::I64(7)]),
330 }));
331 assert_eq!(enc(&rt, &value), "{\"text\":\"hi\",\"id\":7}");
332 }
333
334 #[test]
335 fn sum_internally_tagged() {
336 let mut rt = rt();
337 let id = rt
338 .registry
339 .register(TypeDesc {
340 name: "Shape".to_string(),
341 strategy: Strategy::Structural,
342 body: TypeBody::Sum(vec![
343 VariantDesc {
344 name: "Point".to_string(),
345 payload: None,
346 },
347 VariantDesc {
348 name: "Circle".to_string(),
349 payload: Some(TypeShape::F64),
350 },
351 ]),
352 })
353 .unwrap();
354 let point = Value::Sum(Ref::new(SumVal {
355 type_id: id,
356 variant: 0,
357 payload: None,
358 }));
359 let circle = Value::Sum(Ref::new(SumVal {
360 type_id: id,
361 variant: 1,
362 payload: Some(Value::F64(2.5)),
363 }));
364 assert_eq!(enc(&rt, &point), "{\"tag\":\"Point\"}");
365 assert_eq!(enc(&rt, &circle), "{\"tag\":\"Circle\",\"value\":2.5}");
366 }
367
368 #[test]
369 fn list_and_map() {
370 let rt = rt();
371 let list = Value::List(Ref::new(vec![Value::I64(1), Value::I64(2)]));
372 assert_eq!(enc(&rt, &list), "[1,2]");
373
374 let mut map = MapVal::new(MapOrder::Structural);
375 map.insert(&rt, Value::I64(2), Value::Utf8(Ref::from("b")))
376 .unwrap();
377 map.insert(&rt, Value::I64(1), Value::Utf8(Ref::from("a")))
378 .unwrap();
379 assert_eq!(
380 enc(&rt, &Value::Map(Ref::new(map))),
381 "[{\"key\":1,\"value\":\"a\"},{\"key\":2,\"value\":\"b\"}]"
382 );
383 }
384
385 #[test]
386 fn opaque_and_closure() {
387 let mut rt = rt();
388 let id = rt
389 .registry
390 .register(TypeDesc {
391 name: "Handle".to_string(),
392 strategy: Strategy::Opaque,
393 body: TypeBody::Opaque,
394 })
395 .unwrap();
396 let opaque = Value::Opaque(Ref::new(OpaqueVal {
397 type_id: id,
398 payload: Box::new(()),
399 }));
400 assert_eq!(enc(&rt, &opaque), "\"<handle>\"");
401
402 let closure = Value::Closure(Ref::new(Closure::native(|_| Ok(Value::Unit))));
403 let err = encode(&rt, &closure).unwrap_err();
404 assert_eq!(err.kind, PanicKind::DerivationError);
405 }
406
407 #[test]
408 fn encoding_is_deterministic() {
409 let rt = rt();
410 let value = Value::List(Ref::new(vec![Value::F64(0.1), Value::Utf8(Ref::from("x"))]));
411 assert_eq!(enc(&rt, &value), enc(&rt, &value));
412 }
413}