hyaenidae/profiles/src/store/mod.rs
2021-01-06 10:45:23 -06:00

593 lines
13 KiB
Rust

use chrono::{DateTime, Utc};
use sled::Db;
use std::fmt;
use uuid::Uuid;
mod comment;
mod file;
mod profile;
mod react;
mod submission;
pub mod view;
#[derive(Clone)]
pub struct Store {
pub profiles: profile::Store,
pub files: file::Store,
pub submissions: submission::Store,
pub comments: comment::Store,
pub reacts: react::Store,
pub view: view::Store,
}
impl Store {
pub fn build(db: &Db) -> Result<Store, sled::Error> {
Ok(Store {
profiles: profile::Store::build(db)?,
files: file::Store::build(db)?,
submissions: submission::Store::build(db)?,
comments: comment::Store::build(db)?,
reacts: react::Store::build(db)?,
view: view::Store::build(db)?,
})
}
}
#[derive(Clone, Debug)]
pub enum OwnerSource {
Local(Uuid),
Remote(String),
}
impl OwnerSource {
pub fn is_local(&self) -> bool {
matches!(self, OwnerSource::Local(_))
}
}
#[derive(Clone, Debug)]
pub struct Profile {
id: Uuid,
owner_source: OwnerSource,
handle: String,
domain: String,
display_name: Option<String>,
description: Option<String>,
icon: Option<Uuid>,
banner: Option<Uuid>,
published: DateTime<Utc>,
login_required: bool,
}
impl Profile {
pub fn update(&self) -> ProfileChanges {
ProfileChanges {
id: self.id,
display_name: None,
description: None,
login_required: None,
}
}
pub fn update_images(&self) -> ProfileImageChanges {
ProfileImageChanges {
id: self.id,
icon: None,
banner: None,
}
}
pub(crate) fn id(&self) -> Uuid {
self.id
}
pub(crate) fn owner_source(&self) -> &OwnerSource {
&self.owner_source
}
pub fn handle(&self) -> &str {
&self.handle
}
pub fn domain(&self) -> &str {
&self.domain
}
pub fn display_name(&self) -> Option<&str> {
self.display_name.as_ref().map(|dn| dn.as_str())
}
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(|d| d.as_str())
}
pub fn icon(&self) -> Option<Uuid> {
self.icon
}
pub fn banner(&self) -> Option<Uuid> {
self.banner
}
pub fn published(&self) -> DateTime<Utc> {
self.published
}
pub fn login_required(&self) -> bool {
self.login_required
}
}
#[derive(Clone, Debug)]
pub struct PictRsFile {
key: String,
token: String,
width: usize,
height: usize,
media_type: mime::Mime,
}
impl PictRsFile {
pub(crate) fn new(
key: &str,
token: &str,
width: usize,
height: usize,
media_type: mime::Mime,
) -> Self {
PictRsFile {
key: key.to_owned(),
token: token.to_owned(),
width,
height,
media_type,
}
}
pub fn key(&self) -> &str {
&self.key
}
pub(crate) fn token(&self) -> &str {
&self.token
}
}
#[derive(Clone, Debug)]
pub enum FileSource {
PictRs(PictRsFile),
}
#[derive(Clone, Debug)]
pub struct File {
id: Uuid,
source: FileSource,
}
impl File {
pub(crate) fn id(&self) -> Uuid {
self.id
}
pub fn source(&self) -> &FileSource {
&self.source
}
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
pub enum Visibility {
Public,
Unlisted,
Followers,
}
#[derive(Clone, Debug)]
pub struct Submission {
id: Uuid,
profile_id: Uuid,
title: String,
description: Option<String>,
files: Vec<Uuid>,
published: Option<DateTime<Utc>>,
visibility: Visibility,
}
impl Submission {
pub fn update(&self) -> SubmissionChanges {
SubmissionChanges {
id: self.id,
title: None,
description: None,
published: self.published,
}
}
pub fn update_files(&self) -> SubmissionFileChanges {
SubmissionFileChanges {
id: self.id,
files: self.files.clone(),
}
}
pub(crate) fn id(&self) -> Uuid {
self.id
}
pub(crate) fn profile_id(&self) -> Uuid {
self.profile_id
}
pub(crate) fn title(&self) -> &str {
&self.title
}
pub(crate) fn description(&self) -> Option<&str> {
self.description.as_ref().map(|d| d.as_str())
}
pub(crate) fn files(&self) -> &[Uuid] {
&self.files
}
pub(crate) fn published(&self) -> Option<DateTime<Utc>> {
self.published
}
pub(crate) fn visibility(&self) -> Visibility {
self.visibility
}
}
#[derive(Clone, Debug)]
pub enum Comment {
Submission(SubmissionComment),
Reply(ReplyComment),
}
impl Comment {
pub(crate) fn id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { id, .. }) => *id,
Comment::Submission(SubmissionComment { id, .. }) => *id,
}
}
pub(crate) fn submission_id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { submission_id, .. }) => *submission_id,
Comment::Submission(SubmissionComment { submission_id, .. }) => *submission_id,
}
}
pub(crate) fn profile_id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { profile_id, .. }) => *profile_id,
Comment::Submission(SubmissionComment { profile_id, .. }) => *profile_id,
}
}
pub(crate) fn comment_id(&self) -> Option<Uuid> {
match self {
Comment::Reply(ReplyComment { comment_id, .. }) => Some(*comment_id),
_ => None,
}
}
pub(crate) fn body(&self) -> &str {
match self {
Comment::Reply(ReplyComment { body, .. }) => &body,
Comment::Submission(SubmissionComment { body, .. }) => &body,
}
}
pub(crate) fn published(&self) -> DateTime<Utc> {
match self {
Comment::Reply(ReplyComment { published, .. }) => *published,
Comment::Submission(SubmissionComment { published, .. }) => *published,
}
}
pub(crate) fn update(&self) -> CommentChanges {
match self {
Comment::Reply(rc) => rc.update(),
Comment::Submission(sc) => sc.update(),
}
}
}
#[derive(Clone, Debug)]
pub struct SubmissionComment {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
body: String,
published: DateTime<Utc>,
}
impl SubmissionComment {
fn update(&self) -> CommentChanges {
CommentChanges {
id: self.id,
body: None,
}
}
}
#[derive(Clone, Debug)]
pub struct ReplyComment {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Uuid,
body: String,
published: DateTime<Utc>,
}
impl ReplyComment {
fn update(&self) -> CommentChanges {
CommentChanges {
id: self.id,
body: None,
}
}
}
#[derive(Clone, Debug)]
pub enum React {
Submission(SubmissionReact),
Reply(ReplyReact),
}
impl React {
pub(crate) fn id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { id, .. }) => *id,
React::Reply(ReplyReact { id, .. }) => *id,
}
}
pub(crate) fn submission_id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { submission_id, .. }) => *submission_id,
React::Reply(ReplyReact { submission_id, .. }) => *submission_id,
}
}
pub(crate) fn profile_id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { profile_id, .. }) => *profile_id,
React::Reply(ReplyReact { profile_id, .. }) => *profile_id,
}
}
pub(crate) fn comment_id(&self) -> Option<Uuid> {
match self {
React::Reply(ReplyReact { comment_id, .. }) => Some(*comment_id),
_ => None,
}
}
pub(crate) fn react(&self) -> &str {
match self {
React::Submission(SubmissionReact { react, .. }) => &react,
React::Reply(ReplyReact { react, .. }) => &react,
}
}
pub(crate) fn published(&self) -> DateTime<Utc> {
match self {
React::Submission(SubmissionReact { published, .. }) => *published,
React::Reply(ReplyReact { published, .. }) => *published,
}
}
}
#[derive(Clone, Debug)]
pub struct SubmissionReact {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
react: String,
published: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct ReplyReact {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Uuid,
react: String,
published: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct Undo<T>(pub T);
#[derive(Debug)]
pub struct ProfileChanges {
id: Uuid,
display_name: Option<String>,
description: Option<String>,
login_required: Option<bool>,
}
impl ProfileChanges {
pub fn display_name(&mut self, display_name: &str) -> &mut Self {
self.display_name = Some(display_name.to_owned());
self
}
pub fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_owned());
self
}
pub fn login_required(&mut self, required: bool) -> &mut Self {
self.login_required = Some(required);
self
}
}
#[derive(Debug)]
pub struct ProfileImageChanges {
id: Uuid,
icon: Option<Uuid>,
banner: Option<Uuid>,
}
impl ProfileImageChanges {
pub fn icon(&mut self, file: &File) -> &mut Self {
self.icon = Some(file.id);
self
}
pub fn banner(&mut self, file: &File) -> &mut Self {
self.banner = Some(file.id);
self
}
}
#[derive(Debug)]
pub struct SubmissionChanges {
id: Uuid,
title: Option<String>,
description: Option<String>,
published: Option<DateTime<Utc>>,
}
impl SubmissionChanges {
pub fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(title.to_owned());
self
}
pub fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_owned());
self
}
pub fn publish(&mut self, time: Option<DateTime<Utc>>) -> &mut Self {
self.published = time.or_else(|| Some(Utc::now()));
self
}
}
#[derive(Debug)]
pub struct SubmissionFileChanges {
id: Uuid,
files: Vec<Uuid>,
}
impl SubmissionFileChanges {
pub fn add_file(&mut self, file: &File) -> &mut Self {
self.files.push(file.id);
self
}
pub fn delete_file(&mut self, file: &File) -> &mut Self {
self.files.retain(|id| *id != file.id);
self
}
}
#[derive(Debug)]
pub struct CommentChanges {
id: Uuid,
body: Option<String>,
}
impl CommentChanges {
pub fn body(&mut self, body: &str) -> &mut CommentChanges {
self.body = Some(body.to_owned());
self
}
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("{0}")]
Json(#[from] serde_json::Error),
#[error("{0}")]
Sled(#[from] sled::Error),
#[error("{0}")]
Transaction(#[from] sled::transaction::TransactionError),
#[error("Profile changed during modification")]
DoubleStore,
#[error("Cannot update missing item")]
Missing,
}
fn modify<T>(
tree: &sled::transaction::TransactionalTree,
key: &str,
f: impl Fn(&mut T),
) -> Result<(), sled::transaction::ConflictableTransactionError>
where
T: serde::Serialize + Default,
for<'de> T: serde::Deserialize<'de>,
{
let mut item = match tree.get(key.as_bytes())? {
Some(ivec) => {
let item: T = serde_json::from_slice(&ivec).expect("JSON is valid");
item
}
None => T::default(),
};
(f)(&mut item);
let item_vec = serde_json::to_vec(&item).expect("JSON is valid");
tree.insert(key.as_bytes(), item_vec.as_slice())?;
Ok(())
}
fn count(
tree: &sled::transaction::TransactionalTree,
key: &str,
f: impl Fn(u64) -> u64,
) -> Result<(), sled::transaction::ConflictableTransactionError> {
let count = match tree.get(key.as_bytes())? {
Some(ivec) => {
let s = String::from_utf8_lossy(&ivec);
let count: u64 = s.parse().expect("Count is valid");
count
}
None => 0,
};
let count = (f)(count).to_string();
tree.insert(key.as_bytes(), count.as_bytes())?;
Ok(())
}
impl fmt::Display for OwnerSource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
OwnerSource::Local(id) => write!(f, "local:{}", id),
OwnerSource::Remote(s) => write!(f, "remote:{}", s),
}
}
}
impl fmt::Debug for Store {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Store")
.field("profiles", &"ProfileStore")
.field("files", &"FileStore")
.field("submissions", &"SubmissionStore")
.field("comments", &"CommentStore")
.field("reacts", &"ReactStore")
.field("view", &"ViewStore")
.finish()
}
}