From 27b7572842c4be0d28c98912a7fd49f6deba663f Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 30 Jul 2021 09:45:25 +1000 Subject: [PATCH] 468 valueset abstraction (#538) --- ethics/EXAMPLES.md | 2 +- kanidmd/src/lib/entry.rs | 104 +++++++------- kanidmd/src/lib/lib.rs | 1 + kanidmd/src/lib/macros.rs | 27 +++- kanidmd/src/lib/plugins/memberof.rs | 7 +- kanidmd/src/lib/schema.rs | 23 +-- kanidmd/src/lib/value.rs | 14 +- kanidmd/src/lib/valueset.rs | 210 ++++++++++++++++++++++++++++ 8 files changed, 317 insertions(+), 71 deletions(-) create mode 100644 kanidmd/src/lib/valueset.rs diff --git a/ethics/EXAMPLES.md b/ethics/EXAMPLES.md index f6aedc1a6..9f0cadd2e 100644 --- a/ethics/EXAMPLES.md +++ b/ethics/EXAMPLES.md @@ -33,7 +33,7 @@ because names can take many forms such as. * lastname firstname And many many more that are not listed here. This is why our names are displayName as a freetext -UTF8 field, with case sensitivitiy and no limits. +UTF8 field, with case sensitivity and no limits. ## Informed consent and Privacy of their data diff --git a/kanidmd/src/lib/entry.rs b/kanidmd/src/lib/entry.rs index 2da0b5531..7bd516f43 100644 --- a/kanidmd/src/lib/entry.rs +++ b/kanidmd/src/lib/entry.rs @@ -33,6 +33,7 @@ use crate::repl::cid::Cid; use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction}; use crate::value::{IndexType, SyntaxType}; use crate::value::{PartialValue, Value}; +use crate::valueset::ValueSet; use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Filter as ProtoFilter; use kanidm_proto::v1::{OperationError, SchemaError}; @@ -151,7 +152,7 @@ pub struct EntryReduced { uuid: Uuid, } -fn compare_attrs(left: &Map>, right: &Map>) -> bool { +fn compare_attrs(left: &Map, right: &Map) -> bool { // We can't shortcut based on len because cid mod may not be present. // Build the set of all keys between both. let allkeys: Set<&str> = left @@ -198,7 +199,7 @@ pub struct Entry { valid: VALID, state: STATE, // We may need to change this to Set to allow borrow of Value -> PartialValue for lookups. - attrs: Map>, + attrs: Map, } impl std::fmt::Debug for Entry @@ -270,12 +271,12 @@ impl Entry { // Somehow we need to take the tree of e attrs, and convert // all ref types to our types ... - let map2: Result>, OperationError> = e + let map2: Result, OperationError> = e .attrs .iter() .map(|(k, v)| { let nk = qs.get_schema().normalise_attr_name(k); - let nv: Result, _> = + let nv: Result = v.iter().map(|vr| qs.clone_value(audit, &nk, vr)).collect(); match nv { Ok(nvi) => Ok((nk, nvi)), @@ -325,10 +326,10 @@ impl Entry { // str -> proto entry let pe: ProtoEntry = serde_json::from_str(es).expect("Invalid Proto Entry"); // use a const map to convert str -> ava - let x: Map> = pe.attrs.into_iter() + let x: Map = pe.attrs.into_iter() .map(|(k, vs)| { let attr = AttrString::from(k.to_lowercase()); - let vv: Set = match attr.as_str() { + let vv: ValueSet = match attr.as_str() { "attributename" | "classname" | "domain" => { vs.into_iter().map(|v| Value::new_iutf8(&v)).collect() } @@ -500,8 +501,12 @@ impl Entry { } /// Replace the existing content of an attribute set of this Entry, with a new set of Values. - pub fn set_ava(&mut self, attr: &str, values: Set) { - self.set_ava_int(attr, values) + // pub fn set_ava(&mut self, attr: &str, values: Set) { + pub fn set_ava(&mut self, attr: &str, iter: T) + where + T: IntoIterator, + { + self.set_ava_int(attr, iter) } } @@ -1290,12 +1295,12 @@ impl Entry { pub fn from_dbentry(au: &mut AuditScope, db_e: DbEntry, id: u64) -> Result { // Convert attrs from db format to value - let r_attrs: Result>, ()> = match db_e.ent { + let r_attrs: Result, ()> = match db_e.ent { DbEntryVers::V1(v1) => v1 .attrs .into_iter() .map(|(k, vs)| { - let vv: Result, ()> = + let vv: Result = vs.into_iter().map(Value::from_db_valuev1).collect(); match vv { Ok(vv) => Ok((k, vv)), @@ -1377,14 +1382,14 @@ impl Entry { /// Convert this recycled entry, into a tombstone ready for reaping. pub fn to_tombstone(&self, cid: Cid) -> Entry { // Duplicate this to a tombstone entry - let class_ava = btreeset![Value::new_class("object"), Value::new_class("tombstone")]; - let last_mod_ava = btreeset![Value::new_cid(cid.clone())]; + let class_ava = valueset![Value::new_class("object"), Value::new_class("tombstone")]; + let last_mod_ava = valueset![Value::new_cid(cid.clone())]; - let mut attrs_new: Map> = Map::new(); + let mut attrs_new: Map = Map::new(); attrs_new.insert( AttrString::from("uuid"), - btreeset![Value::new_uuidr(&self.get_uuid())], + valueset![Value::new_uuidr(&self.get_uuid())], ); attrs_new.insert(AttrString::from("class"), class_ava); attrs_new.insert(AttrString::from("last_modified_cid"), last_mod_ava); @@ -1575,21 +1580,26 @@ impl Entry { let v = self .attrs .entry(AttrString::from(attr)) - .or_insert_with(Set::new); + .or_insert_with(ValueSet::new); // Here we need to actually do a check/binary search ... v.insert(value); // Doesn't matter if it already exists, equality will replace. } /// Overwrite the current set of values for an attribute, with this new set. - pub fn set_ava_int(&mut self, attr: &str, values: Set) { + // pub fn set_ava_int(&mut self, attr: &str, values: Set) { + pub fn set_ava_int(&mut self, attr: &str, iter: T) + where + T: IntoIterator, + { // Overwrite the existing value, build a tree from the list. + let values = iter.into_iter().collect(); let _ = self.attrs.insert(AttrString::from(attr), values); } /// Update the last_changed flag of this entry to the given change identifier. fn set_last_changed(&mut self, cid: Cid) { - let cv = btreeset![Value::new_cid(cid)]; + let cv = valueset![Value::new_cid(cid)]; let _ = self.attrs.insert(AttrString::from("last_modified_cid"), cv); } @@ -1608,7 +1618,7 @@ impl Entry { #[inline(always)] /// Return a reference to the current set of values that are associated to this attribute. - pub fn get_ava_set(&self, attr: &str) -> Option<&Set> { + pub fn get_ava_set(&self, attr: &str) -> Option<&ValueSet> { self.attrs.get(attr) } @@ -1755,24 +1765,20 @@ impl Entry { /// Assert if an attribute of this name is present, and one of it's values contains /// the following substring, if possible to perform the substring comparison. pub fn attribute_substring(&self, attr: &str, subvalue: &PartialValue) -> bool { - match self.attrs.get(attr) { - Some(v_list) => v_list - .iter() - .fold(false, |acc, v| if acc { acc } else { v.contains(subvalue) }), - None => false, - } + self.attrs + .get(attr) + .map(|vset| vset.substring(subvalue)) + .unwrap_or(false) } #[inline(always)] /// Assert if an attribute of this name is present, and one of it's values is less than /// the following partial value pub fn attribute_lessthan(&self, attr: &str, subvalue: &PartialValue) -> bool { - match self.attrs.get(attr) { - Some(v_list) => v_list - .iter() - .fold(false, |acc, v| if acc { acc } else { v.lessthan(subvalue) }), - None => false, - } + self.attrs + .get(attr) + .map(|vset| vset.lessthan(subvalue)) + .unwrap_or(false) } // Since EntryValid/Invalid is just about class adherenece, not Value correctness, we @@ -1946,13 +1952,17 @@ where } /// Remove all values of this attribute from the entry, and return their content. - pub fn pop_ava(&mut self, attr: &str) -> Option> { + pub fn pop_ava(&mut self, attr: &str) -> Option { self.attrs.remove(attr) } /// Replace the content of this attribute with a new value set. - pub fn set_ava(&mut self, attr: &str, values: Set) { - self.set_ava_int(attr, values) + // pub fn set_ava(&mut self, attr: &str, values: Set) { + pub fn set_ava(&mut self, attr: &str, iter: T) + where + T: IntoIterator, + { + self.set_ava_int(attr, iter) } /* @@ -1999,21 +2009,21 @@ impl PartialEq for Entry { impl From<&SchemaAttribute> for Entry { fn from(s: &SchemaAttribute) -> Self { // Convert an Attribute to an entry ... make it good! - let uuid_v = btreeset![Value::new_uuidr(&s.uuid)]; + let uuid_v = valueset![Value::new_uuidr(&s.uuid)]; - let name_v = btreeset![Value::new_iutf8(s.name.as_str())]; - let desc_v = btreeset![Value::new_utf8(s.description.clone())]; + let name_v = valueset![Value::new_iutf8(s.name.as_str())]; + let desc_v = valueset![Value::new_utf8(s.description.clone())]; - let multivalue_v = btreeset![Value::from(s.multivalue)]; - let unique_v = btreeset![Value::from(s.unique)]; + let multivalue_v = valueset![Value::from(s.multivalue)]; + let unique_v = valueset![Value::from(s.unique)]; - let index_v: Set<_> = s.index.iter().map(|i| Value::from(i.clone())).collect(); + let index_v: ValueSet = s.index.iter().cloned().map(Value::from).collect(); - let syntax_v = btreeset![Value::from(s.syntax.clone())]; + let syntax_v = valueset![Value::from(s.syntax.clone())]; // Build the Map of the attributes relevant // let mut attrs: Map> = Map::with_capacity(8); - let mut attrs: Map> = Map::new(); + let mut attrs: Map = Map::new(); attrs.insert(AttrString::from("attributename"), name_v); attrs.insert(AttrString::from("description"), desc_v); attrs.insert(AttrString::from("uuid"), uuid_v); @@ -2023,7 +2033,7 @@ impl From<&SchemaAttribute> for Entry { attrs.insert(AttrString::from("syntax"), syntax_v); attrs.insert( AttrString::from("class"), - btreeset![ + valueset![ Value::new_class("object"), Value::new_class("system"), Value::new_class("attributetype") @@ -2042,19 +2052,19 @@ impl From<&SchemaAttribute> for Entry { impl From<&SchemaClass> for Entry { fn from(s: &SchemaClass) -> Self { - let uuid_v = btreeset![Value::new_uuidr(&s.uuid)]; + let uuid_v = valueset![Value::new_uuidr(&s.uuid)]; - let name_v = btreeset![Value::new_iutf8(s.name.as_str())]; - let desc_v = btreeset![Value::new_utf8(s.description.clone())]; + let name_v = valueset![Value::new_iutf8(s.name.as_str())]; + let desc_v = valueset![Value::new_utf8(s.description.clone())]; // let mut attrs: Map> = Map::with_capacity(8); - let mut attrs: Map> = Map::new(); + let mut attrs: Map = Map::new(); attrs.insert(AttrString::from("classname"), name_v); attrs.insert(AttrString::from("description"), desc_v); attrs.insert(AttrString::from("uuid"), uuid_v); attrs.insert( AttrString::from("class"), - btreeset![ + valueset![ Value::new_class("object"), Value::new_class("system"), Value::new_class("classtype") diff --git a/kanidmd/src/lib/lib.rs b/kanidmd/src/lib/lib.rs index 527b6c0c3..35fcba4cd 100644 --- a/kanidmd/src/lib/lib.rs +++ b/kanidmd/src/lib/lib.rs @@ -47,6 +47,7 @@ mod interval; pub(crate) mod ldap; mod modify; pub mod value; +pub mod valueset; #[macro_use] mod plugins; mod access; diff --git a/kanidmd/src/lib/macros.rs b/kanidmd/src/lib/macros.rs index f286324a5..3e5e0869c 100644 --- a/kanidmd/src/lib/macros.rs +++ b/kanidmd/src/lib/macros.rs @@ -145,6 +145,7 @@ macro_rules! entry_str_to_account { use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::idm::account::Account; use crate::value::Value; + use std::iter::once; let mut e: Entry = unsafe { Entry::unsafe_from_entry_str($entry_str).into_invalid_new() }; @@ -153,7 +154,7 @@ macro_rules! entry_str_to_account { .get_ava_single_str("name") .map(|s| Value::new_spn_str(s, "example.com")) .expect("Failed to munge spn from name!"); - e.set_ava("spn", btreeset![spn]); + e.set_ava("spn", once(spn)); let e = unsafe { e.into_sealed_committed() }; @@ -576,6 +577,30 @@ macro_rules! btreeset { }); } +#[allow(unused_macros)] +#[macro_export] +macro_rules! valueset { + () => ( + compile_error!("ValueSet needs at least 1 element") + ); + ($e:expr) => ({ + use crate::valueset::ValueSet; + let mut x: ValueSet = ValueSet::new(); + assert!(x.insert($e)); + x + }); + ($e:expr,) => ({ + valueset!($e) + }); + ($e:expr, $($item:expr),*) => ({ + use crate::valueset::ValueSet; + let mut x: ValueSet = ValueSet::new(); + assert!(x.insert($e)); + $(assert!(x.insert($item));)* + x + }); +} + #[allow(unused_macros)] #[macro_export] macro_rules! entry_init { diff --git a/kanidmd/src/lib/plugins/memberof.rs b/kanidmd/src/lib/plugins/memberof.rs index a1c12745d..82bc67f52 100644 --- a/kanidmd/src/lib/plugins/memberof.rs +++ b/kanidmd/src/lib/plugins/memberof.rs @@ -12,14 +12,13 @@ use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntrySealed}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent}; -use crate::prelude::*; -// use crate::modify::{Modify, ModifyList}; use crate::plugins::Plugin; +use crate::prelude::*; use crate::value::{PartialValue, Value}; +use crate::valueset::ValueSet; use kanidm_proto::v1::{ConsistencyError, OperationError}; use hashbrown::HashMap; -use std::collections::BTreeSet; use uuid::Uuid; lazy_static! { @@ -351,7 +350,7 @@ impl Plugin for MemberOf { }; // for all direct -> add uuid to map - let d_groups_set: BTreeSet<_> = direct_memberof + let d_groups_set: ValueSet = direct_memberof .iter() .map(|e| Value::new_refer(*e.get_uuid())) .collect(); diff --git a/kanidmd/src/lib/schema.rs b/kanidmd/src/lib/schema.rs index 709acd59e..261a6ed50 100644 --- a/kanidmd/src/lib/schema.rs +++ b/kanidmd/src/lib/schema.rs @@ -19,11 +19,12 @@ use crate::audit::AuditScope; use crate::be::IdxKey; use crate::prelude::*; +use crate::valueset::ValueSet; use kanidm_proto::v1::{ConsistencyError, OperationError, SchemaError}; use hashbrown::{HashMap, HashSet}; use std::borrow::Borrow; -use std::collections::BTreeSet; +// use std::collections::BTreeSet; use uuid::Uuid; // use concread::cowcell::asynch::*; @@ -222,7 +223,7 @@ impl SchemaAttribute { } } - pub fn validate_ava(&self, a: &str, ava: &BTreeSet) -> Result<(), SchemaError> { + pub fn validate_ava(&self, a: &str, ava: &ValueSet) -> Result<(), SchemaError> { // ltrace!("Checking for valid {:?} -> {:?}", self.name, ava); // Check multivalue if !self.multivalue && ava.len() > 1 { @@ -1737,12 +1738,12 @@ mod tests { }; let r1 = - single_value_string.validate_ava("single_value", &btreeset![Value::new_iutf8("test")]); + single_value_string.validate_ava("single_value", &valueset![Value::new_iutf8("test")]); assert_eq!(r1, Ok(())); let r2 = single_value_string.validate_ava( "single_value", - &btreeset![Value::new_iutf8("test1"), Value::new_iutf8("test2")], + &valueset![Value::new_iutf8("test1"), Value::new_iutf8("test2")], ); assert_eq!( r2, @@ -1767,7 +1768,7 @@ mod tests { let r5 = multi_value_string.validate_ava( "mv_string", - &btreeset![Value::new_utf8s("test1"), Value::new_utf8s("test2")], + &valueset![Value::new_utf8s("test1"), Value::new_utf8s("test2")], ); assert_eq!(r5, Ok(())); @@ -1785,7 +1786,7 @@ mod tests { let r3 = multi_value_boolean.validate_ava( "mv_bool", - &btreeset![ + &valueset![ Value::new_bool(true), Value::new_iutf8("test1"), Value::new_iutf8("test2") @@ -1798,7 +1799,7 @@ mod tests { let r4 = multi_value_boolean.validate_ava( "mv_bool", - &btreeset![Value::new_bool(true), Value::new_bool(false)], + &valueset![Value::new_bool(true), Value::new_bool(false)], ); assert_eq!(r4, Ok(())); @@ -1817,12 +1818,12 @@ mod tests { let r6 = single_value_syntax.validate_ava( "sv_syntax", - &btreeset![Value::new_syntaxs("UTF8STRING").unwrap()], + &valueset![Value::new_syntaxs("UTF8STRING").unwrap()], ); assert_eq!(r6, Ok(())); let r7 = single_value_syntax - .validate_ava("sv_syntax", &btreeset![Value::new_utf8s("thaeountaheu")]); + .validate_ava("sv_syntax", &valueset![Value::new_utf8s("thaeountaheu")]); assert_eq!( r7, Err(SchemaError::InvalidAttributeSyntax("sv_syntax".to_string())) @@ -1842,12 +1843,12 @@ mod tests { // let r8 = single_value_index.validate_ava( "sv_index", - &btreeset![Value::new_indexs("EQUALITY").unwrap()], + &valueset![Value::new_indexs("EQUALITY").unwrap()], ); assert_eq!(r8, Ok(())); let r9 = single_value_index - .validate_ava("sv_index", &btreeset![Value::new_utf8s("thaeountaheu")]); + .validate_ava("sv_index", &valueset![Value::new_utf8s("thaeountaheu")]); assert_eq!( r9, Err(SchemaError::InvalidAttributeSyntax("sv_index".to_string())) diff --git a/kanidmd/src/lib/value.rs b/kanidmd/src/lib/value.rs index 6283f14bc..7b36dc5ba 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/src/lib/value.rs @@ -558,7 +558,7 @@ impl PartialValue { } } - pub fn contains(&self, s: &PartialValue) -> bool { + pub fn substring(&self, s: &PartialValue) -> bool { match (self, s) { (PartialValue::Utf8(s1), PartialValue::Utf8(s2)) => s1.contains(s2), (PartialValue::Iutf8(s1), PartialValue::Iutf8(s2)) => s1.contains(s2), @@ -620,10 +620,10 @@ impl PartialValue { /// or modification operation where you are applying a set of complete values into an entry. #[derive(Clone, Debug)] pub struct Value { - pv: PartialValue, + pub(crate) pv: PartialValue, // Later we'll add extra data fields for different v types. They'll have to switch on // pv somehow, so probably need optional or union? - data: Option>, + pub(crate) data: Option>, } // TODO: Impl display @@ -1072,14 +1072,14 @@ impl Value { self.pv.is_url() } - pub fn contains(&self, s: &PartialValue) -> bool { - self.pv.contains(s) - } - pub fn lessthan(&self, s: &PartialValue) -> bool { self.pv.lessthan(s) } + pub fn substring(&self, s: &PartialValue) -> bool { + self.pv.substring(s) + } + // Converters between DBRepr -> MemRepr. It's likely many of these // will be just wrappers to our from str types. diff --git a/kanidmd/src/lib/valueset.rs b/kanidmd/src/lib/valueset.rs new file mode 100644 index 000000000..182ab5d38 --- /dev/null +++ b/kanidmd/src/lib/valueset.rs @@ -0,0 +1,210 @@ +use crate::prelude::*; +// use hashbrown::HashSet; +use std::borrow::Borrow; +use std::collections::BTreeSet; +use std::iter::FromIterator; + +pub struct ValueSet { + inner: BTreeSet, +} + +impl Default for ValueSet { + fn default() -> Self { + ValueSet { + inner: BTreeSet::new(), + } + } +} + +impl ValueSet { + pub fn new() -> Self { + Self::default() + } + + // insert + pub fn insert(&mut self, value: Value) -> bool { + // Return true if the element is new. + self.inner.insert(value) + } + + // set values + pub fn set(&mut self, iter: impl Iterator) { + self.inner.clear(); + self.inner.extend(iter); + } + + pub fn get(&self, value: &Q) -> Option<&Value> + where + Value: Borrow + Ord, + Q: Ord + ?Sized, + { + self.inner.get(value) + } + + // delete a value + pub fn remove(&mut self, value: &Q) -> bool + where + Value: Borrow + Ord, + Q: Ord + ?Sized, + { + self.inner.remove(value) + } + + pub fn contains(&self, value: &Q) -> bool + where + Value: Borrow + Ord, + Q: Ord + ?Sized, + { + self.inner.contains(value) + } + + pub fn substring(&self, value: &PartialValue) -> bool { + self.inner.iter().any(|v| v.substring(value)) + } + + pub fn lessthan(&self, value: &PartialValue) -> bool { + self.inner.iter().any(|v| v.lessthan(value)) + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + // We'll need to be able to do partialeq/intersect etc later. + + pub fn iter(&self) -> Iter { + (&self).into_iter() + } + + pub fn difference<'a>(&'a self, other: &'a ValueSet) -> Difference<'a> { + Difference { + iter: self.inner.difference(&other.inner), + } + } + + pub fn symmetric_difference<'a>(&'a self, other: &'a ValueSet) -> SymmetricDifference<'a> { + SymmetricDifference { + iter: self.inner.symmetric_difference(&other.inner), + } + } +} + +impl PartialEq for ValueSet { + fn eq(&self, other: &Self) -> bool { + self.inner.eq(&other.inner) + } +} + +impl FromIterator for ValueSet { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + ValueSet { + inner: BTreeSet::from_iter(iter), + } + } +} + +impl Clone for ValueSet { + fn clone(&self) -> Self { + ValueSet { + inner: self.inner.clone(), + } + } +} + +pub struct Iter<'a> { + iter: std::collections::btree_set::Iter<'a, Value>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option<&'a Value> { + self.iter.next() + } +} + +impl<'a> IntoIterator for &'a ValueSet { + type Item = &'a Value; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + Iter { + iter: (&self.inner).into_iter(), + } + } +} + +pub struct Difference<'a> { + iter: std::collections::btree_set::Difference<'a, Value>, +} + +impl<'a> Iterator for Difference<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option<&'a Value> { + self.iter.next() + } +} + +pub struct SymmetricDifference<'a> { + iter: std::collections::btree_set::SymmetricDifference<'a, Value>, +} + +impl<'a> Iterator for SymmetricDifference<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option<&'a Value> { + self.iter.next() + } +} + +pub struct IntoIter { + iter: std::collections::btree_set::IntoIter, +} + +impl Iterator for IntoIter { + type Item = Value; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +impl IntoIterator for ValueSet { + type Item = Value; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + iter: self.inner.into_iter(), + } + } +} + +impl std::fmt::Debug for ValueSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ValueSet") + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(test)] +mod tests { + use crate::value::Value; + use crate::valueset::ValueSet; + + #[test] + fn test_valueset_basic() { + let mut vs = ValueSet::new(); + assert!(vs.insert(Value::new_uint32(0))); + assert!(!vs.insert(Value::new_uint32(0))); + } +}