Create client to establish connection with discord and add basic framework with sample command

This commit is contained in:
2025-01-29 22:22:08 +00:00
parent fc07051558
commit a719b143de
50 changed files with 1712 additions and 128 deletions

View File

@@ -0,0 +1,38 @@
#[cfg(feature = "mysql")]
pub mod mysql;
#[cfg(feature = "postgres")]
pub mod postgres;
#[cfg(feature = "sqlite")]
pub mod sqlite;
#[derive(Clone, Debug)]
pub enum DatabaseDialect {
#[cfg(feature = "mysql")]
Mysql,
#[cfg(feature = "postgres")]
Postgres,
#[cfg(feature = "sqlite")]
Sqlite,
}
#[derive(Debug, thiserror::Error)]
pub enum BackendError {
#[error(transparent)]
DieselConnectionError(#[from] diesel::ConnectionError),
#[error(transparent)]
DieselQueryError(#[from] diesel::result::Error),
#[error(transparent)]
DieselMigrationError(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Bb8RunError(#[from] diesel_async::pooled_connection::bb8::RunError),
}
impl From<diesel_async::pooled_connection::PoolError> for BackendError {
fn from(value: diesel_async::pooled_connection::PoolError) -> Self {
use diesel_async::pooled_connection::PoolError as E;
match value {
E::ConnectionError(connection_error) => Self::from(connection_error),
E::QueryError(error) => Self::from(error),
}
}
}

View File

@@ -0,0 +1,29 @@
use diesel::Connection;
use diesel::MysqlConnection;
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_migrations::embed_migrations;
use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
use repository::MysqlRepositoryProvider;
use crate::BackendError;
pub mod repository;
mod schema;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/mysql");
pub fn run_pending_migrations(database_url: &str) -> Result<(), BackendError> {
let mut connection = MysqlConnection::establish(database_url)?;
connection
.run_pending_migrations(MIGRATIONS)
.map_err(BackendError::DieselMigrationError)?;
Ok(())
}
pub async fn repository_provider(database_url: &str) -> Result<MysqlRepositoryProvider, BackendError> {
let config = AsyncDieselConnectionManager::new(database_url);
let pool = Pool::builder().build(config).await?;
Ok(MysqlRepositoryProvider::new(pool))
}

View File

@@ -0,0 +1,49 @@
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::bb8::PooledConnection;
use diesel_async::AsyncMysqlConnection;
use cipher_core::repository::Repository;
use cipher_core::repository::RepositoryError;
use cipher_core::repository::RepositoryProvider;
use crate::BackendError;
mod user_repository;
pub struct MysqlRepository<'a> {
conn: PooledConnection<'a, AsyncMysqlConnection>,
}
impl<'a> MysqlRepository<'a> {
pub fn new(conn: PooledConnection<'a, AsyncMysqlConnection>) -> Self {
Self { conn }
}
}
impl Repository for MysqlRepository<'_> {
type BackendError = BackendError;
}
pub struct MysqlRepositoryProvider {
pool: Pool<AsyncMysqlConnection>,
}
impl MysqlRepositoryProvider {
pub fn new(pool: Pool<AsyncMysqlConnection>) -> Self {
Self { pool }
}
}
#[async_trait::async_trait]
impl RepositoryProvider for MysqlRepositoryProvider {
type BackendError = BackendError;
type Repository<'a> = MysqlRepository<'a>;
async fn get(&self) -> Result<Self::Repository<'_>, RepositoryError<Self::BackendError>> {
self.pool
.get()
.await
.map(MysqlRepository::new)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}

View File

@@ -0,0 +1,161 @@
use diesel::prelude::*;
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncConnection;
use diesel_async::RunQueryDsl;
use cipher_core::repository::user_repository::NewUser;
use cipher_core::repository::user_repository::User;
use cipher_core::repository::user_repository::UserRepository;
use cipher_core::repository::RepositoryError;
use crate::mysql::schema::users;
use crate::BackendError;
use super::MysqlRepository;
#[async_trait::async_trait]
#[rustfmt::skip]
impl UserRepository for MysqlRepository<'_> {
type BackendError = BackendError;
async fn user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
users::dsl::users.find(id)
.first::<ModelUser>(&mut self.conn)
.await
.optional()
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn insert_user(&mut self, new_user: NewUser) -> Result<User, RepositoryError<Self::BackendError>> {
let model_new_user = ModelNewUser::from(new_user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
diesel::insert_into(users::table)
.values(&model_new_user)
.execute(conn)
.await?;
users::table
.order(users::id.desc())
.select(ModelUser::as_select())
.first(conn)
.await
}.scope_boxed())
.await
.map(User::from)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn update_user(&mut self, user: User) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
let model_user = ModelUser::from(user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
let option_previous = users::dsl::users.find(model_user.id)
.select(ModelUser::as_select())
.first(conn)
.await
.optional()?;
let previous = match option_previous {
Some(previous) => previous,
None => return Ok(None),
};
diesel::update(users::dsl::users.find(model_user.id))
.set(&model_user)
.execute(conn)
.await?;
Ok(Some(previous))
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
self.conn
.transaction::<_, diesel::result::Error, _>(move |conn| async move {
let option_removed = users::dsl::users.find(id)
.select(ModelUser::as_select())
.first(conn)
.await
.optional()?;
let removed = match option_removed {
Some(previous) => previous,
None => return Ok(None),
};
diesel::delete(users::dsl::users.find(id))
.execute(conn)
.await?;
Ok(Some(removed))
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}
#[derive(Queryable, Selectable, AsChangeset)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelUser {
id: i32,
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<ModelUser> for User {
fn from(value: ModelUser) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as u64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
impl From<User> for ModelUser {
fn from(value: User) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
#[derive(Insertable)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelNewUser {
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self {
Self {
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}

View File

@@ -0,0 +1,14 @@
// @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Integer,
discord_user_id -> Bigint,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
}
}

View File

@@ -0,0 +1,29 @@
use diesel::Connection;
use diesel::PgConnection;
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_migrations::embed_migrations;
use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
use repository::PostgresRepositoryProvider;
use crate::BackendError;
pub mod repository;
mod schema;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/postgres");
pub fn run_pending_migrations(database_url: &str) -> Result<(), BackendError> {
let mut connection = PgConnection::establish(database_url)?;
connection
.run_pending_migrations(MIGRATIONS)
.map_err(BackendError::DieselMigrationError)?;
Ok(())
}
pub async fn repository_provider(database_url: &str) -> Result<PostgresRepositoryProvider, BackendError> {
let config = AsyncDieselConnectionManager::new(database_url);
let pool = Pool::builder().build(config).await?;
Ok(PostgresRepositoryProvider::new(pool))
}

View File

@@ -0,0 +1,49 @@
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::bb8::PooledConnection;
use diesel_async::AsyncPgConnection;
use cipher_core::repository::Repository;
use cipher_core::repository::RepositoryError;
use cipher_core::repository::RepositoryProvider;
use crate::BackendError;
mod user_repository;
pub struct PostgresRepository<'a> {
conn: PooledConnection<'a, AsyncPgConnection>,
}
impl<'a> PostgresRepository<'a> {
pub fn new(conn: PooledConnection<'a, AsyncPgConnection>) -> Self {
Self { conn }
}
}
impl Repository for PostgresRepository<'_> {
type BackendError = BackendError;
}
pub struct PostgresRepositoryProvider {
pool: Pool<AsyncPgConnection>,
}
impl PostgresRepositoryProvider {
pub fn new(pool: Pool<AsyncPgConnection>) -> Self {
Self { pool }
}
}
#[async_trait::async_trait]
impl RepositoryProvider for PostgresRepositoryProvider {
type BackendError = BackendError;
type Repository<'a> = PostgresRepository<'a>;
async fn get(&self) -> Result<Self::Repository<'_>, RepositoryError<Self::BackendError>> {
self.pool
.get()
.await
.map(PostgresRepository::new)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}

View File

@@ -0,0 +1,144 @@
use diesel::prelude::*;
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncConnection;
use diesel_async::RunQueryDsl;
use cipher_core::repository::user_repository::NewUser;
use cipher_core::repository::user_repository::User;
use cipher_core::repository::user_repository::UserRepository;
use cipher_core::repository::RepositoryError;
use crate::postgres::schema::users;
use crate::BackendError;
use super::PostgresRepository;
#[async_trait::async_trait]
#[rustfmt::skip]
impl UserRepository for PostgresRepository<'_> {
type BackendError = BackendError;
async fn user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
users::dsl::users.find(id)
.first::<ModelUser>(&mut self.conn)
.await
.optional()
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn insert_user(&mut self, new_user: NewUser) -> Result<User, RepositoryError<Self::BackendError>> {
let model_new_user = ModelNewUser::from(new_user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
diesel::insert_into(users::table)
.values(&model_new_user)
.returning(ModelUser::as_returning())
.get_result(conn)
.await
}.scope_boxed())
.await
.map(User::from)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn update_user(&mut self, user: User) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
let model_user = ModelUser::from(user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
let option_previous = users::dsl::users.find(model_user.id)
.select(ModelUser::as_select())
.first(conn)
.await
.optional()?;
let previous = match option_previous {
Some(previous) => previous,
None => return Ok(None),
};
diesel::update(users::dsl::users.find(model_user.id))
.set(&model_user)
.execute(conn)
.await?;
Ok(Some(previous))
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
self.conn
.transaction::<_, diesel::result::Error, _>(move |conn| async move {
diesel::delete(users::dsl::users.find(id))
.returning(ModelUser::as_returning())
.get_result(conn)
.await
.optional()
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}
#[derive(Queryable, Selectable, AsChangeset)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct ModelUser {
id: i32,
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<ModelUser> for User {
fn from(value: ModelUser) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as u64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
impl From<User> for ModelUser {
fn from(value: User) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
#[derive(Insertable)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct ModelNewUser {
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self {
Self {
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}

View File

@@ -0,0 +1,14 @@
// @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Int4,
discord_user_id -> Int8,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
}
}

View File

@@ -0,0 +1,29 @@
use diesel::Connection;
use diesel::SqliteConnection;
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_migrations::embed_migrations;
use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
use repository::SqliteRepositoryProvider;
use crate::BackendError;
pub mod repository;
mod schema;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/sqlite");
pub fn run_pending_migrations(database_url: &str) -> Result<(), BackendError> {
let mut connection = SqliteConnection::establish(database_url)?;
connection
.run_pending_migrations(MIGRATIONS)
.map_err(BackendError::DieselMigrationError)?;
Ok(())
}
pub async fn repository_provider(database_url: &str) -> Result<SqliteRepositoryProvider, BackendError> {
let config = AsyncDieselConnectionManager::new(database_url);
let pool = Pool::builder().build(config).await?;
Ok(SqliteRepositoryProvider::new(pool))
}

View File

@@ -0,0 +1,50 @@
use diesel::SqliteConnection;
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::bb8::PooledConnection;
use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
use cipher_core::repository::Repository;
use cipher_core::repository::RepositoryError;
use cipher_core::repository::RepositoryProvider;
use crate::BackendError;
mod user_repository;
pub struct SqliteRepository<'a> {
conn: PooledConnection<'a, SyncConnectionWrapper<SqliteConnection>>,
}
impl<'a> SqliteRepository<'a> {
pub fn new(conn: PooledConnection<'a, SyncConnectionWrapper<SqliteConnection>>) -> Self {
Self { conn }
}
}
impl Repository for SqliteRepository<'_> {
type BackendError = BackendError;
}
pub struct SqliteRepositoryProvider {
pool: Pool<SyncConnectionWrapper<SqliteConnection>>,
}
impl SqliteRepositoryProvider {
pub fn new(pool: Pool<SyncConnectionWrapper<SqliteConnection>>) -> Self {
Self { pool }
}
}
#[async_trait::async_trait]
impl RepositoryProvider for SqliteRepositoryProvider {
type BackendError = BackendError;
type Repository<'a> = SqliteRepository<'a>;
async fn get(&self) -> Result<Self::Repository<'_>, RepositoryError<Self::BackendError>> {
self.pool
.get()
.await
.map(SqliteRepository::new)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}

View File

@@ -0,0 +1,144 @@
use diesel::prelude::*;
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncConnection;
use diesel_async::RunQueryDsl;
use cipher_core::repository::user_repository::NewUser;
use cipher_core::repository::user_repository::User;
use cipher_core::repository::user_repository::UserRepository;
use cipher_core::repository::RepositoryError;
use crate::sqlite::schema::users;
use crate::BackendError;
use super::SqliteRepository;
#[async_trait::async_trait]
#[rustfmt::skip]
impl UserRepository for SqliteRepository<'_> {
type BackendError = BackendError;
async fn user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
users::dsl::users.find(id)
.first::<ModelUser>(&mut self.conn)
.await
.optional()
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn insert_user(&mut self, new_user: NewUser) -> Result<User, RepositoryError<Self::BackendError>> {
let model_new_user = ModelNewUser::from(new_user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
diesel::insert_into(users::table)
.values(&model_new_user)
.returning(ModelUser::as_returning())
.get_result(conn)
.await
}.scope_boxed())
.await
.map(User::from)
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn update_user(&mut self, user: User) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
let model_user = ModelUser::from(user);
self.conn
.transaction::<_, diesel::result::Error, _>(|conn| async move {
let option_previous = users::dsl::users.find(model_user.id)
.select(ModelUser::as_select())
.first(conn)
.await
.optional()?;
let previous = match option_previous {
Some(previous) => previous,
None => return Ok(None),
};
diesel::update(users::dsl::users.find(model_user.id))
.set(&model_user)
.execute(conn)
.await?;
Ok(Some(previous))
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
self.conn
.transaction::<_, diesel::result::Error, _>(move |conn| async move {
diesel::delete(users::dsl::users.find(id))
.returning(ModelUser::as_returning())
.get_result(conn)
.await
.optional()
}.scope_boxed())
.await
.map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err)))
}
}
#[derive(Queryable, Selectable, AsChangeset)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
struct ModelUser {
id: i32,
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<ModelUser> for User {
fn from(value: ModelUser) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as u64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
impl From<User> for ModelUser {
fn from(value: User) -> Self {
Self {
id: value.id,
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}
#[derive(Insertable)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
struct ModelNewUser {
discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
}
impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self {
Self {
discord_user_id: value.discord_user_id as i64,
pokemon_go_code: value.pokemon_go_code,
pokemon_pocket_code: value.pokemon_pocket_code,
switch_code: value.switch_code,
}
}
}

View File

@@ -0,0 +1,11 @@
// @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Integer,
discord_user_id -> BigInt,
pokemon_go_code -> Nullable<Text>,
pokemon_pocket_code -> Nullable<Text>,
switch_code -> Nullable<Text>,
}
}