tenebrous-sheets/src/models/characters.rs

193 lines
6.2 KiB
Rust

use crate::errors::Error;
use crate::models::proto::cofd::*;
use crate::models::users::User;
use prost::bytes::BytesMut;
use serde::Serialize;
use strum::{EnumIter, EnumString};
/// Dynamic character data is an opaque container type that holds
/// successfully deserialized character data protobuf object of any
/// type. It does not know what kind of type it has. This is a
/// semantically more appropriate name for what is returned from the
/// dyn_deserialize function.
pub(crate) type DynCharacterData = dyn erased_serde::Serialize;
/// Control system visibility of a character for a particular user.
/// Implemented as a trait because there are multiple character
/// structs that need this.
pub(crate) trait Visibility {
/// User ID that owns this character.
fn user_id(&self) -> i32;
/// If the character is publicly visible.
fn viewable(&self) -> bool;
/// Transform to an Option that holds the character, if the
/// character is viewable to a potentially existing user. A
/// character is "visible" if the public viewable property is set
/// to true, or the user is the owner of the character. Consumes
/// self.
fn as_visible_for(self, user: Option<&User>) -> Option<Self>
where
Self: std::marker::Sized,
{
if self.viewable() || user.map(|u| u.id) == Some(self.user_id()) {
Some(self)
} else {
None
}
}
}
/// Enum representing all game systems supported by the character
/// service. Game systems are kept unique instead of lumping them
/// together under common umbrella systems, even if the different
/// games use the same (or similar) character sheets. This is because
/// of the possibility for slight differences in rules and data
/// between even similar systems. It's simpler to err on the side of
/// uniqueness. Usually, systems based on the same ruleset will have
/// one character sheet type, with a oneof field for game-specific
/// information.
#[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)]
#[sqlx(rename_all = "snake_case")]
pub enum CharacterDataType {
/// A character for the core Chronicles of Darkness rules.
ChroniclesOfDarkness,
/// A character for Changeling 2E rules.
Changeling,
}
impl CharacterDataType {
/// Create the default serialized protobuf data (character sheet)
/// for the game system represented by the enum variant.
pub fn default_serialized_data(&self) -> Result<BytesMut, Error> {
use prost::Message;
use CharacterDataType::*;
let data: BytesMut = match self {
ChroniclesOfDarkness | Changeling => {
let sheet = CofdSheet::default_sheet(*self);
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
};
Ok(data)
}
/// Returns whether or not this enum variant represents a Chronicles
/// of Darkness game system.
pub fn is_cofd_system(&self) -> bool {
use CharacterDataType::*;
match self {
ChroniclesOfDarkness | Changeling => true,
}
}
}
/// An entry that appears in a user's character list. Properties are
/// in order of table columns.
#[derive(Serialize, Debug, sqlx::FromRow)]
pub struct Character {
pub id: i32,
pub user_id: i32,
pub viewable: bool,
pub character_name: String,
pub data_type: CharacterDataType,
pub data_version: i32,
pub data: Vec<u8>,
}
impl Visibility for Character {
fn user_id(&self) -> i32 {
self.user_id
}
fn viewable(&self) -> bool {
self.viewable
}
}
impl Character {
/// Attempt to deserialize the character's data into the given
/// type, which must be one of the protobuf types.
pub fn try_deserialize<T>(&self) -> Result<T, Error>
where
T: prost::Message + std::default::Default,
{
let decoded = T::decode(self.data.as_ref())?;
Ok(decoded)
}
/// Attempt to deserialize the character's data based on its
/// stored type, but return the deserialized protobuf type as a
/// trait object. Primarily used for passing character sheets to
/// templates or other places (like a REST API).
pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> {
use CharacterDataType::*;
let decoded: Box<dyn erased_serde::Serialize> = match self.data_type {
ChroniclesOfDarkness => Box::new(self.try_deserialize::<CofdSheet>()?),
Changeling => Box::new(self.try_deserialize::<CofdSheet>()?),
};
Ok(decoded)
}
/// Upgrade the stored protobuf character data to its newest
/// iteration, if new types have been created. Consumes self.
pub fn uprade(self) -> Result<Character, Error> {
// Currently, this just returns itself because there are no
// iterations. But we could for example go from CofdSheet v1
// to CofdSheet v2 by deserializing v1, applying a migration
// to v2, then reserializing, and copying over all other
// fields.
Ok(self)
}
/// Update the existing character with new serialized protobuf
/// data.
pub fn update_data<T>(&mut self, data: &T) -> Result<(), Error>
where
T: prost::Message + std::default::Default,
{
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&data));
data.encode(&mut buf)?;
self.data = buf.to_vec();
Ok(())
}
}
/// Same as regular character type, but without the actual protobuf
/// data loaded into memory.
#[derive(Serialize, Debug, sqlx::FromRow)]
pub struct StrippedCharacter {
pub id: i32,
pub user_id: i32,
pub viewable: bool,
pub character_name: String,
pub data_type: CharacterDataType,
pub data_version: i32,
}
impl Visibility for StrippedCharacter {
fn user_id(&self) -> i32 {
self.user_id
}
fn viewable(&self) -> bool {
self.viewable
}
}
/// Represents insert of a new character into the database. Property
/// names correspond to columns.
pub struct NewCharacter<'a> {
pub user_id: i32,
pub viewable: bool,
pub character_name: &'a str,
pub data_type: CharacterDataType,
pub data_version: i32,
pub data: &'a [u8],
}