PronounsToday/src/user_preferences/v0.rs

364 lines
8.6 KiB
Rust

//! Version 0 Prefstrings
use crate::{
InstanceSettings,
Pronoun,
user_preferences::{Preference, ParseError},
WeightedTable,
};
use std::{
collections::HashMap,
iter::{Extend, self},
};
/// A parsed version of the V0 prefstring
///
/// See the [prefstring specification][1] for more information about how this is interpretted.
///
/// [1]: https://fem.mint.lgbt/Emi/PronounsToday/raw/branch/main/doc/User-Preference-String-Spec.txt
pub struct UserPreferencesV0 {
pub default_weight: u8,
pub default_enabled: bool,
pub commands: Vec<Command>,
}
impl Preference for UserPreferencesV0 {
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError> {
let mut remaining_pronouns = settings.pronoun_list.iter();
let mut weighted_pronouns = HashMap::with_capacity(settings.pronoun_list.len());
for command in &self.commands {
match command {
Command::SetWeight(weight) => {
if *weight > 0 {
weighted_pronouns.insert(
remaining_pronouns.next()
.ok_or(ParseError::PrefstringExceedsPronounCount)?,
*weight
);
}
},
Command::Move { toggle_enabled, distance } => {
let skipped_pronouns = iter::repeat_with(|| remaining_pronouns.next())
.take(*distance as usize)
.collect::<Option<Vec<_>>>()
.ok_or(ParseError::PrefstringExceedsPronounCount)?;
if self.default_enabled {
weighted_pronouns.extend(
skipped_pronouns
.into_iter()
.map(|pronoun| (pronoun, self.default_weight))
)
}
let last_enabled = *toggle_enabled != self.default_enabled;
let last_pronoun = remaining_pronouns.next()
.ok_or(ParseError::PrefstringExceedsPronounCount)?;
if last_enabled {
weighted_pronouns.insert(last_pronoun, self.default_weight);
}
}
}
}
if self.default_enabled {
weighted_pronouns.extend(
remaining_pronouns
.into_iter()
.map(|pronoun| (pronoun, self.default_weight))
);
}
WeightedTable::new(weighted_pronouns)
}
/// ```
/// # use pronouns_today::user_preferences::{Preference, v0::{Command, UserPreferencesV0}};
/// let pbytes = &[ 0x00, 0x81, 0x08, 0xcf, 0x80 ][..];
/// let prefs = UserPreferencesV0::from_prefstring_bytes(&pbytes).unwrap();
/// assert_eq!(prefs.default_weight, 1);
/// assert!(prefs.default_enabled);
/// assert_eq!(prefs.commands, vec![
/// Command::SetWeight(8),
/// Command::Move { toggle_enabled: true, distance: 15 },
/// Command::Move { toggle_enabled: false, distance: 0 },
/// ]);
/// ```
fn from_prefstring_bytes(pbytes: &[u8]) -> Result<Self, ParseError> {
// Some simple error checks
if pbytes.len() == 0 {
return Err(ParseError::ZeroLengthPrefstring);
} else if pbytes[0] != 00 {
return Err(ParseError::VersionMismatch {
expected_version: 0..1,
expected_variant: 0..1,
actual_version_byte: pbytes[0],
})
} else if pbytes.len() == 1 {
return Err(
ParseError::MalformedContent(
"Version 0 prefstrings must be at least two bytes long, but this one
was just one byte long".into()
)
)
}
// Extract the defaults from byte 1
Ok(UserPreferencesV0 {
default_enabled: pbytes[1] & 0b10000000 > 0,
default_weight: pbytes[1] & 0b01111111,
commands: pbytes[2..].iter().map(|b| Command::from(*b)).collect(),
})
}
fn into_prefstring_bytes(&self) -> Vec<u8> {
todo!()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Command {
SetWeight(u8),
Move {
toggle_enabled: bool,
distance: u8,
}
}
impl From<u8> for Command {
/// ```
/// use pronouns_today::user_preferences::v0::Command;
/// assert_eq!(
/// Command::from(0b01001000),
/// Command::SetWeight(0b01001000)
/// );
/// assert_eq!(
/// Command::from(0b11000000),
/// Command::Move {
/// toggle_enabled: true,
/// distance: 0b00000000
/// }
/// );
/// assert_eq!(
/// Command::from(0b10101000),
/// Command::Move {
/// toggle_enabled: false,
/// distance: 0b00101000
/// }
/// );
/// ```
fn from(b: u8) -> Self {
if b & 0b10000000 > 0 {
Command::Move {
toggle_enabled: b & 0b01000000 > 0,
distance: b & 0b00111111,
}
} else {
Command::SetWeight(b)
}
}
}
#[cfg(test)]
mod tests {
use crate::{InstanceSettings, Pronoun, WeightedTable};
use crate::user_preferences::{Preference, ParseError};
use crate::user_preferences::v0::{UserPreferencesV0, Command};
use std::collections::HashMap;
fn testing_instance_settings() -> InstanceSettings {
InstanceSettings {
pronoun_list: vec![
["she", "her", "her", "hers", "herself" ].into(),
["he", "him", "his", "his", "himself" ].into(),
["they", "them", "their", "theirs", "themself" ].into(),
["it", "it", "its", "its", "itself" ].into(),
["xe", "xem", "xyr", "xyrs", "xemself" ].into(),
],
}
}
fn check_table(actual: WeightedTable<&Pronoun>, expected: Vec<(&str, u8)>) {
let actual_simplified = actual.weights()
.into_iter()
.filter(|(pronoun, weight)| **weight > 0)
.map(|(pronoun, weight)| (pronoun.subject_pronoun.as_str(), *weight))
.collect::<HashMap<&str, u8>>();
let expected_owned = expected.into_iter()
.map(|(pronoun, weight)| (pronoun, weight))
.collect::<HashMap<&str, u8>>();
assert_eq!(actual_simplified, expected_owned);
}
#[test]
fn test_into_weighted_table_no_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 2,
commands: Vec::new(),
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 2),
("he", 2),
("they", 2),
("it", 2),
("xe", 2),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_move_commands_select_deactivation() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 3,
commands: vec![
Command::Move {
toggle_enabled: false,
distance: 1
},
Command::Move {
toggle_enabled: true,
distance: 2
},
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 3),
("he", 3),
("they", 3),
("it", 3),
];
}
#[test]
fn test_into_weighted_table_weight_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::SetWeight(5),
Command::SetWeight(4),
Command::SetWeight(3),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 5),
("he", 4),
("they", 3),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_combined_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 9,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 1
},
Command::SetWeight(5),
Command::Move {
toggle_enabled: false,
distance: 0
},
Command::SetWeight(4),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("he", 9),
("they", 5),
("xe", 4),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_move_past_boundry() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 5
},
],
};
let err = p.into_weighted_table(&s).unwrap_err();
assert_eq!(err, ParseError::PrefstringExceedsPronounCount);
}
#[test]
fn test_into_weighted_table_set_weight_past_boundry() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 4
},
Command::SetWeight(9),
],
};
let err = p.into_weighted_table(&s).unwrap_err();
assert_eq!(err, ParseError::PrefstringExceedsPronounCount);
}
#[test]
fn test_into_weighted_table_zero_default_weight() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 0,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 2
},
Command::SetWeight(3),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("it", 3),
];
check_table(table, expected_table);
}
}