Add messages, with global lookup map
Add support for messages, along with a global lookup map and random choice of messages.
This commit is contained in:
		
							parent
							
								
									78a52142d1
								
							
						
					
					
						commit
						c643ee1dfc
					
				
					 10 changed files with 443 additions and 52 deletions
				
			
		
							
								
								
									
										186
									
								
								src/messages.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/messages.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,186 @@ | |||
| use rand::seq::SliceRandom; | ||||
| use rand::Rng; | ||||
| use serde::de::MapAccess; | ||||
| use serde::de::SeqAccess; | ||||
| use serde::de::Visitor; | ||||
| use std::collections::HashMap; | ||||
| use std::fmt; | ||||
| use std::marker::PhantomData; | ||||
| 
 | ||||
| #[derive(Deserialize, Debug, PartialEq, Eq)] | ||||
| #[serde(untagged)] | ||||
| enum Message<'a> { | ||||
|     Single(&'a str), | ||||
|     Choice(Vec<&'a str>), | ||||
| } | ||||
| 
 | ||||
| impl<'a> Message<'a> { | ||||
|     fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a str> { | ||||
|         use Message::*; | ||||
|         match self { | ||||
|             Single(msg) => Some(*msg), | ||||
|             Choice(msgs) => msgs.choose(rng).map(|msg| *msg), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, PartialEq, Eq)] | ||||
| enum NestedMap<'a> { | ||||
|     Direct(Message<'a>), | ||||
|     Nested(HashMap<&'a str, NestedMap<'a>>), | ||||
| } | ||||
| 
 | ||||
| impl<'a> NestedMap<'a> { | ||||
|     fn lookup(&'a self, path: &str) -> Option<&'a Message<'a>> { | ||||
|         use NestedMap::*; | ||||
|         let leaf = | ||||
|             path.split(".") | ||||
|                 .fold(Some(self), |current, key| match current { | ||||
|                     Some(Nested(m)) => m.get(key), | ||||
|                     _ => None, | ||||
|                 }); | ||||
|         match leaf { | ||||
|             Some(Direct(msg)) => Some(msg), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct NestedMapVisitor<'a> { | ||||
|     marker: PhantomData<fn() -> NestedMap<'a>>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> NestedMapVisitor<'a> { | ||||
|     fn new() -> Self { | ||||
|         NestedMapVisitor { | ||||
|             marker: PhantomData, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'de> Visitor<'de> for NestedMapVisitor<'de> { | ||||
|     type Value = NestedMap<'de>; | ||||
| 
 | ||||
|     fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { | ||||
|         formatter.write_str( | ||||
|             "A message, a list of messages, or a nested map of messages", | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> { | ||||
|         Ok(NestedMap::Direct(Message::Single(v))) | ||||
|     } | ||||
| 
 | ||||
|     fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> | ||||
|     where | ||||
|         A: SeqAccess<'de>, | ||||
|     { | ||||
|         let mut choices = Vec::with_capacity(seq.size_hint().unwrap_or(0)); | ||||
|         while let Some(choice) = seq.next_element()? { | ||||
|             choices.push(choice); | ||||
|         } | ||||
|         Ok(NestedMap::Direct(Message::Choice(choices))) | ||||
|     } | ||||
| 
 | ||||
|     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> | ||||
|     where | ||||
|         A: MapAccess<'de>, | ||||
|     { | ||||
|         let mut nested = HashMap::with_capacity(map.size_hint().unwrap_or(0)); | ||||
|         while let Some((k, v)) = map.next_entry()? { | ||||
|             nested.insert(k, v); | ||||
|         } | ||||
|         Ok(NestedMap::Nested(nested)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'de> serde::Deserialize<'de> for NestedMap<'de> { | ||||
|     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||
|     where | ||||
|         D: serde::Deserializer<'de>, | ||||
|     { | ||||
|         deserializer.deserialize_any(NestedMapVisitor::new()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_deserialize_nested_map() { | ||||
|         let src = r#" | ||||
| [global] | ||||
| hello = "Hello World!" | ||||
| 
 | ||||
| [foo.bar] | ||||
| single = "Single" | ||||
| choice = ["Say this", "Or this"] | ||||
| "#;
 | ||||
|         let result = toml::from_str(src); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             Ok(NestedMap::Nested(hashmap! { | ||||
|                 "global" => NestedMap::Nested(hashmap!{ | ||||
|                     "hello" => NestedMap::Direct(Message::Single("Hello World!")), | ||||
|                 }), | ||||
|                 "foo" => NestedMap::Nested(hashmap!{ | ||||
|                     "bar" => NestedMap::Nested(hashmap!{ | ||||
|                         "single" => NestedMap::Direct(Message::Single("Single")), | ||||
|                         "choice" => NestedMap::Direct(Message::Choice( | ||||
|                             vec!["Say this", "Or this"] | ||||
|                         )) | ||||
|                     }) | ||||
|                 }) | ||||
|             })) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_lookup() { | ||||
|         let map: NestedMap<'static> = toml::from_str( | ||||
|             r#" | ||||
| [global] | ||||
| hello = "Hello World!" | ||||
| 
 | ||||
| [foo.bar] | ||||
| single = "Single" | ||||
| choice = ["Say this", "Or this"] | ||||
| "#,
 | ||||
|         ) | ||||
|         .unwrap(); | ||||
| 
 | ||||
|         assert_eq!( | ||||
|             map.lookup("global.hello"), | ||||
|             Some(&Message::Single("Hello World!")) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             map.lookup("foo.bar.single"), | ||||
|             Some(&Message::Single("Single")) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             map.lookup("foo.bar.choice"), | ||||
|             Some(&Message::Choice(vec!["Say this", "Or this"])) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static MESSAGES_RAW: &'static str = include_str!("messages.toml"); | ||||
| 
 | ||||
| lazy_static! { | ||||
|     static ref MESSAGES: NestedMap<'static> = | ||||
|         toml::from_str(MESSAGES_RAW).unwrap(); | ||||
| } | ||||
| 
 | ||||
| /// Look up a game message based on the given (dot-separated) name, with the
 | ||||
| /// given random generator used to select from choice-based messages
 | ||||
| pub fn message<R: Rng + ?Sized>(name: &str, rng: &mut R) -> &'static str { | ||||
|     use Message::*; | ||||
|     MESSAGES | ||||
|         .lookup(name) | ||||
|         .and_then(|msg| msg.resolve(rng)) | ||||
|         .unwrap_or_else(|| { | ||||
|             error!("Message not found: {}", name); | ||||
|             "Message not found" | ||||
|         }) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue