PronounsToday/src/user_preferences/mod.rs

190 lines
7.0 KiB
Rust

//! Pronoun selection & parsing for various version of user preferences
//!
//! There may be several possible representations of parsed prefstrings, each of which possibly
//! with a different algorithm for selecting pronouns, and a different way of parsing itself. This
//! module houses the implementations for each of these versions.
pub mod v0;
mod error;
use core::str::FromStr;
use core::convert::TryFrom;
pub use error::ParseError;
use crate::user_preferences::v0::UserPreferencesV0;
use crate::{InstanceSettings, Pronoun, WeightedTable};
use data_encoding::BASE32_NOPAD;
use serde::{Serialize, Deserialize};
/// A user's preferences for the probabilities of certain pronouns
///
/// This is the parsed version of a prefstring. The actual implementation details may vary across
/// versions, but universally they must be able to at least produce a weighted list of pronouns,
/// representing the probabilities the user wants for each pronoun set.
///
/// To this end, all versions of the user preferences implement [`Preference`]. For convenience,
/// `UserPreferences` also implements [`Preference`].
///
/// Because parsing a prefstring must be done in relation to an [`InstanceSettings`], the
/// `UserPreferences` struct shares a lifetime with the settings it was created with.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Intermediary", into = "Intermediary")]
pub enum UserPreferences {
V0(v0::UserPreferencesV0)
}
impl Default for UserPreferences {
fn default() -> Self {
UserPreferences::V0(UserPreferencesV0::default())
}
}
/// Functionality provided by any version of user preferences
///
/// See also: [`UserPreferences`]
pub trait Preference {
/// Produce a weighted list of pronouns based on these preferences
///
/// This is a one-directional conversion to a [`WeightedTable`]. This method is a
/// crucial step to randomly selecting a pronoun set based on a user's preferences, as
/// any selection is done by using a [`WeightedTable`]. All preference versions must
/// implement this method.
fn create_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError>;
/// Parse a given prefstring, after it's extraction from base64
///
/// This should attempt to parse the data contained in a prefstring into the appropriate
/// format. Prefstrings will already have been parsed from base64 before being passed to the
/// implementation. Users looking to turn a b64 prefstring into a `Preference` should use
/// [`Preference::from_prefstring()`]
fn from_prefstring_bytes(bytes: &[u8]) -> Result<Self, ParseError> where Self: Sized;
/// Serialize these preferences into as few bytes as possible
///
/// This should produce a series of bytes that, when passed to
/// [`Preference::from_prefstring_bytes()`] should produce this `Preference` object again.
/// This should be done in accordance with the [prefstring specification][1].
///
/// [1]: https://fem.mint.lgbt/Emi/PronounsToday/raw/branch/main/doc/User-Preference-String-Spec.txt
fn as_prefstring_bytes(&self) -> Vec<u8>;
/// Create a Preference instance from a list of pronoun preferences
///
/// This should produce an instance of Preference using the provided preferences.
/// `prefs` is a list of pronoun preferences. Each entry is the weight of the pronoun
/// at that index in the pronoun list in the settings.
fn from_preferences(prefs: &[u8]) -> Self where Self: Sized;
/// Parse a base64 prefstring
///
/// This is the primary method of creating a `Preference` object from a prefstring. The
/// default implementation calls the underlying [`Preference::from_prefstring_bytes()`] method.
fn from_prefstring(prefstring: &str) -> Result<Self, ParseError> where Self: Sized {
BASE32_NOPAD.decode(prefstring.to_uppercase().as_bytes())
.map_err(ParseError::Base32Error)
.and_then(|ps| Self::from_prefstring_bytes(&ps))
}
/// Serialize into a base64 prefstring
///
/// This is the primary method of creating a prefstring from a `Preference` object. The
/// default implementation calls the underlying [`Preference::into_prefstring_bytes()`] method.
fn as_prefstring(&self) -> String {
BASE32_NOPAD.encode(&self.as_prefstring_bytes()).to_lowercase()
}
/// Select a pronoun for today using this Preference's WeightedTable.
///
/// Is shorthand for
///
/// ```
/// # use pronouns_today::user_preferences::ParseError;
/// # use pronouns_today::{InstanceSettings, UserPreferences};
/// # use crate::pronouns_today::user_preferences::Preference;
/// # let settings = InstanceSettings::default();
/// # let seed = &[];
/// # let prefs = UserPreferences::default();
/// prefs.create_weighted_table(&settings)?.select_today(seed);
/// # Ok::<(), ParseError>(())
/// ```
fn select_pronoun<'a>(&self, settings: &'a InstanceSettings, name: Option<&str>) -> Result<&'a Pronoun, ParseError> {
let seed = match name {
Some(name) => name.as_bytes(),
None => &[],
};
Ok(self.create_weighted_table(settings)?.select_today(seed))
}
}
impl Preference for UserPreferences {
fn create_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError> {
match self {
UserPreferences::V0(pref) => pref,
}.create_weighted_table(settings)
}
fn from_prefstring_bytes(bytes: &[u8]) -> Result<Self, ParseError> where Self: Sized {
let version_byte = bytes.get(0).ok_or(ParseError::ZeroLengthPrefstring)?;
let version = version_byte >> 3;
let varient = version_byte & 0b111;
match (version, varient) {
(0, 0) => UserPreferencesV0::from_prefstring_bytes(bytes).map(UserPreferences::V0),
_ => Err(ParseError::VersionMismatch {
expected_version: 0..1,
expected_variant: 0..1,
actual_version_byte: *version_byte,
})
}
}
fn as_prefstring_bytes(&self) -> Vec<u8> {
match self {
UserPreferences::V0(pref) => pref,
}.as_prefstring_bytes()
}
fn from_preferences(prefs: &[u8]) -> Self where Self: Sized {
UserPreferences::V0(UserPreferencesV0::from_preferences(prefs))
}
}
/// Serves as an intermediary for serializing and deserializing [`Preference`] objects
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
struct Intermediary(String);
impl From<UserPreferences> for Intermediary {
fn from(prefs: UserPreferences) -> Intermediary {
Intermediary(prefs.as_prefstring())
}
}
impl TryFrom<Intermediary> for UserPreferences {
type Error = ParseError;
fn try_from(prefs: Intermediary) -> Result<UserPreferences, ParseError> {
UserPreferences::from_prefstring(prefs.0.as_str())
}
}
impl FromStr for UserPreferences {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, ParseError> {
UserPreferences::from_prefstring(s)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prefstring_bytes_round_trip() {
let prefs = UserPreferences::default();
let bytes = prefs.as_prefstring_bytes();
assert_eq!(prefs, UserPreferences::from_prefstring_bytes(&bytes).unwrap());
}
}