use std::fmt;
use serde::{
de::{self, MapAccess, SeqAccess, Visitor},
Deserialize, Deserializer, Serializer,
};
use crate::{Channel, ValueAndLang};
pub fn bool_to_new_tag<S>(x: &bool, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if *x {
s.serialize_some("")
} else {
s.serialize_none()
}
}
pub fn new_tag_to_boolean<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let _: () = de::Deserialize::deserialize(deserializer)?;
Ok(true)
}
pub fn bool_to_yes_no<S>(x: &bool, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if *x {
s.serialize_str("yes")
} else {
s.serialize_str("no")
}
}
pub fn yes_no_to_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let value: Option<&str> = de::Deserialize::deserialize(deserializer)?;
if let Some(v) = value {
return Ok(v.to_lowercase() == "yes");
}
Ok(false)
}
impl<'de> Deserialize<'de> for ValueAndLang {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Debug)]
enum Field {
Lang,
Text,
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("`$text` or `@lang`")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: de::Error,
{
match value {
"@lang" => Ok(Field::Lang),
"$text" => Ok(Field::Text),
_ => Err(de::Error::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct ValueAndLangVisitor;
impl<'de> Visitor<'de> for ValueAndLangVisitor {
type Value = ValueAndLang;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct ValueAndLang")
}
fn visit_seq<V>(self, mut seq: V) -> Result<ValueAndLang, V::Error>
where
V: SeqAccess<'de>,
{
let text = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let lang = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
Ok(ValueAndLang { value: text, lang })
}
fn visit_map<V>(self, mut map: V) -> Result<ValueAndLang, V::Error>
where
V: MapAccess<'de>,
{
let mut text = None;
let mut lang = None;
while let Some(key) = map.next_key()? {
match key {
Field::Text => {
if text.is_some() {
return Err(de::Error::duplicate_field("text"));
}
text = Some(map.next_value()?);
}
Field::Lang => {
if lang.is_some() {
return Err(de::Error::duplicate_field("lang"));
}
lang = Some(map.next_value()?);
}
}
}
let text = text.unwrap_or(String::new());
let lang = lang.unwrap_or(None);
Ok(ValueAndLang { value: text, lang })
}
}
const FIELDS: &[&str] = &["$text", "@lang"];
deserializer.deserialize_struct("ValueAndLang", FIELDS, ValueAndLangVisitor)
}
}
#[allow(dead_code)]
trait Trimable {
fn trimmed(self) -> String;
}
impl Trimable for String {
fn trimmed(self) -> String {
let value: String = self.trim_start_matches(char::is_whitespace).to_owned();
let data = value.lines();
let n = data.count();
if n == 0 {
return String::new();
} else if n == 1 {
return value.trim().to_owned();
}
let mut result = String::with_capacity(value.len());
for (i, line) in value.lines().enumerate() {
let trimmed = line.trim_matches(char::is_whitespace);
result.push_str(trimmed);
let ends_with_punctuation = trimmed.ends_with(['.', '!', '?']);
if !ends_with_punctuation {
result.push(' ');
}
if i < n - 1 && ends_with_punctuation {
result.push('\n');
}
}
let new_len = result
.char_indices()
.rev()
.find(|(_, c)| !matches!(c, '\n' | '\r' | ' ' | '\t'))
.map_or(0, |(i, _)| i + 1);
if new_len != self.len() {
result.truncate(new_len);
}
result
}
}
pub fn channels_to_map(channels: &[Channel]) -> std::collections::HashMap<&str, Channel> {
let mut map: std::collections::HashMap<&str, Channel> =
std::collections::HashMap::with_capacity(channels.len());
for c in channels {
map.insert(c.id.as_str(), c.clone());
}
map
}
#[cfg(test)]
mod tests {
use crate::utils::Trimable;
#[test]
fn test_trim_strings() {
assert_eq!(String::new().trimmed(), "".to_owned());
assert_eq!(String::from("\n\n").trimmed(), "".to_owned());
assert_eq!(String::from("a\n \t\n").trimmed(), "a".to_owned());
assert_eq!(String::from(" \t a\t ").trimmed(), "a".to_owned());
assert_eq!(String::from("a\na\n\n").trimmed(), "a a".to_owned());
assert_eq!(String::from("\n\na\na\n\n").trimmed(), "a a".to_owned());
assert_eq!(String::from("\n\na.\na\n\n").trimmed(), "a.\na".to_owned());
assert_eq!(String::from("a.\n").trimmed(), "a.".to_owned());
assert_eq!(String::from("Actresses Kim Raver, Brooke Shields and Lindsay Price (\"Lipstick Jungle\");
women in their 40s tell why they got breast implants; a 30-minute meal.").trimmed(), "Actresses Kim Raver, Brooke Shields and Lindsay Price (\"Lipstick Jungle\"); women in their 40s tell why they got breast implants; a 30-minute meal.".to_owned());
assert_eq!(String::from(" Bilko claims he's had a close encounter with an alien in order
to be given some compassionate leave so he can visit an old
flame in New York.").trimmed(), "Bilko claims he's had a close encounter with an alien in order to be given some compassionate leave so he can visit an old flame in New York.".to_owned());
}
}