From 9d205ee256db8c476007ad65fa0c49575ba68556 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Fri, 27 Mar 2026 16:49:28 +0200 Subject: [PATCH] Add constructuion from failable iterator. Made the Dict small size hint public. Clarify some docs. --- src/haystack/val/dict.rs | 113 +++++++++++++++++++++++++++++++------- tests/values/test_dict.rs | 91 +++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 20 deletions(-) diff --git a/src/haystack/val/dict.rs b/src/haystack/val/dict.rs index 7e20636..9d18245 100644 --- a/src/haystack/val/dict.rs +++ b/src/haystack/val/dict.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2020 - 2022, J2 Innovations +// Copyright (C) 2020 - 2026, J2 Innovations //! Haystack Dict @@ -15,8 +15,6 @@ use std::ops::Index; // Alias for the underlying Dict type pub(crate) type DictType = BTreeMap; -const SMALL_DICT_MAX_ENTRIES: usize = 32; - #[derive(Clone, Debug)] enum DictRepr { Small(Vec<(String, Value)>), @@ -130,12 +128,17 @@ pub trait HaystackDict { } impl Dict { + /// Hint for the maximum number of entries for the small-vector back-store. + pub const SMALL_DICT_MAX_ENTRIES_HINT: usize = 32; + /// Construct a new `Dict` with a threshold of 32 entries for the small-vector back-store. pub fn new() -> Dict { - Self::with_small_max_entries(SMALL_DICT_MAX_ENTRIES) + Self::with_small_max_entries(Self::SMALL_DICT_MAX_ENTRIES_HINT) } /// Construct a new `Dict` with a custom small-store threshold. + /// If `small_max_entries` is 0, the small-vector back-store is disabled + /// and the dict will use the `BTreeMap` representation. pub fn with_small_max_entries(small_max_entries: usize) -> Dict { let value = if small_max_entries == 0 { DictRepr::Tree(DictType::new()) @@ -325,6 +328,55 @@ impl Dict { inner: self.iter_mut(), } } + + /// Returns `None` when the size hint signals the entry count will exceed + /// the small-vec threshold (callers should build a `Tree` directly), or + /// `Some(dict)` with a `Small`-backed dict pre-allocated to the hinted + /// capacity. + fn prepare_from_hint(lower: usize, upper: Option) -> Option { + if lower > Self::SMALL_DICT_MAX_ENTRIES_HINT + || upper.is_some_and(|upper| upper > Self::SMALL_DICT_MAX_ENTRIES_HINT) + { + return None; + } + let mut dict = Dict::new(); + if lower > 0 + && let DictRepr::Small(entries) = &mut dict.value + { + entries.reserve(lower.min(dict.small_max_entries)); + } + Some(dict) + } + + /// Constructs a `Dict` from a fallible iterator of `(String, Value)` pairs. + /// + /// Applies the same size-hint optimisation as [`FromIterator`]: when the + /// iterator reports more than `small_max_entries` items the backing store + /// starts as a `Tree` directly, skipping the small-vec stage. + /// + /// The first `Err` item short-circuits collection and is returned + /// immediately, leaving any remaining items unconsumed. + pub fn try_from_iter(iter: I) -> Result + where + I: IntoIterator>, + { + let iter = iter.into_iter(); + let (lower, upper) = iter.size_hint(); + + let Some(mut dict) = Dict::prepare_from_hint(lower, upper) else { + let map = iter.collect::>()?; + return Ok(Dict { + value: DictRepr::Tree(map), + small_max_entries: Dict::SMALL_DICT_MAX_ENTRIES_HINT, + }); + }; + + for result in iter { + let (k, v) = result?; + dict.insert(k, v); + } + Ok(dict) + } } impl Default for Dict { @@ -518,6 +570,37 @@ impl<'a> Iterator for DictValuesMut<'a> { impl ExactSizeIterator for DictValuesMut<'_> {} +/// A newtype wrapper around any `IntoIterator` whose items are +/// `Result<(String, Value), E>`, used as the source type for +/// [`TryFrom> for Dict`]. +/// +/// # Example +/// ``` +/// use libhaystack::val::{Dict, Value, FalliblePairs}; +/// +/// let pairs: Vec> = vec![ +/// Ok(("a".into(), Value::make_str("hello"))), +/// Ok(("b".into(), 42.into())), +/// ]; +/// let dict = Dict::try_from(FalliblePairs(pairs)).unwrap(); +/// assert_eq!(dict.len(), 2); +/// ``` +pub struct FalliblePairs(pub I); + +/// Converts a [`FalliblePairs`]-wrapped iterator into a [`Dict`]. +/// +/// The first `Err` item short-circuits the conversion. +impl TryFrom> for Dict +where + I: IntoIterator>, +{ + type Error = E; + + fn try_from(src: FalliblePairs) -> Result { + Dict::try_from_iter(src.0) + } +} + /// Implement FromIterator for `Dict` /// /// Allows constructing a `Dict` from a `(String, Value)` tuple iterator @@ -525,21 +608,13 @@ impl FromIterator<(String, Value)> for Dict { fn from_iter>(iter: T) -> Self { let mut iter = iter.into_iter(); let (lower, upper) = iter.size_hint(); - let mut dict = Dict::new(); - if lower > dict.small_max_entries - || upper.is_some_and(|upper| upper > dict.small_max_entries) - { - dict.value = DictRepr::Tree(iter.collect()); - return dict; - } - // Reserve capacity up-front when the hint is available and fits in Small, - // avoiding repeated Vec reallocations for the common fixed-size-collection case. - if lower > 0 - && let DictRepr::Small(entries) = &mut dict.value - { - entries.reserve(lower.min(dict.small_max_entries)); - } + let Some(mut dict) = Dict::prepare_from_hint(lower, upper) else { + return Dict { + value: DictRepr::Tree(iter.collect()), + small_max_entries: Dict::SMALL_DICT_MAX_ENTRIES_HINT, + }; + }; for (k, v) in iter.by_ref() { dict.insert(k, v); @@ -658,7 +733,7 @@ impl HaystackDict for Dict { /// Converts from `DictType` to a `Dict` impl From for Dict { fn from(from: DictType) -> Self { - let small_max_entries = SMALL_DICT_MAX_ENTRIES; + let small_max_entries = Dict::SMALL_DICT_MAX_ENTRIES_HINT; if from.len() <= small_max_entries { Dict { value: DictRepr::Small(from.into_iter().collect()), diff --git a/tests/values/test_dict.rs b/tests/values/test_dict.rs index ad0627e..95710c0 100644 --- a/tests/values/test_dict.rs +++ b/tests/values/test_dict.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2020 - 2022, J2 Innovations +// Copyright (C) 2020 - 2026, J2 Innovations //! Test Dict @@ -309,3 +309,92 @@ fn test_dict_into_btreemap_empty() { let map: BTreeMap = Dict::new().into(); assert!(map.is_empty()); } + +// --- try_from_iter / FalliblePairs --- + +type StrErr = &'static str; + +fn ok_pair(k: &'static str, v: impl Into) -> Result<(String, Value), StrErr> { + Ok((k.into(), v.into())) +} + +#[test] +fn test_dict_try_from_iter_all_ok_small() { + // All items succeed; result stays in the small-vec repr (3 << 32). + let dict = Dict::try_from_iter::([ + ok_pair("a", "hello"), + ok_pair("b", 42), + ok_pair("c", true), + ]) + .unwrap(); + + assert_eq!(dict.len(), 3); + assert_eq!(dict.get_str("a"), Some(&Str::from("hello"))); + assert_eq!(dict.get_num("b"), Some(&Number::from(42))); + assert_eq!(dict.get_bool("c"), Some(&Bool::from(true))); +} + +#[test] +fn test_dict_try_from_iter_short_circuits_on_err() { + // The second item is an error; collection must stop and return it. + let items: Vec> = vec![ + ok_pair("a", 1), + Err("parse error"), + ok_pair("c", 3), // must never be inserted + ]; + let result = Dict::try_from_iter(items); + assert_eq!(result, Err("parse error")); +} + +#[test] +fn test_dict_try_from_iter_empty() { + let dict = Dict::try_from_iter::(std::iter::empty()).unwrap(); + assert!(dict.is_empty()); +} + +#[test] +fn test_dict_try_from_iter_tree_via_size_hint() { + // Wrap in a Vec so the size_hint lower-bound exceeds the threshold (32). + let pairs: Vec> = (0..64_usize) + .map(|i| Ok((format!("k{i:02}"), Value::from(i as i32)))) + .collect(); + + let dict = Dict::try_from_iter(pairs).unwrap(); + assert_eq!(dict.len(), 64); + // Verify a few entries are accessible. + assert_eq!(dict.get_num("k00"), Some(&Number::from(0))); + assert_eq!(dict.get_num("k63"), Some(&Number::from(63))); +} + +#[test] +fn test_dict_try_from_iter_tree_size_hint_err() { + // Same large-iter path but an error occurs mid-way. + let pairs: Vec> = (0..64_usize) + .map(|i| { + if i == 32 { + Err("mid-stream error") + } else { + Ok((format!("k{i:02}"), Value::from(i as i32))) + } + }) + .collect(); + + let result = Dict::try_from_iter(pairs); + assert_eq!(result, Err("mid-stream error")); +} + +#[test] +fn test_dict_fallible_pairs_tryfrom_ok() { + // TryFrom> surface. + let pairs = vec![ok_pair("x", "val"), ok_pair("y", 7)]; + let dict = Dict::try_from(FalliblePairs(pairs)).unwrap(); + assert_eq!(dict.len(), 2); + assert_eq!(dict.get_str("x"), Some(&Str::from("val"))); +} + +#[test] +fn test_dict_fallible_pairs_tryfrom_err() { + let items: Vec> = vec![ok_pair("a", 1), Err("bad")]; + let result = Dict::try_from(FalliblePairs(items)); + assert_eq!(result, Err("bad")); +}