diff --git a/CHANGELOG.md b/CHANGELOG.md index de2e16b..c2c603f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +- prevent out of bound construction and define empty vs nonempty at compile time + ## [0.7.1] - 2022-08-01 ### Added - fix `Abrbitrary` impl to honor upper(U) and lower(L) bounds; diff --git a/Cargo.toml b/Cargo.toml index aca58e4..5ac0615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "bounded-vec" version = "0.7.1" license = "CC0-1.0" authors = ["Denys Zadorozhnyi "] -edition = "2018" +edition = "2021" description = "Non-empty rust Vec wrapper with type guarantees on lower and upper bounds for items quantity." repository = "https://github.com/ergoplatform/bounded-vec" diff --git a/README.md b/README.md index b6731a9..42a27c2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## bounded-vec `BoundedVec` - Non-empty rust `std::vec::Vec` wrapper with type guarantees on lower(`L`) and upper(`U`) bounds for items quantity. Inspired by [vec1](https://github.com/rustonaut/vec1). +This crate is `#![no_std]` compatible with `alloc`. ## Example @@ -21,6 +22,7 @@ assert_eq!(data, [2u8,4].into()); ## Crate features - optional(non-default) `serde` feature that adds serialization to `BoundedVec`. +- optional(non-default) `schema` feature that adds JSON schema support via `schemars` (requires `serde`). - optional(non-default) `arbitrary` feature that adds `proptest::Arbitrary` implementation to `BoundedVec`. ## Changelog diff --git a/src/bounded_vec.rs b/src/bounded_vec.rs index 9a31aad..3461ff6 100644 --- a/src/bounded_vec.rs +++ b/src/bounded_vec.rs @@ -4,22 +4,17 @@ use core::convert::{TryFrom, TryInto}; use core::slice::{Iter, IterMut}; use thiserror::Error; -/// Non-empty Vec bounded with minimal (L - lower bound) and maximal (U - upper bound) items quantity +/// Non-empty Vec bounded with minimal (L - lower bound) and maximal (U - upper bound) items quantity. +/// +/// # Type Parameters +/// +/// * `W` - witness type to prove vector ranges and shape if interface accordingly #[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)] -pub struct BoundedVec -// enable when feature(const_evaluatable_checked) is stable -// where -// Assert<{ L > 0 }>: IsTrue, -{ +pub struct BoundedVec> { inner: Vec, + _marker: core::marker::PhantomData, } -// enum Assert {} - -// trait IsTrue {} - -// impl IsTrue for Assert {} - /// BoundedVec errors #[derive(Error, PartialEq, Eq, Debug, Clone)] pub enum BoundedVecOutOfBounds { @@ -41,34 +36,121 @@ pub enum BoundedVecOutOfBounds { }, } -impl BoundedVec { +/// Module for type witnesses used to prove vector bounds at compile time +pub mod witnesses { + + // NOTE: we can have proves if needed for some cases like 8/16/32/64 upper bound, so can make memory and serde more compile safe and efficient + + /// Compile-time proof of valid bounds. Must be consturcted with same bounds to instantiate `BoundedVec`. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct NonEmpty(()); + + /// Possibly empty vector with upper bound. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct Empty(()); + + /// Type a compile-time proof of valid bounds + pub const fn non_empty() -> NonEmpty { + const { + if L == 0 { + panic!("L must be greater than 0") + } + if L > U { + panic!("L must be less than or equal to U") + } + + NonEmpty::(()) + } + } + + /// Type a compile-time proof for possibly empty vector with upper bound + pub const fn empty() -> Empty { + const { Empty::(()) } + } +} + +impl BoundedVec> { /// Creates new BoundedVec or returns error if items count is out of bounds /// + /// # Parameters + /// + /// * `items` - vector of items within bounds + /// + /// # Errors + /// + /// * `UpperBoundError` - if `items`` len is more than U (upper bound) + /// /// # Example /// ``` /// use bounded_vec::BoundedVec; - /// let data: BoundedVec<_, 2, 8> = BoundedVec::from_vec(vec![1u8, 2]).unwrap(); + /// use bounded_vec::witnesses; + /// let data: BoundedVec<_, 0, 8, witnesses::Empty<8>> = + /// BoundedVec::<_, 0, 8, witnesses::Empty<8>>::from_vec(vec![1u8, 2]).unwrap(); /// ``` pub fn from_vec(items: Vec) -> Result { - // remove when feature(const_evaluatable_checked) is stable - // and this requirement is encoded in type sig - assert!(L > 0); + let _witness = witnesses::empty::(); let len = items.len(); - if len < L { - Err(BoundedVecOutOfBounds::LowerBoundError { - lower_bound: L, - got: len, - }) - } else if len > U { + if len > U { Err(BoundedVecOutOfBounds::UpperBoundError { upper_bound: U, got: len, }) } else { - Ok(BoundedVec { inner: items }) + Ok(BoundedVec { + inner: items, + _marker: core::marker::PhantomData, + }) } } + /// Returns the first element of the vector, or `None` if it is empty + /// + /// # Example + /// ``` + /// use bounded_vec::BoundedVec; + /// use bounded_vec::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.first(), Some(&1u8)); + /// ``` + pub fn first(&self) -> Option<&T> { + self.inner.first() + } + + /// Returns `true` if the vector contains no elements + /// + /// # Example + /// ``` + /// use bounded_vec::BoundedVec; + /// use bounded_vec::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.is_empty(), false); + /// ``` + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Returns the last element of the vector, or `None` if it is empty + /// + /// # Example + /// ``` + /// use bounded_vec::BoundedVec; + /// use bounded_vec::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.last(), Some(&2u8)); + /// ``` + pub fn last(&self) -> Option<&T> { + self.inner.last() + } +} + +/// Part which works for all witnesses +impl BoundedVec { /// Returns a reference to underlying `Vec`` /// /// # Example @@ -94,49 +176,100 @@ impl BoundedVec { /// assert_eq!(data.to_vec(), vec![1u8,2]); /// ``` pub fn to_vec(self) -> Vec { - self.into() + self.inner } - /// Returns the number of elements in the vector + /// Extracts a slice containing the entire vector. /// /// # Example /// ``` /// use bounded_vec::BoundedVec; /// use std::convert::TryInto; /// - /// let data: BoundedVec = vec![1u8,2].try_into().unwrap(); - /// assert_eq!(data.len(), 2); + /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.as_slice(), &[1u8,2]); /// ``` - pub fn len(&self) -> usize { - self.inner.len() + pub fn as_slice(&self) -> &[T] { + self.inner.as_slice() } - /// Always returns `false` (cannot be empty) + /// Returns a reference for an element at index or `None` if out of bounds /// /// # Example + /// /// ``` /// use bounded_vec::BoundedVec; - /// use std::convert::TryInto; + /// let data: BoundedVec = [1u8,2].into(); + /// let elem = *data.get(1).unwrap(); + /// assert_eq!(elem, 2); + /// ``` + pub fn get(&self, index: usize) -> Option<&T> { + self.inner.get(index) + } + + /// Returns an iterator + pub fn iter(&self) -> Iter { + self.inner.iter() + } + + /// Returns an iterator that allows to modify each value + pub fn iter_mut(&mut self) -> IterMut { + self.inner.iter_mut() + } +} + +impl BoundedVec> { + /// Creates new BoundedVec or returns error if items count is out of bounds /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.is_empty(), false); + /// # Parameters + /// + /// * `items` - vector of items within bounds + /// + /// # Errors + /// + /// * `LowerBoundError` - if `items`` len is less than L (lower bound) + /// * `UpperBoundError` - if `items`` len is more than U (upper bound) + /// + /// # Example /// ``` - pub fn is_empty(&self) -> bool { - false + /// use bounded_vec::BoundedVec; + /// use bounded_vec::witnesses; + /// let data: BoundedVec<_, 2, 8, witnesses::NonEmpty<2, 8>> = + /// BoundedVec::<_, 2, 8, witnesses::NonEmpty<2, 8>>::from_vec(vec![1u8, 2]).unwrap(); + /// ``` + pub fn from_vec(items: Vec) -> Result { + let _witness = witnesses::non_empty::(); + let len = items.len(); + if len < L { + Err(BoundedVecOutOfBounds::LowerBoundError { + lower_bound: L, + got: len, + }) + } else if len > U { + Err(BoundedVecOutOfBounds::UpperBoundError { + upper_bound: U, + got: len, + }) + } else { + Ok(BoundedVec { + inner: items, + _marker: core::marker::PhantomData, + }) + } } - /// Extracts a slice containing the entire vector. + /// Returns the number of elements in the vector /// /// # Example /// ``` /// use bounded_vec::BoundedVec; /// use std::convert::TryInto; /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.as_slice(), &[1u8,2]); + /// let data: BoundedVec = vec![1u8,2].try_into().unwrap(); + /// assert_eq!(data.len(), 2); /// ``` - pub fn as_slice(&self) -> &[T] { - self.inner.as_slice() + pub fn len(&self) -> usize { + self.inner.len() } /// Returns the first element of non-empty Vec @@ -182,12 +315,13 @@ impl BoundedVec { /// let data = data.mapped(|x|x*2); /// assert_eq!(data, [2u8,4].into()); /// ``` - pub fn mapped(self, map_fn: F) -> BoundedVec + pub fn mapped(self, map_fn: F) -> BoundedVec> where F: FnMut(T) -> N, { BoundedVec { inner: self.inner.into_iter().map(map_fn).collect::>(), + _marker: core::marker::PhantomData, } } @@ -204,12 +338,13 @@ impl BoundedVec { /// let data = data.mapped_ref(|x|x*2); /// assert_eq!(data, [2u8,4].into()); /// ``` - pub fn mapped_ref(&self, map_fn: F) -> BoundedVec + pub fn mapped_ref(&self, map_fn: F) -> BoundedVec> where F: FnMut(&T) -> N, { BoundedVec { inner: self.inner.iter().map(map_fn).collect::>(), + _marker: core::marker::PhantomData, } } @@ -238,7 +373,10 @@ impl BoundedVec { /// let data: Result, _> = data.try_mapped(|x| Err("failed")); /// assert_eq!(data, Err("failed")); /// ``` - pub fn try_mapped(self, map_fn: F) -> Result, E> + pub fn try_mapped( + self, + map_fn: F, + ) -> Result>, E> where F: FnMut(T) -> Result, { @@ -248,7 +386,7 @@ impl BoundedVec { out.push(map_fn(element)?); } #[allow(clippy::unwrap_used)] - Ok(BoundedVec::from_vec(out).unwrap()) + Ok(BoundedVec::>::from_vec(out).unwrap()) } /// Create a new `BoundedVec` by mapping references of `self` elements @@ -270,7 +408,10 @@ impl BoundedVec { /// let data: Result, _> = data.try_mapped_ref(|x| Err("failed")); /// assert_eq!(data, Err("failed")); /// ``` - pub fn try_mapped_ref(&self, map_fn: F) -> Result, E> + pub fn try_mapped_ref( + &self, + map_fn: F, + ) -> Result>, E> where F: FnMut(&T) -> Result, { @@ -280,31 +421,7 @@ impl BoundedVec { out.push(map_fn(element)?); } #[allow(clippy::unwrap_used)] - Ok(BoundedVec::from_vec(out).unwrap()) - } - - /// Returns a reference for an element at index or `None` if out of bounds - /// - /// # Example - /// - /// ``` - /// use bounded_vec::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let elem = *data.get(1).unwrap(); - /// assert_eq!(elem, 2); - /// ``` - pub fn get(&self, index: usize) -> Option<&T> { - self.inner.get(index) - } - - /// Returns an iterator - pub fn iter(&self) -> Iter { - self.inner.iter() - } - - /// Returns an iterator that allows to modify each value - pub fn iter_mut(&mut self) -> IterMut { - self.inner.iter_mut() + Ok(BoundedVec::>::from_vec(out).unwrap()) } /// Returns the last and all the rest of the elements @@ -314,7 +431,7 @@ impl BoundedVec { } /// Return a new BoundedVec with indices included - pub fn enumerated(self) -> BoundedVec<(usize, T), L, U> { + pub fn enumerated(self) -> BoundedVec<(usize, T), L, U, witnesses::NonEmpty> { #[allow(clippy::unwrap_used)] self.inner .into_iter() @@ -337,40 +454,66 @@ impl BoundedVec { /// assert!(opt_bv_some.is_some()); /// assert_eq!(opt_bv_some.to_vec(), vec![0u8, 2]); /// ``` - pub fn opt_empty_vec(v: Vec) -> Result>, BoundedVecOutOfBounds> { + pub fn opt_empty_vec( + v: Vec, + ) -> Result>>, BoundedVecOutOfBounds> { if v.is_empty() { Ok(None) } else { - Ok(Some(BoundedVec::from_vec(v)?)) + Ok(Some(Self::from_vec(v)?)) } } } /// A non-empty Vec with no effective upper-bound on its length -pub type NonEmptyVec = BoundedVec; +pub type NonEmptyVec = BoundedVec>; + +/// Possibly empty Vec with upper-bound on its length +pub type EmptyBoundedVec = BoundedVec>; -impl TryFrom> for BoundedVec { +/// Non-empty Vec with bounded length +pub type NonEmptyBoundedVec = + BoundedVec>; + +impl TryFrom> + for BoundedVec> +{ type Error = BoundedVecOutOfBounds; fn try_from(value: Vec) -> Result { - BoundedVec::from_vec(value) + Self::from_vec(value) + } +} + +impl TryFrom> for BoundedVec> { + type Error = BoundedVecOutOfBounds; + + fn try_from(value: Vec) -> Result { + Self::from_vec(value) } } // when feature(const_evaluatable_checked) is stable cover all array sizes (L..=U) -impl From<[T; L]> for BoundedVec { +impl From<[T; L]> + for BoundedVec> +{ fn from(arr: [T; L]) -> Self { - BoundedVec { inner: arr.into() } + BoundedVec { + inner: arr.into(), + _marker: core::marker::PhantomData, + } } } -impl From> for Vec { - fn from(v: BoundedVec) -> Self { +impl From>> + for Vec +{ + fn from(v: BoundedVec>) -> Self { v.inner } } -impl IntoIterator for BoundedVec { +impl IntoIterator for BoundedVec { type Item = T; type IntoIter = vec::IntoIter; @@ -379,7 +522,7 @@ impl IntoIterator for BoundedVec { } } -impl<'a, T, const L: usize, const U: usize> IntoIterator for &'a BoundedVec { +impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a BoundedVec { type Item = &'a T; type IntoIter = core::slice::Iter<'a, T>; @@ -388,7 +531,7 @@ impl<'a, T, const L: usize, const U: usize> IntoIterator for &'a BoundedVec IntoIterator for &'a mut BoundedVec { +impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a mut BoundedVec { type Item = &'a mut T; type IntoIter = core::slice::IterMut<'a, T>; @@ -397,25 +540,25 @@ impl<'a, T, const L: usize, const U: usize> IntoIterator for &'a mut BoundedVec< } } -impl AsRef> for BoundedVec { +impl AsRef> for BoundedVec { fn as_ref(&self) -> &Vec { &self.inner } } -impl AsRef<[T]> for BoundedVec { +impl AsRef<[T]> for BoundedVec { fn as_ref(&self) -> &[T] { self.inner.as_ref() } } -impl AsMut> for BoundedVec { +impl AsMut> for BoundedVec { fn as_mut(&mut self) -> &mut Vec { self.inner.as_mut() } } -impl AsMut<[T]> for BoundedVec { +impl AsMut<[T]> for BoundedVec { fn as_mut(&mut self) -> &mut [T] { self.inner.as_mut() } @@ -427,7 +570,9 @@ pub trait OptBoundedVecToVec { fn to_vec(self) -> Vec; } -impl OptBoundedVecToVec for Option> { +impl OptBoundedVecToVec + for Option>> +{ fn to_vec(self) -> Vec { self.map(|bv| bv.into()).unwrap_or_default() } @@ -443,7 +588,8 @@ mod arbitrary { use proptest::prelude::*; use proptest::strategy::BoxedStrategy; - impl Arbitrary for BoundedVec + impl Arbitrary + for BoundedVec> where T::Strategy: 'static, { @@ -452,7 +598,7 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { vec(any::(), L..=U) - .prop_map(|items| BoundedVec::from_vec(items).unwrap()) + .prop_map(|items| Self::from_vec(items).unwrap()) .boxed() } } @@ -494,7 +640,10 @@ mod serde_impl { U ))); }; - Ok(BoundedVec { inner }) + Ok(BoundedVec { + inner, + _marker: core::marker::PhantomData, + }) } } @@ -546,7 +695,7 @@ mod tests { #[test] fn is_empty() { - let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + let data: EmptyBoundedVec<_, 8> = vec![1u8, 2].try_into().unwrap(); assert!(!data.is_empty()); }