Initial commit

This commit is contained in:
Daan Vanoverloop 2022-08-29 12:03:21 +02:00
commit b96420ec1d
Signed by: Danacus
GPG Key ID: F2272B50E129FC5C
13 changed files with 2622 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2326
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

32
Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "lan-party"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] }
dashmap = "5.3.4"
thiserror = "1.0"
schemars = "0.8.10"
okapi = { version = "0.7.0-rc.1" }
rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger", "rocket_db_pools"] }
futures = "0.3"
[dependencies.sqlx]
version = "*"
default-features = false
features = ["macros", "offline", "migrate"]
[dependencies.rocket_db_pools]
version = "0.1.0-rc.2"
features = ["sqlx_sqlite"]
[dependencies.uuid]
version = "1.1.2"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]

2
Rocket.toml Normal file
View File

@ -0,0 +1,2 @@
[default.databases.party]
url = "party.sqlite"

5
build.rs Normal file
View File

@ -0,0 +1,5 @@
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=db/migrations");
}

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL,
score INTEGER NOT NULL DEFAULT 0
);

BIN
party.db Normal file

Binary file not shown.

BIN
party.sqlite Normal file

Binary file not shown.

BIN
party.sqlite-shm Normal file

Binary file not shown.

BIN
party.sqlite-wal Normal file

Binary file not shown.

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

3
sqlx-data.json Normal file
View File

@ -0,0 +1,3 @@
{
"db": "SQLite"
}

246
src/main.rs Normal file
View File

@ -0,0 +1,246 @@
use futures::{prelude::*, stream::TryStreamExt};
use rocket::{
fairing,
fairing::AdHoc,
http::Status,
response,
response::{status::Created, Responder},
serde::{json::Json, Deserialize, Serialize},
Build, Request, Rocket,
};
use rocket_db_pools::{
sqlx::{self},
Connection, Database,
};
use rocket_okapi::{
openapi, openapi_get_routes,
response::OpenApiResponderInner,
swagger_ui::{make_swagger_ui, SwaggerUIConfig},
JsonSchema,
};
use sqlx::{Acquire, Connection as SqlxConnection, FromRow, Sqlite};
use std::collections::HashMap;
#[macro_use]
extern crate rocket;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PartyError {
#[error("user `{0}` does not exist")]
UserNotFound(i64),
#[error("unknown error: {0}")]
Unknown(String),
#[error("invalid parameter: {0}")]
InvalidParameter(String),
#[error("uuid error {source:?}")]
UuidError {
#[from]
source: uuid::Error,
},
#[error("sqlx error {source:?}")]
SqlxError {
#[from]
source: sqlx::Error,
},
}
impl OpenApiResponderInner for PartyError {
fn responses(
_gen: &mut rocket_okapi::gen::OpenApiGenerator,
) -> rocket_okapi::Result<okapi::openapi3::Responses> {
Ok(okapi::openapi3::Responses::default())
}
}
impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
// log `self` to your favored error tracker, e.g.
// sentry::capture_error(&self);
match self {
// in our simplistic example, we're happy to respond with the default 500 responder in all cases
Self::UserNotFound(_) => Status::NotFound,
_ => Status::InternalServerError,
}
.respond_to(req)
}
}
#[derive(Clone, Debug, FromForm, Serialize, Deserialize, JsonSchema, FromRow)]
#[serde(crate = "rocket::serde")]
struct User {
name: String,
#[serde(default)]
score: i64,
#[serde(default)]
id: i64,
}
#[derive(Database)]
#[database("party")]
struct Db(sqlx::SqlitePool);
#[openapi]
#[get("/")]
fn index() -> String {
format!("Hello, world!")
}
#[openapi]
#[post("/user", data = "<user>")]
async fn add_user(
mut db: Connection<Db>,
mut user: Json<User>,
) -> Result<Created<Json<User>>, PartyError> {
let result = sqlx::query!("INSERT INTO users (name) VALUES (?)", user.name)
.execute(&mut *db)
.await?;
user.id = result.last_insert_rowid();
Ok(Created::new("/").body(user))
}
#[openapi]
#[delete("/user/<id>")]
async fn delete_user(mut db: Connection<Db>, id: i64) -> Result<Status, PartyError> {
sqlx::query!("DELETE FROM users where (id = ?)", id)
.execute(&mut *db)
.await?;
Ok(Status::Ok)
}
#[openapi]
#[get("/user/<id>")]
async fn get_user(mut db: Connection<Db>, id: i64) -> Result<Json<User>, PartyError> {
let user = sqlx::query_as!(User, "SELECT id, name, score FROM users WHERE (id = ?)", id)
.fetch_one(&mut *db)
.await?;
Ok(Json(user))
}
#[derive(Clone, Debug, FromFormField, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum UserSort {
#[field(value = "score")]
Score,
#[field(value = "name")]
Name,
#[field(value = "id")]
Id,
}
#[derive(Clone, Debug, FromFormField, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum Ordering {
#[field(value = "desc")]
Desc,
#[field(value = "asc")]
Asc,
}
impl ToString for Ordering {
fn to_string(&self) -> String {
match self {
Self::Desc => "DESC",
Self::Asc => "ASC",
}
.into()
}
}
impl ToString for UserSort {
fn to_string(&self) -> String {
match self {
Self::Score => "score",
Self::Name => "name",
Self::Id => "id",
}
.into()
}
}
#[openapi]
#[get("/user?<sort>&<order>")]
async fn get_all_users(
mut db: Connection<Db>,
sort: Option<UserSort>,
order: Option<Ordering>,
) -> Result<Json<Vec<User>>, PartyError> {
let users = sqlx::query_as::<_, User>(&format!(
"SELECT id, name, score FROM users ORDER BY {} {}",
sort.unwrap_or(UserSort::Id).to_string(),
order.unwrap_or(Ordering::Asc).to_string()
))
.fetch_all(&mut *db)
.await?;
Ok(Json(users))
}
#[openapi]
#[post("/user/<id>/score", data = "<score>")]
async fn set_score(
mut db: Connection<Db>,
id: i64,
score: Json<i64>,
) -> Result<Status, PartyError> {
sqlx::query!("UPDATE users SET score = ? WHERE id = ?", *score, id)
.execute(&mut *db)
.await?;
Ok(Status::Ok)
}
#[openapi]
#[get("/user/<id>/score")]
async fn get_score(mut db: Connection<Db>, id: i64) -> Result<Json<i64>, PartyError> {
let score = sqlx::query_scalar!("SELECT score FROM users WHERE id = ?", id)
.fetch_one(&mut *db)
.await?;
Ok(Json(score))
}
fn get_docs() -> SwaggerUIConfig {
SwaggerUIConfig {
url: "../api/openapi.json".to_owned(),
..Default::default()
}
}
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
match Db::fetch(&rocket) {
Some(db) => match sqlx::migrate!("db/migrations").run(&**db).await {
Ok(_) => {
println!("Migrations completed");
Ok(rocket)
}
Err(e) => {
error!("Failed to initialize SQLx database: {}", e);
Err(rocket)
}
},
None => Err(rocket),
}
}
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations))
.mount("/", openapi_get_routes![index])
.mount(
"/api",
openapi_get_routes![
add_user,
get_user,
get_all_users,
delete_user,
set_score,
get_score
],
)
.mount("/swagger", make_swagger_ui(&get_docs()))
}