diff --git a/frame/composable-support/Cargo.toml b/frame/composable-support/Cargo.toml index fa29d531d8f..6372561f79a 100644 --- a/frame/composable-support/Cargo.toml +++ b/frame/composable-support/Cargo.toml @@ -11,6 +11,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dev-dependencies] proptest = { version = "1.0" } +serde_json = "1.*" [dependencies] frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } @@ -19,6 +20,10 @@ sp-arithmetic = { default-features = false, git = "https://github.com/paritytech sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sorted-vec = "0.7.0" +serde = { version = "1.*", features = [ "derive" ], optional = true } +is_sorted = "0.1.1" + [dependencies.codec] default-features = false @@ -35,4 +40,4 @@ std = [ "sp-runtime/std", "sp-std/std", "scale-info/std", -] \ No newline at end of file +] diff --git a/frame/composable-support/src/collections/mod.rs b/frame/composable-support/src/collections/mod.rs new file mode 100644 index 00000000000..861c190d3f8 --- /dev/null +++ b/frame/composable-support/src/collections/mod.rs @@ -0,0 +1,3 @@ +pub mod vec; + +// pub use vec::BoundedSortedVec; diff --git a/frame/composable-support/src/collections/vec/bounded/mod.rs b/frame/composable-support/src/collections/vec/bounded/mod.rs new file mode 100644 index 00000000000..a84e135a7ae --- /dev/null +++ b/frame/composable-support/src/collections/vec/bounded/mod.rs @@ -0,0 +1,3 @@ +pub mod sorted_vec; + +pub use self::sorted_vec::BoundedSortedVec; diff --git a/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs b/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs new file mode 100644 index 00000000000..fd8b60e5247 --- /dev/null +++ b/frame/composable-support/src/collections/vec/bounded/sorted_vec.rs @@ -0,0 +1,344 @@ +use crate::collections::vec::sorted::SortedVec; +use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; +use core::{ + ops::{Deref, Index}, + slice::SliceIndex, +}; +use frame_support::{traits::Get, BoundedVec}; +use sp_std::{convert::TryFrom, fmt, marker::PhantomData, prelude::*}; + +/// A bounded, sorted vector. +/// +/// It has implementations for efficient append and length decoding, as with a normal `Vec<_>`, once +/// put into storage as a raw value, map or double-map. +/// +/// As the name suggests, the length of the queue is always bounded and sorted. All internal +/// operations ensure this bound is respected and order is maintained. +#[derive(Encode)] +pub struct BoundedSortedVec(SortedVec, PhantomData); + +impl> Decode for BoundedSortedVec { + #[inline] + fn decode(input: &mut I) -> Result { + let inner = Vec::::decode(input)?; + if inner.len() > S::get() as usize { + return Err("BoundedSortedVec exceeds its limit".into()) + } + Ok(Self(SortedVec::from_unsorted(inner), PhantomData)) + } + + #[inline] + fn skip(input: &mut I) -> Result<(), codec::Error> { + Vec::::skip(input) + } +} + +// `BoundedSortedVec`s encode to something which will always decode as a `Vec`. +impl> EncodeLike> for BoundedSortedVec {} + +impl BoundedSortedVec { + /// Create `Self` from `t` without any checks. + fn unchecked_from(t: Vec) -> Self { + Self(SortedVec::unchecked_from(t), Default::default()) + } + + /// Consume self, and return the inner `Vec`. Henceforth, the `Vec<_>` can be altered in an + /// arbitrary way. At some point, if the reverse conversion is required, `TryFrom>` can + /// be used. + /// + /// This is useful for cases if you need access to an internal API of the inner `Vec<_>` which + /// is not provided by the wrapper `BoundedVec`. + #[inline] + pub fn into_inner(self) -> SortedVec { + self.0 + } + + /// Exactly the same semantics as [`Vec::remove`]. + /// + /// # Panics + /// + /// Panics if `index` is out of bounds. + #[inline] + pub fn remove(&mut self, index: usize) -> T { + self.0.remove(index) + } + + /// Exactly the same semantics as [`Vec::retain`]. + #[inline] + pub fn retain bool>(&mut self, f: F) { + self.0.retain(f) + } +} + +impl> From> for Vec { + #[inline] + fn from(x: BoundedSortedVec) -> Vec { + x.0.into_inner() + } +} + +impl> TryFrom> for BoundedVec { + type Error = (); + + #[inline] + fn try_from(x: BoundedSortedVec) -> Result { + BoundedVec::try_from(x.0.into_inner()) + } +} + +impl> BoundedSortedVec { + /// Get the bound of the type in `usize`. + #[inline] + pub fn bound() -> usize { + S::get() as usize + } + + /// Exactly the same semantics as [`Vec::insert`], but returns an `Err` (and is a noop) if the + /// new length of the vector exceeds `S`. + #[inline] + pub fn try_insert(&mut self, element: T) -> Result<(), T> { + if self.len() < Self::bound() { + self.0.insert(element); + Ok(()) + } else { + Err(element) + } + } + + /// Exactly the same semantics as [`Vec::push`], but returns an `Err` (and is a noop) if the + /// new length of the vector exceeds `S`. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds isize::MAX bytes. + #[inline] + pub fn try_push(&mut self, element: T) -> Result<(), T> { + if self.len() < Self::bound() { + self.0.insert(element); + Ok(()) + } else { + Err(element) + } + } +} + +impl Default for BoundedSortedVec { + fn default() -> Self { + // the bound cannot be below 0, which is satisfied by an empty vector + Self::unchecked_from(Vec::default()) + } +} + +#[cfg(feature = "std")] +impl fmt::Debug for BoundedSortedVec +where + T: fmt::Debug, + S: Get, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("BoundedVec").field(&self.0).field(&Self::bound()).finish() + } +} + +impl Clone for BoundedSortedVec +where + T: Clone, +{ + #[inline] + fn clone(&self) -> Self { + // bound is retained + Self::unchecked_from(self.0.as_inner().to_vec()) + } +} + +impl> TryFrom> for BoundedSortedVec { + type Error = (); + + #[inline] + fn try_from(t: Vec) -> Result { + if t.len() <= Self::bound() { + // explicit check just above + Ok(Self::unchecked_from(t)) + } else { + Err(()) + } + } +} + +// It is okay to give a non-mutable reference of the inner vec to anyone. +impl AsRef> for BoundedSortedVec { + #[inline] + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +impl AsRef<[T]> for BoundedSortedVec { + #[inline] + fn as_ref(&self) -> &[T] { + &self.0 + } +} + +// will allow for immutable all operations of `Vec` on `BoundedVec`. +impl Deref for BoundedSortedVec { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// Allows for indexing similar to a normal `Vec`. Can panic if out of bound. +impl Index for BoundedSortedVec +where + I: SliceIndex<[T]>, +{ + type Output = I::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + self.0.index(index) + } +} + +impl sp_std::iter::IntoIterator for BoundedSortedVec { + type Item = T; + type IntoIter = sp_std::vec::IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.0.into_inner().into_iter() + } +} + +impl codec::DecodeLength for BoundedSortedVec { + #[inline] + fn len(self_encoded: &[u8]) -> Result { + // `BoundedSortedVec` stored just a `Vec`, thus the length is at the beginning in + // `Compact` form, and same implementation as `Vec` can be used. + as codec::DecodeLength>::len(self_encoded) + } +} + +/// Allows for comparing vectors with different bounds. +impl, S2: Get> PartialEq> for BoundedSortedVec +where + T: PartialEq + Ord, +{ + #[inline] + fn eq(&self, rhs: &BoundedSortedVec) -> bool { + self.0 == rhs.0 + } +} + +impl> PartialEq> for BoundedSortedVec { + #[inline] + fn eq(&self, other: &Vec) -> bool { + self.0.as_inner() == other + } +} + +impl> Eq for BoundedSortedVec where T: Eq {} + +impl MaxEncodedLen for BoundedSortedVec +where + T: MaxEncodedLen, + S: Get, + BoundedSortedVec: Encode, +{ + #[inline] + fn max_encoded_len() -> usize { + // BoundedSortedVec encodes like Vec which encodes like [T], which is a compact u32 + // plus each item in the slice: + // https://substrate.dev/rustdocs/v3.0.0/src/parity_scale_codec/codec.rs.html#798-808 + codec::Compact(S::get()) + .encoded_size() + .saturating_add(Self::bound().saturating_mul(T::max_encoded_len())) + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use frame_support::sp_io::TestExternalities; + use sp_std::convert::TryInto; + + frame_support::parameter_types! { + pub const Seven: u32 = 7; + pub const Four: u32 = 4; + } + + frame_support::generate_storage_alias! { Prefix, Foo => Value> } + + #[test] + fn store_works() { + TestExternalities::default().execute_with(|| { + let bounded: BoundedSortedVec = vec![1, 2, 3].try_into().unwrap(); + Foo::put(bounded.clone()); + assert_eq!(Foo::get().unwrap(), bounded); + }); + } + + #[test] + fn try_append_is_correct() { + assert_eq!(BoundedSortedVec::::bound(), 7); + } + + #[test] + fn try_insert_works() { + let mut bounded: BoundedSortedVec = vec![1, 2, 3].try_into().unwrap(); + bounded.try_insert(1).unwrap(); + assert_eq!(*bounded, vec![1, 1, 2, 3]); + + assert!(bounded.try_insert(0).is_err()); + assert_eq!(*bounded, vec![1, 1, 2, 3]); + } + + #[test] + #[should_panic] + fn try_insert_panics_if_oob() { + let mut bounded: BoundedSortedVec = vec![1, 2, 3, 4].try_into().unwrap(); + bounded.try_insert(9).unwrap(); + } + + #[test] + fn try_push_works() { + let mut bounded: BoundedSortedVec = vec![1, 2, 3].try_into().unwrap(); + bounded.try_push(0).unwrap(); + assert_eq!(*bounded, vec![0, 1, 2, 3]); + + assert!(bounded.try_push(9).is_err()); + } + + #[test] + fn deref_coercion_works() { + let bounded: BoundedSortedVec = vec![1, 2, 3].try_into().unwrap(); + // these methods come from deref-ed vec. + assert_eq!(bounded.len(), 3); + assert!(bounded.iter().next().is_some()); + assert!(!bounded.is_empty()); + } + + #[test] + fn slice_indexing_works() { + let bounded: BoundedSortedVec = vec![1, 2, 3, 4, 5, 6].try_into().unwrap(); + assert_eq!(&bounded[0..=2], &[1, 2, 3]); + } + + #[test] + fn vec_eq_works() { + let bounded: BoundedSortedVec = vec![1, 2, 3, 4, 5, 6].try_into().unwrap(); + assert_eq!(bounded, vec![1, 2, 3, 4, 5, 6]); + } + + #[test] + fn too_big_vec_fail_to_decode() { + let v: Vec = vec![1, 2, 3, 4, 5]; + assert_eq!( + BoundedSortedVec::::decode(&mut &v.encode()[..]), + Err("BoundedSortedVec exceeds its limit".into()), + ); + } +} diff --git a/frame/composable-support/src/collections/vec/mod.rs b/frame/composable-support/src/collections/vec/mod.rs new file mode 100644 index 00000000000..57c94bad77e --- /dev/null +++ b/frame/composable-support/src/collections/vec/mod.rs @@ -0,0 +1,8 @@ +/// Bounded collections types. For inputs of extrinsics, you'll want these 99% of the time. +pub mod bounded; + +/// Sorted collection types, useful for keeping data in a valid state through the type system. +pub mod sorted; + +pub use bounded::BoundedSortedVec; +pub use sorted::SortedVec; diff --git a/frame/composable-support/src/collections/vec/sorted/mod.rs b/frame/composable-support/src/collections/vec/sorted/mod.rs new file mode 100644 index 00000000000..34aa6448f43 --- /dev/null +++ b/frame/composable-support/src/collections/vec/sorted/mod.rs @@ -0,0 +1,448 @@ +//! Sorted vectors. +//! +//! Cannibalized from [Repository](https://gitlab.com/spearman/sorted-vec) to be no_std and substrate compatible. +//! +//! - `SortedVec` -- sorted from least to greatest, may contain duplicates +//! - `SortedSet` -- sorted from least to greatest, unique elements +use codec::{Decode, Encode, EncodeLike, WrapperTypeEncode}; +use core::hash::Hash; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use sp_std::prelude::*; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(all(feature = "serde", not(feature = "serde-nontransparent")), serde(transparent))] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] +pub struct SortedVec { + #[cfg_attr(feature = "serde", serde(deserialize_with = "parse_vec"))] + #[cfg_attr(feature = "serde", serde(bound(deserialize = "T : serde::Deserialize <'de>")))] + vec: Vec, +} + +impl<'a, T: Encode + Decode + Ord> EncodeLike> for SortedVec {} +impl WrapperTypeEncode for SortedVec {} + +impl Decode for SortedVec { + fn decode(input: &mut I) -> Result { + let inner = Vec::::decode(input)?; + Ok(Self::from_unsorted(inner)) + } + + fn skip(input: &mut I) -> Result<(), codec::Error> { + Vec::::skip(input) + } +} + +#[cfg(feature = "serde")] +fn parse_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: Ord + serde::Deserialize<'de>, +{ + use is_sorted::IsSorted; + use serde::de::Error; + + let v = Vec::deserialize(deserializer)?; + if !IsSorted::is_sorted(&mut v.iter()) { + Err(D::Error::custom("input sequence is not sorted")) + } else { + Ok(v) + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(all(feature = "serde", not(feature = "serde-nontransparent")), serde(transparent))] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] +pub struct SortedSet { + set: SortedVec, +} + +impl SortedVec { + #[inline] + pub fn new() -> Self { + SortedVec { vec: Vec::new() } + } + + pub(crate) fn unchecked_from(vec: Vec) -> Self { + SortedVec { vec } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + SortedVec { vec: Vec::with_capacity(capacity) } + } + + /// Appends the item to the end of the vector. If that would unsort the vector, it returns the + /// item as an Err. + #[inline] + pub fn append(&mut self, t: T) -> Result { + if let Some(last) = self.last() { + if last <= &t { + let idx = self.append_unchecked(t); + Ok(idx) + } else { + Err(t) + } + } else { + let idx = self.insert(t); + Ok(idx) + } + } + + fn append_unchecked(&mut self, t: T) -> usize { + self.vec.push(t); + self.len() - 1 + } + + /// Uses `sort_unstable()` to sort in place. + #[inline] + pub fn from_unsorted(mut vec: Vec) -> Self { + vec.sort_unstable(); + SortedVec { vec } + } + + /// Insert an element into sorted position, returning the order index at which + /// it was placed. + #[inline] + pub fn insert(&mut self, element: T) -> usize { + let insert_at = match self.binary_search(&element) { + Ok(insert_at) | Err(insert_at) => insert_at, + }; + self.vec.insert(insert_at, element); + insert_at + } + + /// Find the element and return the index with `Ok`, otherwise insert the + /// element and return the new element index with `Err`. + #[inline] + pub fn find_or_insert(&mut self, element: T) -> Result { + self.binary_search(&element).map_err(|insert_at| { + self.vec.insert(insert_at, element); + insert_at + }) + } + + #[inline] + pub fn remove_item(&mut self, item: &T) -> Option { + match self.vec.binary_search(item) { + Ok(remove_at) => Some(self.vec.remove(remove_at)), + Err(_) => None, + } + } + + /// Panics if index is out of bounds + #[inline] + pub fn remove(&mut self, index: usize) -> T { + self.vec.remove(index) + } + + #[inline] + pub fn pop(&mut self) -> Option { + self.vec.pop() + } + + #[inline] + pub fn clear(&mut self) { + self.vec.clear() + } + + #[inline] + pub fn dedup(&mut self) { + self.vec.dedup(); + } + + #[inline] + pub fn dedup_by_key(&mut self, key: F) + where + F: FnMut(&mut T) -> K, + K: PartialEq, + { + self.vec.dedup_by_key(key); + } + + #[inline] + pub fn drain(&mut self, range: R) -> sp_std::vec::Drain + where + R: core::ops::RangeBounds, + { + self.vec.drain(range) + } + + #[inline] + pub fn retain(&mut self, f: F) + where + F: FnMut(&T) -> bool, + { + self.vec.retain(f) + } + + #[inline] + pub fn into_inner(self) -> Vec { + self.vec + } + + #[inline] + pub fn as_inner(&self) -> &[T] { + &self.vec + } + + /// Apply a closure mutating the sorted vector and use `sort_unstable()` + /// to re-sort the mutated vector + #[inline] + pub fn mutate(&mut self, f: F) -> O + where + F: FnOnce(&mut Vec) -> O, + { + let res = f(&mut self.vec); + self.vec.sort_unstable(); + res + } +} + +impl Default for SortedVec { + fn default() -> Self { + Self::new() + } +} + +impl From> for SortedVec { + #[inline] + fn from(unsorted: Vec) -> Self { + Self::from_unsorted(unsorted) + } +} + +impl core::ops::Deref for SortedVec { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Vec { + &self.vec + } +} + +impl Extend for SortedVec { + #[inline] + fn extend>(&mut self, iter: I) { + for t in iter { + if let Err(t) = self.append(t) { + self.insert(t); + } + } + } +} + +impl SortedSet { + #[inline] + pub fn new() -> Self { + SortedSet { set: SortedVec::new() } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + SortedSet { set: SortedVec::with_capacity(capacity) } + } + + /// Uses `sort_unstable()` to sort in place and `dedup()` to remove + /// duplicates. + #[inline] + pub fn from_unsorted(vec: Vec) -> Self { + let mut set = SortedVec::from_unsorted(vec); + set.dedup(); + SortedSet { set } + } + + /// Insert an element into sorted position, returning the order index at which + /// it was placed. + #[inline] + pub fn insert(&mut self, element: T) -> usize { + let _ = self.remove_item(&element); + self.set.insert(element) + } + + /// Find the element and return the index with `Ok`, otherwise insert the + /// element and return the new element index with `Err`. + #[inline] + pub fn find_or_insert(&mut self, element: T) -> Result { + self.set.find_or_insert(element) + } + + #[inline] + pub fn remove_item(&mut self, item: &T) -> Option { + self.set.remove_item(item) + } + + /// Panics if index is out of bounds + #[inline] + pub fn remove(&mut self, index: usize) -> T { + self.set.remove(index) + } + + #[inline] + pub fn pop(&mut self) -> Option { + self.set.pop() + } + + #[inline] + pub fn clear(&mut self) { + self.set.clear() + } + + #[inline] + pub fn drain(&mut self, range: R) -> sp_std::vec::Drain + where + R: core::ops::RangeBounds, + { + self.set.drain(range) + } + + #[inline] + pub fn retain(&mut self, f: F) + where + F: FnMut(&T) -> bool, + { + self.set.retain(f) + } + + #[inline] + pub fn into_inner(self) -> Vec { + self.set.into_inner() + } + + /// Apply a closure mutating the sorted vector and use `sort_unstable()` + /// to re-sort the mutated vector and `dedup()` to remove any duplicate + /// values + #[inline] + pub fn mutate(&mut self, f: F) -> O + where + F: FnOnce(&mut Vec) -> O, + { + let res = self.set.mutate(f); + self.set.dedup(); + res + } +} + +impl Default for SortedSet { + fn default() -> Self { + Self::new() + } +} + +impl From> for SortedSet { + #[inline] + fn from(unsorted: Vec) -> Self { + Self::from_unsorted(unsorted) + } +} + +impl core::ops::Deref for SortedSet { + type Target = SortedVec; + + #[inline] + fn deref(&self) -> &SortedVec { + &self.set + } +} + +impl Extend for SortedSet { + #[inline] + fn extend>(&mut self, iter: I) { + for t in iter { + let _ = self.insert(t); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sorted_vec() { + let mut v = SortedVec::new(); + assert_eq!(v.insert(5), 0); + assert_eq!(v.insert(3), 0); + assert_eq!(v.insert(4), 1); + assert_eq!(v.insert(4), 1); + assert_eq!(v.find_or_insert(4), Ok(2)); + assert_eq!(v.len(), 4); + v.dedup(); + assert_eq!(v.len(), 3); + assert_eq!(v.binary_search(&3), Ok(0)); + assert_eq!( + *SortedVec::from_unsorted(vec![5, -10, 99, -11, 2, 17, 10]), + vec![-11, -10, 2, 5, 10, 17, 99] + ); + assert_eq!( + SortedVec::from_unsorted(vec![5, -10, 99, -11, 2, 17, 10]), + vec![5, -10, 99, -11, 2, 17, 10].into() + ); + let mut v = SortedVec::new(); + v.extend(vec![5, -10, 99, -11, 2, 17, 10].into_iter()); + assert_eq!(*v, vec![-11, -10, 2, 5, 10, 17, 99]); + let _ = v.mutate(|v| { + v[0] = 11; + v[3] = 1; + }); + assert_eq!(v.drain(..).collect::>(), vec![-10, 1, 2, 10, 11, 17, 99]); + } + + #[test] + fn test_sorted_set() { + let mut s = SortedSet::new(); + assert_eq!(s.insert(5), 0); + assert_eq!(s.insert(3), 0); + assert_eq!(s.insert(4), 1); + assert_eq!(s.insert(4), 1); + assert_eq!(s.find_or_insert(4), Ok(1)); + assert_eq!(s.len(), 3); + assert_eq!(s.binary_search(&3), Ok(0)); + assert_eq!( + **SortedSet::from_unsorted(vec![5, -10, 99, -10, -11, 10, 2, 17, 10]), + vec![-11, -10, 2, 5, 10, 17, 99] + ); + assert_eq!( + SortedSet::from_unsorted(vec![5, -10, 99, -10, -11, 10, 2, 17, 10]), + vec![5, -10, 99, -10, -11, 10, 2, 17, 10].into() + ); + let mut s = SortedSet::new(); + s.extend(vec![5, -11, -10, 99, -11, 2, 17, 2, 10].into_iter()); + assert_eq!(**s, vec![-11, -10, 2, 5, 10, 17, 99]); + let _ = s.mutate(|s| { + s[0] = 5; + s[3] = 1; + }); + assert_eq!(s.drain(..).collect::>(), vec![-10, 1, 2, 5, 10, 17, 99]); + } + + #[cfg(feature = "serde-nontransparent")] + #[test] + fn test_deserialize() { + let s = r#"{"vec":[-11,-10,2,5,10,17,99]}"#; + let _ = serde_json::from_str::>(s).unwrap(); + } + + #[cfg(all(feature = "serde", not(feature = "serde-nontransparent")))] + #[test] + fn test_deserialize() { + let s = "[-11,-10,2,5,10,17,99]"; + let _ = serde_json::from_str::>(s).unwrap(); + } + + #[cfg(feature = "serde-nontransparent")] + #[test] + #[should_panic] + fn test_deserialize_unsorted() { + let s = r#"{"vec":[99,-11,-10,2,5,10,17]}"#; + let _ = serde_json::from_str::>(s).unwrap(); + } + + #[cfg(all(feature = "serde", not(feature = "serde-nontransparent")))] + #[test] + #[should_panic] + fn test_deserialize_unsorted() { + let s = "[99,-11,-10,2,5,10,17]"; + let _ = serde_json::from_str::>(s).unwrap(); + } +} diff --git a/frame/composable-support/src/lib.rs b/frame/composable-support/src/lib.rs index eae93b38985..6c7d99386a4 100644 --- a/frame/composable-support/src/lib.rs +++ b/frame/composable-support/src/lib.rs @@ -12,4 +12,5 @@ #![warn(clippy::unseparated_literal_suffix)] #![cfg_attr(not(feature = "std"), no_std)] +pub mod collections; pub mod validation;