Skip to main content

wasm_link/
cardinality.rs

1//! Cardinality wrappers for plugin collections.
2
3use std::collections::HashMap ;
4use std::hash::Hash ;
5
6use nonempty_collections::{ NEMap, NonEmptyIterator, IntoNonEmptyIterator };
7use wasmtime::component::Val ;
8
9
10
11/// Cardinality behavior for plugin containers.
12///
13/// Implementations preserve the container shape while allowing the inner value
14/// type to be transformed.
15pub trait Cardinality<Id, T>: Sized {
16	/// Same cardinality with a different inner type.
17	type Rebind<U>;
18
19	/// Maps values by reference while preserving cardinality.
20	fn map<N>( &self, map: impl FnMut( &Id, &T ) -> N ) -> Self::Rebind<N>
21	where
22		Id: Clone;
23
24	/// Maps values by value while preserving cardinality.
25	fn map_mut<N>( self, map: impl FnMut( T ) -> N ) -> Self::Rebind<N> ;
26
27	/// Returns the value associated with `id`, if present.
28	fn get( &self, id: &Id ) -> Option<&T>
29	where
30		Id: Hash + Eq ;
31}
32
33/// Exactly one value with ID, guaranteed present.
34#[derive( Debug, Clone )]
35pub struct ExactlyOne<Id, T>(
36	/// The plugin identifier.
37	pub Id,
38	/// The associated value.
39	pub T
40);
41
42/// Zero or one value with ID.
43#[derive( Debug, Clone )]
44pub struct AtMostOne<Id, T>(
45	/// Optional `(id, value)` pair.
46	pub Option<( Id, T )>
47);
48
49/// One or more values keyed by ID.
50#[derive( Debug, Clone )]
51pub struct AtLeastOne<Id, T>(
52	/// Non-empty map keyed by plugin id.
53	pub NEMap<Id, T>
54);
55
56/// Zero or more values keyed by ID.
57#[derive( Debug, Clone )]
58pub struct Any<Id, T>(
59	/// Map keyed by plugin id.
60	pub HashMap<Id, T>
61);
62
63impl<Id, T> Cardinality<Id, T> for ExactlyOne<Id, T> {
64	type Rebind<U> = ExactlyOne<Id, U>;
65
66	fn map<N>( &self, mut map: impl FnMut( &Id, &T ) -> N ) -> Self::Rebind<N>
67	where
68		Id: Clone,
69	{
70		ExactlyOne( self.0.clone(), map( &self.0, &self.1 ))
71	}
72
73	fn map_mut<N>( self, mut map: impl FnMut( T ) -> N ) -> Self::Rebind<N> {
74		ExactlyOne( self.0, map( self.1 ))
75	}
76
77	fn get( &self, id: &Id ) -> Option<&T>
78	where
79		Id: Hash + Eq,
80	{
81		// In a singleton wrapper, mismatched ids indicate a logic bug upstream.
82		// We still return the only value in release builds to avoid masking state.
83		debug_assert!( &self.0 == id, "singleton cardinality id mismatch" );
84		Some( &self.1 )
85	}
86}
87
88impl<Id, T> Cardinality<Id, T> for AtMostOne<Id, T> {
89	type Rebind<U> = AtMostOne<Id, U>;
90
91	fn map<N>( &self, mut map: impl FnMut( &Id, &T ) -> N ) -> Self::Rebind<N>
92	where
93		Id: Clone,
94	{
95		match &self.0 {
96			None => AtMostOne( None ),
97			Some(( id, value )) => AtMostOne( Some(( id.clone(), map( id, value )))),
98		}
99	}
100
101	fn map_mut<N>( self, mut map: impl FnMut( T ) -> N ) -> Self::Rebind<N> {
102		match self.0 {
103			None => AtMostOne( None ),
104			Some(( id, value )) => AtMostOne( Some(( id, map( value )))),
105		}
106	}
107
108	fn get( &self, id: &Id ) -> Option<&T>
109	where
110		Id: Hash + Eq,
111	{
112		match self.0.as_ref() {
113			None => None,
114			Some(( stored_id, value )) => {
115				// In a singleton wrapper, mismatched ids indicate a logic bug upstream.
116				// We still return the only value in release builds to avoid masking state.
117				debug_assert!( stored_id == id, "singleton cardinality id mismatch" );
118				Some( value )
119			}
120		}
121	}
122}
123
124impl<Id: Hash + Eq, T> Cardinality<Id, T> for AtLeastOne<Id, T> {
125	type Rebind<U> = AtLeastOne<Id, U>;
126
127	fn map<N>( &self, mut map: impl FnMut( &Id, &T ) -> N ) -> Self::Rebind<N>
128	where
129		Id: Clone,
130	{
131		AtLeastOne(
132			self.0.nonempty_iter()
133				.map(|( id, value )| ( id.clone(), map( id, value )))
134				.collect()
135		)
136	}
137
138	fn map_mut<N>( self, mut map: impl FnMut( T ) -> N ) -> Self::Rebind<N> {
139		AtLeastOne(
140			self.0.into_nonempty_iter()
141				.map(|( id, value )| ( id, map( value )))
142				.collect()
143		)
144	}
145
146	fn get( &self, id: &Id ) -> Option<&T>
147	where
148		Id: Hash + Eq,
149	{
150		self.0.get( id )
151	}
152}
153
154impl<Id: Hash + Eq, T> Cardinality<Id, T> for Any<Id, T> {
155	type Rebind<U> = Any<Id, U>;
156
157	fn map<N>( &self, mut map: impl FnMut( &Id, &T ) -> N ) -> Self::Rebind<N>
158	where
159		Id: Clone,
160	{
161		Any( self.0.iter().map(|( id, value )| ( id.clone(), map( id, value ))).collect() )
162	}
163
164	fn map_mut<N>( self, mut map: impl FnMut( T ) -> N ) -> Self::Rebind<N> {
165		Any( self.0.into_iter().map(|( id, value )| ( id, map( value ))).collect() )
166	}
167
168	fn get( &self, id: &Id ) -> Option<&T>
169	where
170		Id: Hash + Eq,
171	{
172		self.0.get( id )
173	}
174}
175
176impl<Id: Hash + Eq + Into<Val>> From<ExactlyOne<Id, Val>> for Val {
177	fn from( socket: ExactlyOne<Id, Val> ) -> Self {
178		Val::Tuple( vec![ socket.0.into(), socket.1 ])
179	}
180}
181
182impl<Id: Hash + Eq + Into<Val>> From<AtMostOne<Id, Val>> for Val {
183	fn from( socket: AtMostOne<Id, Val> ) -> Self {
184		match socket.0 {
185			None => Val::Option( None ),
186			Some(( id, val )) => Val::Option( Some( Box::new( Val::Tuple( vec![ id.into(), val ] )))),
187		}
188	}
189}
190
191impl<Id: Hash + Eq + Into<Val>> From<AtLeastOne<Id, Val>> for Val {
192	fn from( socket: AtLeastOne<Id, Val> ) -> Self {
193		Val::List(
194			socket.0.into_iter()
195				.map(|( id, val )| Val::Tuple( vec![ id.into(), val ]))
196				.collect()
197		)
198	}
199}
200
201impl<Id: Hash + Eq + Into<Val>> From<Any<Id, Val>> for Val {
202	fn from( socket: Any<Id, Val> ) -> Self {
203		Val::List(
204			socket.0.into_iter()
205				.map(|( id, val )| Val::Tuple( vec![ id.into(), val ]))
206				.collect()
207		)
208	}
209}
210
211#[cfg(test)]
212mod tests {
213
214	use crate::nem ;
215	use super::* ;
216
217	#[test]
218	fn exactly_one_maps_and_gets() {
219		let value = ExactlyOne( "plugin".to_string(), 10_u32 );
220		let mapped = value.map(| id, v | format!( "{id}:{v}" ));
221		assert_eq!( mapped.0, "plugin" );
222		assert_eq!( mapped.1, "plugin:10" );
223		assert_eq!( mapped.get( &"plugin".to_string() ), Some( &"plugin:10".to_string() ));
224	}
225
226	#[test]
227	fn at_most_one_maps_none_and_some() {
228		let none: AtMostOne<String, u32> = AtMostOne( None );
229		let mapped_none = none.map(| _, v | v + 1 );
230		assert!( mapped_none.0.is_none() );
231
232		let some = AtMostOne( Some(( "plugin".to_string(), 3_u32 )));
233		let mapped_some = some.map(| _, v | v + 1 );
234		assert_eq!( mapped_some.0, Some(( "plugin".to_string(), 4 )));
235	}
236
237	#[test]
238	fn at_least_one_maps_and_gets() {
239		let values = AtLeastOne( nem! { "a".to_string() => 1_u32, "b".to_string() => 2_u32 } );
240		let mapped = values.map(| _, v | v * 2 );
241		assert_eq!( mapped.get( &"a".to_string() ), Some( &2 ));
242		assert_eq!( mapped.get( &"b".to_string() ), Some( &4 ));
243	}
244
245	#[test]
246	fn any_maps_and_gets() {
247		let values = Any( HashMap::from([
248			( "a".to_string(), 1_u32 ),
249			( "b".to_string(), 2_u32 ),
250		]));
251		let mapped = values.map(| _, v | v + 10 );
252		assert_eq!( mapped.get( &"a".to_string() ), Some( &11 ));
253		assert_eq!( mapped.get( &"b".to_string() ), Some( &12 ));
254	}
255
256	#[test]
257	fn exactly_one_into_val() {
258		let val = Val::from( ExactlyOne( "id".to_string(), Val::U32( 7 )));
259		match val {
260			Val::Tuple( items ) => {
261				assert_eq!( items.len(), 2 );
262				assert!( matches!( &items[0], Val::String( s ) if s == "id" ));
263				assert!( matches!( &items[1], Val::U32( 7 )));
264			}
265			other => panic!( "expected tuple, got {other:?}" ),
266		}
267	}
268
269	#[test]
270	fn at_most_one_into_val() {
271		let none = Val::from( AtMostOne::<String, Val>( None ));
272		assert!( matches!( none, Val::Option( None )));
273
274		let some = Val::from( AtMostOne( Some(( "id".to_string(), Val::U32( 1 )))));
275		match some {
276			Val::Option( Some( boxed )) => match *boxed {
277				Val::Tuple( items ) => {
278					assert_eq!( items.len(), 2 );
279					assert!( matches!( &items[0], Val::String( s ) if s == "id" ));
280					assert!( matches!( &items[1], Val::U32( 1 )));
281				}
282				other => panic!( "expected tuple, got {other:?}" ),
283			},
284			other => panic!( "expected option, got {other:?}" ),
285		}
286	}
287
288	#[test]
289	fn at_least_one_into_val() {
290		let val = Val::from( AtLeastOne( nem! { "a".to_string() => Val::U32( 1 ) }));
291		match val {
292			Val::List( items ) => {
293				assert_eq!( items.len(), 1 );
294				assert!( matches!( &items[0],
295					Val::Tuple( tuple )
296						if tuple.len() == 2
297						&& matches!( &tuple[0], Val::String( s ) if s == "a" )
298						&& matches!( &tuple[1], Val::U32( 1 ))
299				));
300			}
301			other => panic!( "expected list, got {other:?}" ),
302		}
303	}
304
305	#[test]
306	fn any_into_val() {
307		let val = Val::from( Any( HashMap::from([
308			( "a".to_string(), Val::U32( 1 )),
309			( "b".to_string(), Val::U32( 2 )),
310		])));
311		match val {
312			Val::List( items ) => {
313				assert_eq!( items.len(), 2 );
314				let mut seen = ( false, false );
315				for item in items {
316					match item {
317						Val::Tuple( tuple ) if tuple.len() == 2 => match (&tuple[0], &tuple[1]) {
318							( Val::String( s ), Val::U32( 1 )) if s == "a" => seen.0 = true,
319							( Val::String( s ), Val::U32( 2 )) if s == "b" => seen.1 = true,
320							_ => {}
321						},
322						_ => {}
323					}
324				}
325				assert!( seen.0 && seen.1 );
326			}
327			other => panic!( "expected list, got {other:?}" ),
328		}
329	}
330}