diff options
Diffstat (limited to 'src/messages.rs')
-rw-r--r-- | src/messages.rs | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 000000000000..2b9f098f98ca --- /dev/null +++ b/src/messages.rs @@ -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" + }) +} |