about summary refs log tree commit diff
path: root/src/messages.rs
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2019-07-07T16·41-0400
committerGriffin Smith <root@gws.fyi>2019-07-07T16·41-0400
commitc643ee1dfcb8d44b8cd198c768f31dd7659f2ff9 (patch)
tree8178c613e55c00944a6417a85ad304cf2af05587 /src/messages.rs
parent78a52142d191d25a74cb2124d5cca8a69d51ba7f (diff)
Add messages, with global lookup map
Add support for messages, along with a global lookup map and random
choice of messages.
Diffstat (limited to 'src/messages.rs')
-rw-r--r--src/messages.rs186
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"
+        })
+}