mod.rs

6.7 kB · rust · 175 lines

1use mrlycore::errors::Result;2use serde::de::DeserializeOwned;3use serde::Serialize;45mod sha;6pub(crate) mod text;78/// The bang name: a design code pinned to its dimension, lattice and base.9pub mod bang;10/// The rule name: a life rule's birth and survival counts and whether the edge wraps.11pub mod rule;12/// The sequence name: a design's reading pinned to its measure and axis.13pub mod sequence;14/// The tile name: a full tile recipe folded to its one canonical object.15pub mod tile;16/// The word name: an ordered list of design letters, each at its own side.17pub mod word;1819/// One canonical JSON object per mathematical thing, and the views cut from it.20///21/// The object holds `kind` first, then the glossary words as keys in a fixed order per kind, with22/// defaults elided and no whitespace, so equality of things is equality of strings. Every other23/// form is a function of that string: `to_url` puts the keys in a query string, `to_file` in a24/// filename, `to_mrly` in a line of prose, and `to_id` hashes it. The law is25/// `from_json(to_json(x)) == checked(x)` for every value `x`.26pub trait Named: Serialize + DeserializeOwned + Sized {27    /// The kind word, the first value of the object.28    const KIND: &'static str;29    /// The keys whose values are lists even when one item long, so the query string reads them back.30    const LISTS: &'static [&'static str] = &[];31    /// The keys whose word value prints alone in the prose form.32    const BARE: &'static [&'static str] = &[];33    /// Folds a decoded value to its canonical form, or an error for one outside the kind.34    fn checked(self) -> Result<Self>;35    /// Prints the canonical JSON object.36    fn to_json(&self) -> String {37        serde_json::to_string(self).expect("a name serializes")38    }39    /// Reads a JSON object into its canonical value, or an error naming the broken key.40    fn from_json(text: &str) -> Result<Self> {41        let value: Self = serde_json::from_str(text)?;42        value.checked()43    }44    /// Prints the first eight hex digits of the sha256 of the canonical JSON.45    fn to_id(&self) -> String {46        sha::short(self.to_json().as_bytes())47    }48    /// Prints the kind as a path and the keys as a query string, lists comma-joined.49    fn to_url(&self) -> String {50        text::url(&self.to_json())51    }52    /// Reads a path and query string back into the value, or an error.53    fn from_url(text: &str) -> Result<Self> {54        Self::from_json(&text::url_to_json(text, Self::KIND, Self::LISTS)?)55    }56    /// Prints the kind and the `key=value` pairs joined by underscores, lists in brackets.57    fn to_file(&self) -> String {58        text::file(&self.to_json())59    }60    /// Reads a filename back into the value, or an error.61    fn from_file(text: &str) -> Result<Self> {62        Self::from_json(&text::file_to_json(text, Self::KIND)?)63    }64    /// Prints the kind and the keys as a line of prose for pages.65    fn to_mrly(&self) -> String {66        text::mrly(&self.to_json(), Self::BARE)67    }68}6970macro_rules! kind {71    ($word:literal) => {72        #[doc = concat!("The kind marker that spells `", $word, "`.")]73        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]74        pub struct Kind;7576        impl serde::Serialize for Kind {77            fn serialize<S: serde::Serializer>(78                &self,79                serializer: S,80            ) -> std::result::Result<S::Ok, S::Error> {81                serializer.serialize_str($word)82            }83        }8485        impl<'de> serde::Deserialize<'de> for Kind {86            fn deserialize<D: serde::Deserializer<'de>>(87                deserializer: D,88            ) -> std::result::Result<Kind, D::Error> {89                let word = <String as serde::Deserialize>::deserialize(deserializer)?;90                if word == $word {91                    Ok(Kind)92                } else {93                    Err(serde::de::Error::custom(format!(94                        "kind {word:?} is not {:?}",95                        $word96                    )))97                }98            }99        }100    };101}102103pub(crate) use kind;104105pub use bang::{Bang, Lattice};106pub use rule::Rule;107pub use sequence::Sequence;108pub use tile::{classic_code, Tile};109pub use word::Word;110111#[cfg(test)]112mod tests {113    use super::*;114    use crate::life::Counts;115116    #[test]117    fn every_kind_has_one_canonical_string_and_one_id() {118        let bang = Bang::new(7, 2, 2);119        let rule = Rule::new(vec![3], vec![2, 3], false);120        let word = Word::new(2, &[(7, 3), (14, 7)]).unwrap();121        let tile = Tile::from_json(r#"{"kind":"tile","code":7,"side":3,"level":2}"#).unwrap();122        let sequence = Sequence::new(7, 2, 2, "fills", "side");123        let strings = [124            bang.to_json(),125            rule.to_json(),126            word.to_json(),127            tile.to_json(),128            sequence.to_json(),129        ];130        let ids = [131            bang.to_id(),132            rule.to_id(),133            word.to_id(),134            tile.to_id(),135            sequence.to_id(),136        ];137        for (text, id) in strings.iter().zip(&ids) {138            assert!(text.starts_with("{\"kind\":\""));139            assert!(!text.contains(' '));140            assert_eq!(id.len(), 8);141            assert!(id.chars().all(|c| c.is_ascii_hexdigit()));142        }143        assert_eq!(Bang::from_json(&strings[0]).unwrap(), bang);144        assert_eq!(Rule::from_json(&strings[1]).unwrap(), rule);145        assert_eq!(Word::from_json(&strings[2]).unwrap(), word);146        assert_eq!(Tile::from_json(&strings[3]).unwrap(), tile);147        assert_eq!(Sequence::from_json(&strings[4]).unwrap(), sequence);148        assert!(Rule::from_json(&strings[0]).is_err());149        assert_eq!(rule.birth, Counts::List(vec![3]));150    }151152    #[test]153    fn every_example_on_names_md_round_trips() {154        let doc = include_str!("../../NAMES.md");155        let mut seen = 0;156        for piece in doc.split('`') {157            let Some(rest) = piece.strip_prefix("{\"kind\":\"") else {158                continue;159            };160            let kind = rest.split('"').next().unwrap();161            let printed = match kind {162                "bang" => Bang::from_json(piece).map(|v| v.to_json()),163                "rule" => Rule::from_json(piece).map(|v| v.to_json()),164                "sequence" => Sequence::from_json(piece).map(|v| v.to_json()),165                "tile" => Tile::from_json(piece).map(|v| v.to_json()),166                "word" => Word::from_json(piece).map(|v| v.to_json()),167                other => panic!("NAMES.md names no kind {other:?}"),168            };169            let printed = printed.unwrap_or_else(|e| panic!("{piece} does not read: {e}"));170            assert_eq!(printed, piece, "{piece} is not canonical");171            seen += 1;172        }173        assert!(seen >= 13, "NAMES.md kept only {seen} examples");174    }175}