text.rs
11.5 kB · rust · 355 lines
1use mrlycore::errors::{value_error, Result};23#[derive(Clone, Debug, PartialEq, Eq)]4pub(crate) enum Atom {5 Number(String),6 Bool(bool),7 Word(String),8 List(Vec<Atom>),9}1011impl Atom {12 fn json(&self) -> String {13 match self {14 Atom::Number(digits) => digits.clone(),15 Atom::Bool(flag) => flag.to_string(),16 Atom::Word(word) => format!("\"{word}\""),17 Atom::List(items) => {18 let inner: Vec<String> = items.iter().map(Atom::json).collect();19 format!("[{}]", inner.join(","))20 }21 }22 }23 fn plain(&self) -> String {24 match self {25 Atom::Number(digits) => digits.clone(),26 Atom::Bool(flag) => flag.to_string(),27 Atom::Word(word) => word.clone(),28 Atom::List(items) => items.iter().map(Atom::plain).collect::<Vec<_>>().join(","),29 }30 }31 fn typed(token: &str) -> Result<Atom> {32 match token {33 "true" => Ok(Atom::Bool(true)),34 "false" => Ok(Atom::Bool(false)),35 _ if !token.is_empty() && token.bytes().all(|b| b.is_ascii_digit()) => {36 Ok(Atom::Number(token.to_string()))37 }38 _ if token.as_bytes().first().is_some_and(u8::is_ascii_lowercase)39 && token40 .bytes()41 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') =>42 {43 Ok(Atom::Word(token.to_string()))44 }45 _ => value_error(format!(46 "value {token:?} strays outside a-z, 0-9 and underscore."47 )),48 }49 }50 fn list(text: &str) -> Result<Atom> {51 if text.is_empty() {52 return Ok(Atom::List(Vec::new()));53 }54 let items: Result<Vec<Atom>> = text.split(',').map(Atom::typed).collect();55 Ok(Atom::List(items?))56 }57}5859struct Reader<'a> {60 text: &'a str,61 at: usize,62}6364impl<'a> Reader<'a> {65 fn peek(&self) -> Option<u8> {66 self.text.as_bytes().get(self.at).copied()67 }68 fn take(&mut self, byte: u8) -> Result<()> {69 if self.peek() == Some(byte) {70 self.at += 1;71 return Ok(());72 }73 value_error(format!(74 "json {:?} wants {:?} at {}.",75 self.text, byte as char, self.at76 ))77 }78 fn until(&mut self, stop: impl Fn(u8) -> bool) -> &'a str {79 let start = self.at;80 while self.peek().is_some_and(|b| !stop(b)) {81 self.at += 1;82 }83 &self.text[start..self.at]84 }85 fn word(&mut self) -> Result<String> {86 self.take(b'"')?;87 let word = self.until(|b| b == b'"').to_string();88 self.take(b'"')?;89 Ok(word)90 }91 fn atom(&mut self) -> Result<Atom> {92 match self.peek() {93 Some(b'"') => Ok(Atom::Word(self.word()?)),94 Some(b'[') => {95 self.take(b'[')?;96 let mut items = Vec::new();97 while self.peek() != Some(b']') {98 items.push(self.atom()?);99 if self.peek() == Some(b',') {100 self.at += 1;101 }102 }103 self.take(b']')?;104 Ok(Atom::List(items))105 }106 Some(b) if b.is_ascii_digit() => Ok(Atom::Number(107 self.until(|b| !b.is_ascii_digit()).to_string(),108 )),109 Some(b't') | Some(b'f') => {110 let word = self.until(|b| !b.is_ascii_lowercase());111 Atom::typed(word)112 }113 _ => value_error(format!(114 "json {:?} holds no value at {}.",115 self.text, self.at116 )),117 }118 }119}120121pub(crate) fn pairs(json: &str) -> Result<Vec<(String, Atom)>> {122 let mut reader = Reader { text: json, at: 0 };123 reader.take(b'{')?;124 let mut out = Vec::new();125 while reader.peek() != Some(b'}') {126 let key = reader.word()?;127 reader.take(b':')?;128 out.push((key, reader.atom()?));129 if reader.peek() == Some(b',') {130 reader.at += 1;131 }132 }133 reader.take(b'}')?;134 if reader.at != json.len() {135 return value_error(format!("json {json:?} carries a tail."));136 }137 Ok(out)138}139140fn kinded(json: &str) -> (String, Vec<(String, Atom)>) {141 let mut list = pairs(json).expect("a canonical name reads back");142 let (key, kind) = list.remove(0);143 assert_eq!(key, "kind");144 let Atom::Word(kind) = kind else {145 panic!("the kind is a word");146 };147 (kind, list)148}149150pub(crate) fn json(kind: &str, fields: &[(String, Atom)]) -> String {151 let mut out = format!("{{\"kind\":\"{kind}\"");152 for (key, value) in fields {153 out.push_str(&format!(",\"{key}\":{}", value.json()));154 }155 out.push('}');156 out157}158159fn key(text: &str) -> Result<&str> {160 if !text.is_empty() && text.bytes().all(|b| b.is_ascii_lowercase()) {161 return Ok(text);162 }163 value_error(format!("key {text:?} strays outside a-z."))164}165166pub(crate) fn url(json: &str) -> String {167 let (kind, fields) = kinded(json);168 if fields.is_empty() {169 return format!("/{kind}");170 }171 let query: Vec<String> = fields172 .iter()173 .map(|(key, value)| format!("{key}={}", value.plain()))174 .collect();175 format!("/{kind}?{}", query.join("&"))176}177178pub(crate) fn url_to_json(text: &str, kind: &str, lists: &[&str]) -> Result<String> {179 let Some(rest) = text.strip_prefix('/') else {180 return value_error(format!("url {text:?} does not open with a slash."));181 };182 let (path, query) = rest.split_once('?').unwrap_or((rest, ""));183 if path != kind {184 return value_error(format!("url {text:?} is not a {kind}."));185 }186 let mut fields = Vec::new();187 for pair in query.split('&').filter(|pair| !pair.is_empty()) {188 let Some((name, value)) = pair.split_once('=') else {189 return value_error(format!("url field {pair:?} has no value."));190 };191 let listed = lists.contains(&name)192 && value193 .split(',')194 .all(|token| !matches!(Atom::typed(token), Ok(Atom::Word(_))));195 let atom = if value.contains(',') || listed {196 Atom::list(value)?197 } else {198 Atom::typed(value)?199 };200 fields.push((key(name)?.to_string(), atom));201 }202 Ok(json(kind, &fields))203}204205pub(crate) fn file(json: &str) -> String {206 let (kind, fields) = kinded(json);207 let mut out = kind;208 for (key, value) in &fields {209 let spelt = match value {210 Atom::List(_) => format!("[{}]", value.plain()),211 _ => value.plain(),212 };213 out.push_str(&format!("_{key}={spelt}"));214 }215 out216}217218fn opens_field(text: &str) -> bool {219 let letters = text.bytes().take_while(u8::is_ascii_lowercase).count();220 letters > 0 && text.as_bytes().get(letters) == Some(&b'=')221}222223pub(crate) fn file_to_json(text: &str, kind: &str) -> Result<String> {224 let mut cuts = vec![0];225 for (i, b) in text.bytes().enumerate() {226 if b == b'_' && opens_field(&text[i + 1..]) {227 cuts.push(i);228 }229 }230 cuts.push(text.len());231 let mut parts = cuts.windows(2).map(|w| &text[w[0]..w[1]]);232 let head = parts.next().unwrap_or("");233 if head != kind {234 return value_error(format!("file {text:?} is not a {kind}."));235 }236 let mut fields = Vec::new();237 for part in parts {238 let Some((name, value)) = part[1..].split_once('=') else {239 return value_error(format!("file field {part:?} has no value."));240 };241 let atom = match value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {242 Some(inner) => Atom::list(inner)?,243 None => Atom::typed(value)?,244 };245 fields.push((key(name)?.to_string(), atom));246 }247 Ok(json(kind, &fields))248}249250pub(crate) fn mrly(json: &str, bare: &[&str]) -> String {251 let (kind, fields) = kinded(json);252 let mut parts = Vec::new();253 for (key, value) in &fields {254 parts.push(match value {255 Atom::Bool(true) => key.clone(),256 Atom::Word(word) if bare.contains(&key.as_str()) => word.clone(),257 Atom::List(items) => {258 let inner: Vec<String> = items.iter().map(Atom::plain).collect();259 format!("{key} [{}]", inner.join(" "))260 }261 other => format!("{key} {}", other.plain()),262 });263 }264 if parts.is_empty() {265 return kind;266 }267 format!("{kind} {}", parts.join(", "))268}269270pub(crate) fn longest<'a>(text: &'a str, names: &[String]) -> Option<(usize, &'a str)> {271 let mut best: Option<(usize, &str)> = None;272 for (i, name) in names.iter().enumerate() {273 let Some(rest) = text.strip_prefix(name.as_str()) else {274 continue;275 };276 if best.is_none_or(|(_, tail)| rest.len() < tail.len()) {277 best = Some((i, rest));278 }279 }280 best281}282283#[cfg(test)]284mod tests {285 use super::*;286287 const KOCH: &str =288 r#"{"kind":"bang","dim":2,"lattice":"hex","base":3,"code":39,"twist":[0,1,5,0]}"#;289290 #[test]291 fn the_canonical_text_reads_into_pairs_and_back() {292 let (kind, fields) = kinded(KOCH);293 assert_eq!(kind, "bang");294 assert_eq!(fields.len(), 5);295 assert_eq!(fields[1], ("lattice".into(), Atom::Word("hex".into())));296 assert_eq!(json(&kind, &fields), KOCH);297 assert!(pairs("{\"kind\":\"bang\"} ").is_err());298 assert!(pairs("[1]").is_err());299 }300301 #[test]302 fn the_views_hold_verbatim() {303 assert_eq!(304 url(KOCH),305 "/bang?dim=2&lattice=hex&base=3&code=39&twist=0,1,5,0"306 );307 assert_eq!(308 file(KOCH),309 "bang_dim=2_lattice=hex_base=3_code=39_twist=[0,1,5,0]"310 );311 assert_eq!(312 mrly(KOCH, &["lattice"]),313 "bang dim 2, hex, base 3, code 39, twist [0 1 5 0]"314 );315 assert_eq!(316 mrly(KOCH, &[]),317 "bang dim 2, lattice hex, base 3, code 39, twist [0 1 5 0]"318 );319 }320321 #[test]322 fn the_decodable_views_read_back() {323 assert_eq!(url_to_json(&url(KOCH), "bang", &["twist"]).unwrap(), KOCH);324 assert_eq!(file_to_json(&file(KOCH), "bang").unwrap(), KOCH);325 let rule = r#"{"kind":"rule","birth":[3],"survive":"grid_squares_ones","wrap":true}"#;326 assert_eq!(327 url_to_json(&url(rule), "rule", &["birth", "survive"]).unwrap(),328 rule329 );330 assert_eq!(file_to_json(&file(rule), "rule").unwrap(), rule);331 let empty = r#"{"kind":"rule","birth":[],"survive":[]}"#;332 assert_eq!(333 url_to_json(&url(empty), "rule", &["birth", "survive"]).unwrap(),334 empty335 );336 assert_eq!(file_to_json(&file(empty), "rule").unwrap(), empty);337 assert!(url_to_json("/rule?dim=2", "bang", &[]).is_err());338 assert!(url_to_json("bang?dim=2", "bang", &[]).is_err());339 assert!(url_to_json("/bang?dim", "bang", &[]).is_err());340 assert!(url_to_json("/bang?Dim=2", "bang", &[]).is_err());341 assert!(url_to_json("/bang?dim=2&lattice=Hex", "bang", &[]).is_err());342 assert!(file_to_json("rule_dim=2", "bang").is_err());343 assert!(file_to_json("bang_dim=2_x", "bang").is_err());344 }345346 #[test]347 fn a_bare_kind_prints_alone() {348 let plain = r#"{"kind":"rule"}"#;349 assert_eq!(url(plain), "/rule");350 assert_eq!(file(plain), "rule");351 assert_eq!(mrly(plain, &[]), "rule");352 assert_eq!(url_to_json("/rule", "rule", &[]).unwrap(), plain);353 assert_eq!(file_to_json("rule", "rule").unwrap(), plain);354 }355}