557 lines
18 KiB
Rust
557 lines
18 KiB
Rust
use crate::{
|
|
blogs::Blog,
|
|
schema::{blogs, list_elems, lists, users},
|
|
users::User,
|
|
Connection, Error, Result,
|
|
};
|
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
use std::convert::{TryFrom, TryInto};
|
|
|
|
/// Represent what a list is supposed to store. Represented in database as an integer
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub enum ListType {
|
|
User,
|
|
Blog,
|
|
Word,
|
|
Prefix,
|
|
}
|
|
|
|
impl TryFrom<i32> for ListType {
|
|
type Error = ();
|
|
|
|
fn try_from(i: i32) -> std::result::Result<Self, ()> {
|
|
match i {
|
|
0 => Ok(ListType::User),
|
|
1 => Ok(ListType::Blog),
|
|
2 => Ok(ListType::Word),
|
|
3 => Ok(ListType::Prefix),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ListType> for i32 {
|
|
fn from(list_type: ListType) -> Self {
|
|
match list_type {
|
|
ListType::User => 0,
|
|
ListType::Blog => 1,
|
|
ListType::Word => 2,
|
|
ListType::Prefix => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Queryable, Identifiable)]
|
|
pub struct List {
|
|
pub id: i32,
|
|
pub name: String,
|
|
pub user_id: Option<i32>,
|
|
type_: i32,
|
|
}
|
|
|
|
#[derive(Default, Insertable)]
|
|
#[table_name = "lists"]
|
|
struct NewList<'a> {
|
|
pub name: &'a str,
|
|
pub user_id: Option<i32>,
|
|
type_: i32,
|
|
}
|
|
|
|
macro_rules! func {
|
|
(@elem User $id:expr, $value:expr) => {
|
|
NewListElem {
|
|
list_id: $id,
|
|
user_id: Some(*$value),
|
|
blog_id: None,
|
|
word: None,
|
|
}
|
|
};
|
|
(@elem Blog $id:expr, $value:expr) => {
|
|
NewListElem {
|
|
list_id: $id,
|
|
user_id: None,
|
|
blog_id: Some(*$value),
|
|
word: None,
|
|
}
|
|
};
|
|
(@elem Word $id:expr, $value:expr) => {
|
|
NewListElem {
|
|
list_id: $id,
|
|
user_id: None,
|
|
blog_id: None,
|
|
word: Some($value),
|
|
}
|
|
};
|
|
(@elem Prefix $id:expr, $value:expr) => {
|
|
NewListElem {
|
|
list_id: $id,
|
|
user_id: None,
|
|
blog_id: None,
|
|
word: Some($value),
|
|
}
|
|
};
|
|
(@in_type User) => { i32 };
|
|
(@in_type Blog) => { i32 };
|
|
(@in_type Word) => { &str };
|
|
(@in_type Prefix) => { &str };
|
|
(@out_type User) => { User };
|
|
(@out_type Blog) => { Blog };
|
|
(@out_type Word) => { String };
|
|
(@out_type Prefix) => { String };
|
|
|
|
(add: $fn:ident, $kind:ident) => {
|
|
pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> {
|
|
if self.kind() != ListType::$kind {
|
|
return Err(Error::InvalidValue);
|
|
}
|
|
diesel::insert_into(list_elems::table)
|
|
.values(
|
|
vals
|
|
.iter()
|
|
.map(|u| func!(@elem $kind self.id, u))
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.execute(conn)?;
|
|
Ok(())
|
|
}
|
|
};
|
|
|
|
(list: $fn:ident, $kind:ident, $table:ident) => {
|
|
pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> {
|
|
if self.kind() != ListType::$kind {
|
|
return Err(Error::InvalidValue);
|
|
}
|
|
list_elems::table
|
|
.filter(list_elems::list_id.eq(self.id))
|
|
.inner_join($table::table)
|
|
.select($table::all_columns)
|
|
.load(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
};
|
|
|
|
|
|
|
|
(set: $fn:ident, $kind:ident, $add:ident) => {
|
|
pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> {
|
|
if self.kind() != ListType::$kind {
|
|
return Err(Error::InvalidValue);
|
|
}
|
|
self.clear(conn)?;
|
|
self.$add(conn, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Queryable, Identifiable)]
|
|
struct ListElem {
|
|
pub id: i32,
|
|
pub list_id: i32,
|
|
pub user_id: Option<i32>,
|
|
pub blog_id: Option<i32>,
|
|
pub word: Option<String>,
|
|
}
|
|
|
|
#[derive(Default, Insertable)]
|
|
#[table_name = "list_elems"]
|
|
struct NewListElem<'a> {
|
|
pub list_id: i32,
|
|
pub user_id: Option<i32>,
|
|
pub blog_id: Option<i32>,
|
|
pub word: Option<&'a str>,
|
|
}
|
|
|
|
impl List {
|
|
last!(lists);
|
|
get!(lists);
|
|
|
|
fn insert(conn: &Connection, val: NewList<'_>) -> Result<Self> {
|
|
diesel::insert_into(lists::table)
|
|
.values(val)
|
|
.execute(conn)?;
|
|
List::last(conn)
|
|
}
|
|
|
|
pub fn list_for_user(conn: &Connection, user_id: Option<i32>) -> Result<Vec<Self>> {
|
|
if let Some(user_id) = user_id {
|
|
lists::table
|
|
.filter(lists::user_id.eq(user_id))
|
|
.load::<Self>(conn)
|
|
.map_err(Error::from)
|
|
} else {
|
|
lists::table
|
|
.filter(lists::user_id.is_null())
|
|
.load::<Self>(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
}
|
|
|
|
pub fn find_for_user_by_name(
|
|
conn: &Connection,
|
|
user_id: Option<i32>,
|
|
name: &str,
|
|
) -> Result<Self> {
|
|
if let Some(user_id) = user_id {
|
|
lists::table
|
|
.filter(lists::user_id.eq(user_id))
|
|
.filter(lists::name.eq(name))
|
|
.first(conn)
|
|
.map_err(Error::from)
|
|
} else {
|
|
lists::table
|
|
.filter(lists::user_id.is_null())
|
|
.filter(lists::name.eq(name))
|
|
.first(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
}
|
|
|
|
pub fn new(conn: &Connection, name: &str, user: Option<&User>, kind: ListType) -> Result<Self> {
|
|
Self::insert(
|
|
conn,
|
|
NewList {
|
|
name,
|
|
user_id: user.map(|u| u.id),
|
|
type_: kind.into(),
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Returns the kind of a list
|
|
pub fn kind(&self) -> ListType {
|
|
self.type_.try_into().expect("invalid list was constructed")
|
|
}
|
|
|
|
/// Return Ok(true) if the list contain the given user, Ok(false) otherwiser,
|
|
/// and Err(_) on error
|
|
pub fn contains_user(&self, conn: &Connection, user: i32) -> Result<bool> {
|
|
private::ListElem::user_in_list(conn, self, user)
|
|
}
|
|
|
|
/// Return Ok(true) if the list contain the given blog, Ok(false) otherwiser,
|
|
/// and Err(_) on error
|
|
pub fn contains_blog(&self, conn: &Connection, blog: i32) -> Result<bool> {
|
|
private::ListElem::blog_in_list(conn, self, blog)
|
|
}
|
|
|
|
/// Return Ok(true) if the list contain the given word, Ok(false) otherwiser,
|
|
/// and Err(_) on error
|
|
pub fn contains_word(&self, conn: &Connection, word: &str) -> Result<bool> {
|
|
private::ListElem::word_in_list(conn, self, word)
|
|
}
|
|
|
|
/// Return Ok(true) if the list match the given prefix, Ok(false) otherwiser,
|
|
/// and Err(_) on error
|
|
pub fn contains_prefix(&self, conn: &Connection, word: &str) -> Result<bool> {
|
|
private::ListElem::prefix_in_list(conn, self, word)
|
|
}
|
|
|
|
// Insert new users in a list
|
|
func! {add: add_users, User}
|
|
|
|
// Insert new blogs in a list
|
|
func! {add: add_blogs, Blog}
|
|
|
|
// Insert new words in a list
|
|
func! {add: add_words, Word}
|
|
|
|
// Insert new prefixes in a list
|
|
func! {add: add_prefixes, Prefix}
|
|
|
|
// Get all users in the list
|
|
func! {list: list_users, User, users}
|
|
|
|
// Get all blogs in the list
|
|
func! {list: list_blogs, Blog, blogs}
|
|
|
|
/// Get all words in the list
|
|
pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> {
|
|
self.list_stringlike(conn, ListType::Word)
|
|
}
|
|
|
|
/// Get all prefixes in the list
|
|
pub fn list_prefixes(&self, conn: &Connection) -> Result<Vec<String>> {
|
|
self.list_stringlike(conn, ListType::Prefix)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn list_stringlike(&self, conn: &Connection, t: ListType) -> Result<Vec<String>> {
|
|
if self.kind() != t {
|
|
return Err(Error::InvalidValue);
|
|
}
|
|
list_elems::table
|
|
.filter(list_elems::list_id.eq(self.id))
|
|
.filter(list_elems::word.is_not_null())
|
|
.select(list_elems::word)
|
|
.load::<Option<String>>(conn)
|
|
.map_err(Error::from)
|
|
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
|
|
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
|
|
}
|
|
|
|
pub fn clear(&self, conn: &Connection) -> Result<()> {
|
|
diesel::delete(list_elems::table.filter(list_elems::list_id.eq(self.id)))
|
|
.execute(conn)
|
|
.map(|_| ())
|
|
.map_err(Error::from)
|
|
}
|
|
|
|
func! {set: set_users, User, add_users}
|
|
func! {set: set_blogs, Blog, add_blogs}
|
|
func! {set: set_words, Word, add_words}
|
|
func! {set: set_prefixes, Prefix, add_prefixes}
|
|
}
|
|
|
|
mod private {
|
|
pub use super::*;
|
|
use diesel::{
|
|
dsl,
|
|
sql_types::{Nullable, Text},
|
|
IntoSql, TextExpressionMethods,
|
|
};
|
|
|
|
impl ListElem {
|
|
insert!(list_elems, NewListElem<'_>);
|
|
|
|
pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result<bool> {
|
|
dsl::select(dsl::exists(
|
|
list_elems::table
|
|
.filter(list_elems::list_id.eq(list.id))
|
|
.filter(list_elems::user_id.eq(Some(user))),
|
|
))
|
|
.get_result(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
|
|
pub fn blog_in_list(conn: &Connection, list: &List, blog: i32) -> Result<bool> {
|
|
dsl::select(dsl::exists(
|
|
list_elems::table
|
|
.filter(list_elems::list_id.eq(list.id))
|
|
.filter(list_elems::blog_id.eq(Some(blog))),
|
|
))
|
|
.get_result(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
|
|
pub fn word_in_list(conn: &Connection, list: &List, word: &str) -> Result<bool> {
|
|
dsl::select(dsl::exists(
|
|
list_elems::table
|
|
.filter(list_elems::list_id.eq(list.id))
|
|
.filter(list_elems::word.eq(word)),
|
|
))
|
|
.get_result(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
|
|
pub fn prefix_in_list(conn: &Connection, list: &List, word: &str) -> Result<bool> {
|
|
dsl::select(dsl::exists(
|
|
list_elems::table
|
|
.filter(
|
|
word.into_sql::<Nullable<Text>>()
|
|
.like(list_elems::word.concat("%")),
|
|
)
|
|
.filter(list_elems::list_id.eq(list.id)),
|
|
))
|
|
.get_result(conn)
|
|
.map_err(Error::from)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{blogs::tests as blog_tests, tests::db};
|
|
use diesel::Connection;
|
|
|
|
#[test]
|
|
fn list_type() {
|
|
for i in 0..4 {
|
|
assert_eq!(i, Into::<i32>::into(ListType::try_from(i).unwrap()));
|
|
}
|
|
ListType::try_from(4).unwrap_err();
|
|
}
|
|
|
|
#[test]
|
|
fn list_lists() {
|
|
let conn = &db();
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
let (users, _) = blog_tests::fill_database(conn);
|
|
|
|
let l1 = List::new(conn, "list1", None, ListType::User).unwrap();
|
|
let l2 = List::new(conn, "list2", None, ListType::Blog).unwrap();
|
|
let l1u = List::new(conn, "list1", Some(&users[0]), ListType::Word).unwrap();
|
|
|
|
let l_eq = |l1: &List, l2: &List| {
|
|
assert_eq!(l1.id, l2.id);
|
|
assert_eq!(l1.user_id, l2.user_id);
|
|
assert_eq!(l1.name, l2.name);
|
|
assert_eq!(l1.type_, l2.type_);
|
|
};
|
|
|
|
let l1bis = List::get(conn, l1.id).unwrap();
|
|
l_eq(&l1, &l1bis);
|
|
|
|
let l_inst = List::list_for_user(conn, None).unwrap();
|
|
let l_user = List::list_for_user(conn, Some(users[0].id)).unwrap();
|
|
assert_eq!(2, l_inst.len());
|
|
assert_eq!(1, l_user.len());
|
|
assert!(l_inst.iter().all(|l| l.id != l1u.id));
|
|
|
|
l_eq(&l1u, &l_user[0]);
|
|
if l_inst[0].id == l1.id {
|
|
l_eq(&l1, &l_inst[0]);
|
|
l_eq(&l2, &l_inst[1]);
|
|
} else {
|
|
l_eq(&l1, &l_inst[1]);
|
|
l_eq(&l2, &l_inst[0]);
|
|
}
|
|
|
|
l_eq(
|
|
&l1,
|
|
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
|
|
);
|
|
l_eq(
|
|
&&l1u,
|
|
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
|
|
);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_list() {
|
|
let conn = &db();
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
let (users, blogs) = blog_tests::fill_database(conn);
|
|
|
|
let l = List::new(conn, "list", None, ListType::User).unwrap();
|
|
|
|
assert_eq!(l.kind(), ListType::User);
|
|
assert!(l.list_users(conn).unwrap().is_empty());
|
|
|
|
assert!(!l.contains_user(conn, users[0].id).unwrap());
|
|
assert!(l.add_users(conn, &[users[0].id]).is_ok());
|
|
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
|
|
assert!(l.add_users(conn, &[users[1].id]).is_ok());
|
|
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
assert!(l.contains_user(conn, users[1].id).unwrap());
|
|
assert_eq!(2, l.list_users(conn).unwrap().len());
|
|
|
|
assert!(l.set_users(conn, &[users[0].id]).is_ok());
|
|
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
assert!(!l.contains_user(conn, users[1].id).unwrap());
|
|
assert_eq!(1, l.list_users(conn).unwrap().len());
|
|
assert!(users[0] == l.list_users(conn).unwrap()[0]);
|
|
|
|
l.clear(conn).unwrap();
|
|
assert!(l.list_users(conn).unwrap().is_empty());
|
|
|
|
assert!(l.add_blogs(conn, &[blogs[0].id]).is_err());
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_blog_list() {
|
|
let conn = &db();
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
let (users, blogs) = blog_tests::fill_database(conn);
|
|
|
|
let l = List::new(conn, "list", None, ListType::Blog).unwrap();
|
|
|
|
assert_eq!(l.kind(), ListType::Blog);
|
|
assert!(l.list_blogs(conn).unwrap().is_empty());
|
|
|
|
assert!(!l.contains_blog(conn, blogs[0].id).unwrap());
|
|
assert!(l.add_blogs(conn, &[blogs[0].id]).is_ok());
|
|
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
|
|
assert!(l.add_blogs(conn, &[blogs[1].id]).is_ok());
|
|
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
assert!(l.contains_blog(conn, blogs[1].id).unwrap());
|
|
assert_eq!(2, l.list_blogs(conn).unwrap().len());
|
|
|
|
assert!(l.set_blogs(conn, &[blogs[0].id]).is_ok());
|
|
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
assert!(!l.contains_blog(conn, blogs[1].id).unwrap());
|
|
assert_eq!(1, l.list_blogs(conn).unwrap().len());
|
|
assert_eq!(blogs[0].id, l.list_blogs(conn).unwrap()[0].id);
|
|
|
|
l.clear(conn).unwrap();
|
|
assert!(l.list_blogs(conn).unwrap().is_empty());
|
|
|
|
assert!(l.add_users(conn, &[users[0].id]).is_err());
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_word_list() {
|
|
let conn = &db();
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
let l = List::new(conn, "list", None, ListType::Word).unwrap();
|
|
|
|
assert_eq!(l.kind(), ListType::Word);
|
|
assert!(l.list_words(conn).unwrap().is_empty());
|
|
|
|
assert!(!l.contains_word(conn, "plume").unwrap());
|
|
assert!(l.add_words(conn, &["plume"]).is_ok());
|
|
assert!(l.contains_word(conn, "plume").unwrap());
|
|
assert!(!l.contains_word(conn, "plumelin").unwrap());
|
|
|
|
assert!(l.add_words(conn, &["amsterdam"]).is_ok());
|
|
assert!(l.contains_word(conn, "plume").unwrap());
|
|
assert!(l.contains_word(conn, "amsterdam").unwrap());
|
|
assert_eq!(2, l.list_words(conn).unwrap().len());
|
|
|
|
assert!(l.set_words(conn, &["plume"]).is_ok());
|
|
assert!(l.contains_word(conn, "plume").unwrap());
|
|
assert!(!l.contains_word(conn, "amsterdam").unwrap());
|
|
assert_eq!(1, l.list_words(conn).unwrap().len());
|
|
assert_eq!("plume", l.list_words(conn).unwrap()[0]);
|
|
|
|
l.clear(conn).unwrap();
|
|
assert!(l.list_words(conn).unwrap().is_empty());
|
|
|
|
assert!(l.add_prefixes(conn, &["something"]).is_err());
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_prefix_list() {
|
|
let conn = &db();
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
let l = List::new(conn, "list", None, ListType::Prefix).unwrap();
|
|
|
|
assert_eq!(l.kind(), ListType::Prefix);
|
|
assert!(l.list_prefixes(conn).unwrap().is_empty());
|
|
|
|
assert!(!l.contains_prefix(conn, "plume").unwrap());
|
|
assert!(l.add_prefixes(conn, &["plume"]).is_ok());
|
|
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
assert!(l.contains_prefix(conn, "plumelin").unwrap());
|
|
|
|
assert!(l.add_prefixes(conn, &["amsterdam"]).is_ok());
|
|
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
assert!(l.contains_prefix(conn, "amsterdam").unwrap());
|
|
assert_eq!(2, l.list_prefixes(conn).unwrap().len());
|
|
|
|
assert!(l.set_prefixes(conn, &["plume"]).is_ok());
|
|
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
assert!(!l.contains_prefix(conn, "amsterdam").unwrap());
|
|
assert_eq!(1, l.list_prefixes(conn).unwrap().len());
|
|
assert_eq!("plume", l.list_prefixes(conn).unwrap()[0]);
|
|
|
|
l.clear(conn).unwrap();
|
|
assert!(l.list_prefixes(conn).unwrap().is_empty());
|
|
|
|
assert!(l.add_words(conn, &["something"]).is_err());
|
|
Ok(())
|
|
});
|
|
}
|
|
}
|