190 lines
7.0 KiB
Rust
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());
|
|
}
|
|
}
|