use std::borrow::Cow;
use std::iter::Peekable;
use std::str::Chars;
pub(crate) fn normalized_chars(text: &str) -> NormalizedChars<'_> {
NormalizedChars {
chars: text.chars().peekable(),
}
}
pub(crate) fn normalize_text(text: &str) -> Cow<'_, str> {
if text.contains(['\r', '\0']) {
Cow::Owned(normalized_chars(text).collect())
} else {
Cow::Borrowed(text)
}
}
pub(crate) struct NormalizedChars<'a> {
chars: Peekable<Chars<'a>>,
}
impl Iterator for NormalizedChars<'_> {
type Item = char;
fn next(&mut self) -> Option<char> {
match self.chars.next()? {
'\r' => {
if self.chars.peek() == Some(&'\n') {
self.chars.next();
}
Some('\n')
}
'\0' => Some('\u{FFFD}'),
other => Some(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
const VECTORS: &str = include_str!("../tests/fixtures/parity/normalization.json");
fn section(name: &str) -> Vec<Value> {
let all: Value = serde_json::from_str(VECTORS).unwrap();
all[name].as_array().unwrap().clone()
}
#[test]
fn every_shared_value_vector_maps_through_the_iterator_and_the_collector() {
let mut failures = Vec::new();
for case in section("values") {
let (id, input, expected) = (
case["id"].as_str().unwrap(),
case["input"].as_str().unwrap(),
case["expected"].as_str().unwrap(),
);
let streamed: String = normalized_chars(input).collect();
if streamed != expected {
failures.push(format!(
"{id}: iterator gave {streamed:?}, expected {expected:?}"
));
}
let collected = normalize_text(input);
if collected != expected {
failures.push(format!(
"{id}: collector gave {collected:?}, expected {expected:?}"
));
}
let borrowed = matches!(collected, Cow::Borrowed(_));
if borrowed != (input == expected) {
failures.push(format!(
"{id}: borrowed={borrowed} but input {} expected",
if input == expected {
"equals"
} else {
"differs from"
}
));
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn a_crlf_split_across_two_values_is_two_line_feeds() {
let joined: String = normalized_chars("\r")
.chain(normalized_chars("\n"))
.collect();
assert_eq!(joined, "\n\n");
let folded: String = normalized_chars("\r\n").collect();
assert_eq!(folded, "\n");
}
#[test]
fn the_iterator_reports_no_exact_size_and_the_byte_count_is_the_normalized_one() {
let bytes: usize = normalized_chars("\0\r\n").map(char::len_utf8).sum();
assert_eq!(bytes, 4);
assert_eq!("\0\r\n".len(), 3);
}
}