Initial commit
This commit is contained in:
commit
b96420ec1d
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[default.databases.party]
|
||||||
|
url = "party.sqlite"
|
|
@ -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");
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
edition = "2021"
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"db": "SQLite"
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
}
|
Loading…
Reference in New Issue