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}