1use serde::{Deserialize, Serialize};
7use std::borrow::Cow;
8use std::collections::HashMap;
9use std::hash::{Hash, Hasher};
10use std::sync::Arc;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
17#[cfg_attr(feature = "rkyv-archive", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
18#[cfg_attr(feature = "rkyv-archive", archive(check_bytes))]
19#[cfg_attr(feature = "rkyv-archive", archive(bound(
20 serialize = "__S: rkyv::ser::Serializer + rkyv::ser::ScratchSpace + rkyv::ser::SharedSerializeRegistry",
21 deserialize = "__D: rkyv::de::SharedDeserializeRegistry",
22)))]
23#[cfg_attr(feature = "rkyv-archive", archive_attr(check_bytes(
24 bound = "__C: rkyv::validation::ArchiveContext + rkyv::validation::SharedContext, <__C as rkyv::Fallible>::Error: std::error::Error"
25)))]
26pub enum Value {
27 #[default]
29 Undef,
30 Bool(bool),
32 Int(i64),
34 Float(f64),
36 Str(#[cfg_attr(feature = "rkyv-archive", omit_bounds, archive_attr(omit_bounds))] Arc<String>),
38 Array(#[cfg_attr(feature = "rkyv-archive", omit_bounds, archive_attr(omit_bounds))] Arc<Vec<Value>>),
57 Hash(#[cfg_attr(feature = "rkyv-archive", omit_bounds, archive_attr(omit_bounds))] HashMap<String, Value>),
59 Status(i32),
61 Ref(#[cfg_attr(feature = "rkyv-archive", omit_bounds, archive_attr(omit_bounds))] Box<Value>),
63 NativeFn(u16),
65 Obj(u32),
71}
72
73impl Value {
74 pub fn int(n: i64) -> Self {
78 Value::Int(n)
79 }
80
81 pub fn float(f: f64) -> Self {
83 Value::Float(f)
84 }
85
86 pub fn str(s: impl Into<String>) -> Self {
89 Value::Str(Arc::new(s.into()))
90 }
91
92 pub fn bool(b: bool) -> Self {
94 Value::Bool(b)
95 }
96
97 pub fn array(v: Vec<Value>) -> Self {
100 Value::Array(Arc::new(v))
101 }
102
103 pub fn as_array(&self) -> Option<&Vec<Value>> {
106 match self {
107 Value::Array(a) => Some(a),
108 _ => None,
109 }
110 }
111
112 pub fn array_mut(&mut self) -> Option<&mut Vec<Value>> {
121 match self {
122 Value::Array(a) => Some(Arc::make_mut(a)),
123 _ => None,
124 }
125 }
126
127 pub fn into_array(self) -> Option<Vec<Value>> {
130 match self {
131 Value::Array(a) => Some(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
132 _ => None,
133 }
134 }
135
136 pub fn hash(m: HashMap<String, Value>) -> Self {
138 Value::Hash(m)
139 }
140
141 pub fn status(code: i32) -> Self {
145 Value::Status(code)
146 }
147
148 pub fn is_truthy(&self) -> bool {
152 match self {
153 Value::Undef => false,
154 Value::Bool(b) => *b,
155 Value::Int(n) => *n != 0,
156 Value::Float(f) => *f != 0.0,
157 Value::Str(s) => !s.is_empty() && s.as_str() != "0",
158 Value::Array(a) => !a.is_empty(),
159 Value::Hash(h) => !h.is_empty(),
160 Value::Status(c) => *c == 0, Value::Ref(_) => true,
162 Value::NativeFn(_) => true,
163 Value::Obj(_) => true,
166 }
167 }
168
169 pub fn to_int(&self) -> i64 {
171 match self {
172 Value::Int(n) => *n,
173 Value::Float(f) => *f as i64,
174 Value::Bool(b) => *b as i64,
175 Value::Str(s) => s.parse().unwrap_or(0),
176 Value::Status(c) => *c as i64,
177 Value::Array(a) => a.len() as i64,
178 _ => 0,
179 }
180 }
181
182 pub fn to_float(&self) -> f64 {
184 match self {
185 Value::Float(f) => *f,
186 Value::Int(n) => *n as f64,
187 Value::Bool(b) if *b => 1.0,
188 Value::Str(s) => s.parse().unwrap_or(0.0),
189 Value::Status(c) => *c as f64,
190 _ => 0.0,
191 }
192 }
193
194 pub fn to_str(&self) -> String {
196 self.as_str_cow().into_owned()
197 }
198
199 pub fn as_str_cow(&self) -> Cow<'_, str> {
202 match self {
203 Value::Str(s) => Cow::Borrowed(s.as_str()),
204 Value::Int(n) => Cow::Owned(n.to_string()),
205 Value::Float(f) => Cow::Owned(f.to_string()),
206 Value::Bool(b) => Cow::Borrowed(if *b { "1" } else { "" }),
207 Value::Undef => Cow::Borrowed(""),
208 Value::Status(c) => Cow::Owned(c.to_string()),
209 Value::Array(a) => {
210 Cow::Owned(a.iter().map(|v| v.to_str()).collect::<Vec<_>>().join(" "))
211 }
212 Value::Hash(_) => Cow::Borrowed("(hash)"),
213 Value::Ref(_) => Cow::Borrowed("(ref)"),
214 Value::NativeFn(id) => Cow::Owned(format!("(builtin:{})", id)),
215 Value::Obj(id) => Cow::Owned(format!("(obj:{})", id)),
216 }
217 }
218
219 pub fn len(&self) -> usize {
221 match self {
222 Value::Str(s) => s.len(),
223 Value::Array(a) => a.len(),
224 Value::Hash(h) => h.len(),
225 _ => self.to_str().len(),
226 }
227 }
228
229 pub fn is_empty(&self) -> bool {
231 self.len() == 0
232 }
233}
234
235impl Hash for Value {
236 fn hash<H: Hasher>(&self, state: &mut H) {
237 std::mem::discriminant(self).hash(state);
238 match self {
239 Value::Undef => {}
240 Value::Bool(b) => b.hash(state),
241 Value::Int(n) => n.hash(state),
242 Value::Float(f) => f.to_bits().hash(state),
243 Value::Str(s) => s.hash(state),
244 Value::Array(a) => a.hash(state),
245 Value::Hash(h) => {
246 h.len().hash(state);
247 for (k, v) in h {
248 k.hash(state);
249 v.hash(state);
250 }
251 }
252 Value::Status(c) => c.hash(state),
253 Value::Ref(b) => b.hash(state),
254 Value::NativeFn(id) => id.hash(state),
255 Value::Obj(id) => id.hash(state),
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use std::collections::HashMap;
264
265 #[test]
266 fn test_truthiness() {
267 assert!(!Value::Undef.is_truthy());
268 assert!(!Value::Int(0).is_truthy());
269 assert!(Value::Int(1).is_truthy());
270 assert!(!Value::str("").is_truthy());
271 assert!(!Value::str("0").is_truthy());
272 assert!(Value::str("hello").is_truthy());
273 assert!(Value::Status(0).is_truthy()); assert!(!Value::Status(1).is_truthy());
275 }
276
277 #[test]
278 fn test_coercions() {
279 assert_eq!(Value::str("42").to_int(), 42);
280 assert_eq!(Value::Int(42).to_str(), "42");
281 assert_eq!(Value::Float(3.25).to_int(), 3);
282 assert_eq!(Value::Bool(true).to_int(), 1);
283 }
284
285 #[test]
286 fn truthiness_for_floats() {
287 assert!(!Value::Float(0.0).is_truthy());
288 assert!(!Value::Float(-0.0).is_truthy());
289 assert!(Value::Float(0.1).is_truthy());
290 assert!(Value::Float(f64::NAN).is_truthy()); assert!(Value::Float(f64::INFINITY).is_truthy());
292 }
293
294 #[test]
295 fn truthiness_for_collections() {
296 assert!(!Value::array(vec![]).is_truthy());
297 assert!(Value::array(vec![Value::Undef]).is_truthy()); assert!(!Value::Hash(HashMap::new()).is_truthy());
299 }
300
301 #[test]
302 fn to_int_handles_all_variants() {
303 assert_eq!(Value::Undef.to_int(), 0);
304 assert_eq!(Value::Int(-7).to_int(), -7);
305 assert_eq!(Value::Float(3.99).to_int(), 3); assert_eq!(Value::Float(-3.99).to_int(), -3); assert_eq!(Value::Bool(true).to_int(), 1);
308 assert_eq!(Value::Bool(false).to_int(), 0);
309 assert_eq!(Value::Status(42).to_int(), 42);
310 assert_eq!(Value::Status(-1).to_int(), -1);
311 assert_eq!(Value::str("not a number").to_int(), 0);
312 assert_eq!(Value::array(vec![Value::Int(1), Value::Int(2)]).to_int(), 2);
313 }
314
315 #[test]
316 fn to_float_handles_all_variants() {
317 assert_eq!(Value::Int(5).to_float(), 5.0);
318 assert_eq!(Value::Float(2.5).to_float(), 2.5);
319 assert_eq!(Value::Bool(true).to_float(), 1.0);
320 assert_eq!(Value::Bool(false).to_float(), 0.0);
321 assert_eq!(Value::Status(7).to_float(), 7.0);
322 assert_eq!(Value::str("3.25").to_float(), 3.25);
323 assert_eq!(Value::str("garbage").to_float(), 0.0);
324 }
325
326 #[test]
327 fn as_str_cow_borrowed_for_str() {
328 let v = Value::str("hello");
329 match v.as_str_cow() {
330 Cow::Borrowed(s) => assert_eq!(s, "hello"),
331 Cow::Owned(_) => panic!("expected borrowed"),
332 }
333 }
334
335 #[test]
336 fn as_str_cow_owned_for_int() {
337 match Value::Int(42).as_str_cow() {
338 Cow::Owned(s) => assert_eq!(s, "42"),
339 Cow::Borrowed(_) => panic!("expected owned"),
340 }
341 }
342
343 #[test]
344 fn array_to_str_joins_with_space() {
345 let v = Value::array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
346 assert_eq!(v.to_str(), "1 2 3");
347 }
348
349 #[test]
350 fn len_returns_correct_size_per_variant() {
351 assert_eq!(Value::str("abc").len(), 3);
352 assert_eq!(Value::array(vec![Value::Int(1); 5]).len(), 5);
353 assert_eq!(Value::Hash(HashMap::new()).len(), 0);
354 assert_eq!(Value::Int(12345).len(), 5);
356 }
357
358 #[test]
359 fn is_empty_matches_len_zero() {
360 assert!(Value::str("").is_empty());
361 assert!(!Value::str("x").is_empty());
362 assert!(Value::array(vec![]).is_empty());
363 assert!(!Value::array(vec![Value::Int(0)]).is_empty());
364 }
365
366 #[test]
367 fn equality_via_partial_eq() {
368 assert_eq!(Value::Int(42), Value::Int(42));
370 assert_ne!(Value::Int(42), Value::Int(43));
371 assert_eq!(Value::str("hi"), Value::str("hi"));
372 assert_ne!(Value::Float(f64::NAN), Value::Float(f64::NAN));
374 }
375
376 #[test]
377 fn hash_impl_handles_floats_via_bits() {
378 use std::collections::hash_map::DefaultHasher;
381 use std::hash::{Hash, Hasher};
382
383 let nan1 = Value::Float(f64::NAN);
384 let nan2 = Value::Float(f64::NAN);
385 let mut h1 = DefaultHasher::new();
386 nan1.hash(&mut h1);
387 let mut h2 = DefaultHasher::new();
388 nan2.hash(&mut h2);
389 assert_eq!(h1.finish(), h2.finish());
390 }
391
392 #[test]
393 fn as_str_cow_for_undef_and_bool_is_borrowed() {
394 assert!(matches!(Value::Undef.as_str_cow(), Cow::Borrowed("")));
395 assert!(matches!(Value::Bool(true).as_str_cow(), Cow::Borrowed("1")));
396 assert!(matches!(Value::Bool(false).as_str_cow(), Cow::Borrowed("")));
397 }
398
399 #[test]
400 fn as_str_cow_for_status_and_float_is_owned() {
401 match Value::Status(127).as_str_cow() {
402 Cow::Owned(s) => assert_eq!(s, "127"),
403 _ => panic!("expected owned"),
404 }
405 match Value::Float(1.5).as_str_cow() {
406 Cow::Owned(s) => assert_eq!(s, "1.5"),
407 _ => panic!("expected owned"),
408 }
409 }
410
411 #[test]
412 fn native_fn_to_str_formats_id() {
413 assert_eq!(Value::NativeFn(42).to_str(), "(builtin:42)");
414 }
415
416 #[test]
417 fn hash_to_str_is_placeholder() {
418 let mut m = HashMap::new();
419 m.insert("k".to_string(), Value::Int(1));
420 assert_eq!(Value::Hash(m).to_str(), "(hash)");
421 }
422
423 #[test]
424 fn ref_to_str_is_placeholder_and_is_truthy() {
425 let r = Value::Ref(Box::new(Value::Int(0)));
426 assert!(r.is_truthy(), "Ref is always truthy regardless of inner");
427 assert_eq!(r.to_str(), "(ref)");
428 }
429
430 #[test]
431 fn nested_array_to_str_recurses() {
432 let inner = Value::array(vec![Value::Int(1), Value::Int(2)]);
433 let outer = Value::array(vec![inner, Value::Int(3)]);
434 assert_eq!(outer.to_str(), "1 2 3");
436 }
437
438 #[test]
439 fn to_int_from_negative_string_parses() {
440 assert_eq!(Value::str("-123").to_int(), -123);
441 }
442
443 #[test]
444 fn to_int_unhandled_variants_return_zero() {
445 assert_eq!(Value::Hash(HashMap::new()).to_int(), 0);
446 assert_eq!(Value::Ref(Box::new(Value::Int(99))).to_int(), 0);
447 assert_eq!(Value::NativeFn(5).to_int(), 0);
448 }
449
450 #[test]
451 fn to_float_unhandled_variants_return_zero() {
452 assert_eq!(Value::Undef.to_float(), 0.0);
453 assert_eq!(Value::array(vec![Value::Int(1)]).to_float(), 0.0);
454 assert_eq!(Value::Bool(false).to_float(), 0.0);
455 }
456
457 #[test]
458 fn len_for_hash_counts_entries() {
459 let mut m = HashMap::new();
460 m.insert("a".to_string(), Value::Int(1));
461 m.insert("b".to_string(), Value::Int(2));
462 assert_eq!(Value::Hash(m).len(), 2);
463 }
464
465 #[test]
466 fn constructors_produce_expected_variants() {
467 assert!(matches!(Value::int(1), Value::Int(1)));
468 assert!(matches!(Value::float(1.0), Value::Float(_)));
469 assert!(matches!(Value::bool(true), Value::Bool(true)));
470 assert!(matches!(Value::status(7), Value::Status(7)));
471 assert!(matches!(Value::array(vec![]), Value::Array(_)));
472 }
473
474 #[test]
475 fn str_constructor_accepts_string_and_str() {
476 let from_str = Value::str("hi");
477 let from_string = Value::str(String::from("hi"));
478 assert_eq!(from_str, from_string);
479 }
480
481 #[test]
482 fn clone_of_str_shares_arc() {
483 let a = Value::str("hello");
485 let b = a.clone();
486 if let (Value::Str(sa), Value::Str(sb)) = (&a, &b) {
487 assert!(Arc::ptr_eq(sa, sb));
488 } else {
489 panic!("expected Str variants");
490 }
491 }
492
493 #[test]
494 fn default_is_undef() {
495 let v: Value = Default::default();
496 assert_eq!(v, Value::Undef);
497 }
498
499 #[test]
500 fn hash_distinguishes_variants_with_same_payload_bytes() {
501 use std::collections::hash_map::DefaultHasher;
504 let h = |v: &Value| {
505 let mut hs = DefaultHasher::new();
506 v.hash(&mut hs);
507 hs.finish()
508 };
509 let a = h(&Value::Int(0));
510 let b = h(&Value::Bool(false));
511 let c = h(&Value::Status(0));
512 assert_ne!(a, b);
513 assert_ne!(b, c);
514 assert_ne!(a, c);
515 }
516
517 #[test]
518 fn hash_for_hashmap_value_is_deterministic_per_value() {
519 use std::collections::hash_map::DefaultHasher;
523 let mut m = HashMap::new();
524 m.insert("a".to_string(), Value::Int(1));
525 m.insert("b".to_string(), Value::Int(2));
526 let v = Value::Hash(m);
527 let mut h1 = DefaultHasher::new();
528 v.hash(&mut h1);
529 let mut h2 = DefaultHasher::new();
530 v.hash(&mut h2);
531 assert_eq!(h1.finish(), h2.finish());
532 }
533
534 #[test]
535 fn serde_roundtrip_preserves_value() {
536 let cases = vec![
538 Value::Undef,
539 Value::Bool(true),
540 Value::Int(-42),
541 Value::Float(3.25),
542 Value::str("hello"),
543 Value::Status(127),
544 ];
545 for original in cases {
546 let json = serde_json::to_string(&original).unwrap();
547 let restored: Value = serde_json::from_str(&json).unwrap();
548 assert_eq!(original, restored);
549 }
550 }
551
552 #[test]
560 fn to_int_string_with_non_numeric_falls_back_to_zero() {
561 assert_eq!(Value::str("nope").to_int(), 0);
562 }
563
564 #[test]
565 fn to_int_array_returns_length_not_first_element() {
566 let v = Value::array(vec![Value::int(99), Value::int(100), Value::int(101)]);
568 assert_eq!(v.to_int(), 3, "array's to_int must be length, not [0]");
569 }
570
571 #[test]
572 fn to_int_bool_true_is_one_false_is_zero() {
573 assert_eq!(Value::bool(true).to_int(), 1);
574 assert_eq!(Value::bool(false).to_int(), 0);
575 }
576
577 #[test]
578 fn to_int_status_preserves_exit_code() {
579 for code in [0_i32, 1, 2, 42, 127, 128, 255] {
582 assert_eq!(Value::Status(code).to_int(), code as i64);
583 }
584 }
585
586 #[test]
587 fn to_int_float_truncates_toward_zero() {
588 assert_eq!(Value::Float(3.9).to_int(), 3);
589 assert_eq!(Value::Float(-3.9).to_int(), -3);
590 assert_eq!(Value::Float(0.5).to_int(), 0);
591 }
592
593 #[test]
594 fn to_float_string_non_numeric_falls_back_to_zero() {
595 assert_eq!(Value::str("nope").to_float(), 0.0);
596 }
597
598 #[test]
599 fn to_float_bool_only_true_is_one() {
600 assert_eq!(Value::bool(true).to_float(), 1.0);
603 assert_eq!(Value::bool(false).to_float(), 0.0);
604 }
605
606 #[test]
607 fn is_truthy_empty_collections_are_false() {
608 assert!(!Value::array(Vec::new()).is_truthy());
609 assert!(!Value::hash(HashMap::new()).is_truthy());
610 assert!(!Value::str("").is_truthy());
611 assert!(!Value::int(0).is_truthy());
612 assert!(!Value::Float(0.0).is_truthy());
613 assert!(!Value::Undef.is_truthy());
614 }
615
616 #[test]
617 fn is_truthy_single_element_array_is_true() {
618 assert!(Value::array(vec![Value::int(0)]).is_truthy());
619 }
620
621 #[test]
628 fn as_str_cow_bool_renders_as_perl_compatible() {
629 assert_eq!(Value::bool(true).to_str(), "1");
633 assert_eq!(Value::bool(false).to_str(), "");
634 }
635
636 #[test]
637 fn as_str_cow_undef_renders_as_empty_string() {
638 assert_eq!(Value::Undef.to_str(), "");
639 }
640
641 #[test]
642 fn as_str_cow_array_joins_with_single_space() {
643 let v = Value::array(vec![Value::int(1), Value::int(2), Value::int(3)]);
647 assert_eq!(v.to_str(), "1 2 3");
648 }
649
650 #[test]
651 fn as_str_cow_hash_renders_as_opaque_label() {
652 let mut h = HashMap::new();
656 h.insert("x".into(), Value::int(1));
657 assert_eq!(Value::hash(h).to_str(), "(hash)");
658 }
659
660 #[test]
661 fn as_str_cow_status_renders_as_exit_code_only() {
662 assert_eq!(Value::Status(0).to_str(), "0");
665 assert_eq!(Value::Status(127).to_str(), "127");
666 assert_eq!(Value::Status(-1).to_str(), "-1");
667 }
668
669 #[test]
672 fn len_of_str_is_byte_count_not_char_count() {
673 let v = Value::str("é"); assert_eq!(v.len(), 2);
678 }
679
680 #[test]
681 fn len_of_array_is_element_count() {
682 let v = Value::array(vec![Value::int(1); 7]);
683 assert_eq!(v.len(), 7);
684 }
685
686 #[test]
687 fn len_of_int_falls_through_to_str_form() {
688 assert_eq!(Value::int(12345).len(), 5);
689 assert_eq!(Value::int(-99).len(), 3); }
691
692 #[test]
693 fn is_empty_matches_len_zero_full_matrix() {
694 assert!(Value::str("").is_empty());
695 assert!(Value::array(Vec::new()).is_empty());
696 assert!(Value::hash(HashMap::new()).is_empty());
697 assert!(!Value::int(0).is_empty()); assert!(!Value::str("x").is_empty());
699 assert!(Value::Undef.is_empty()); }
701}