Set up some routes using new Rust API
This commit is contained in:
parent
fdc58b428b
commit
fc8e2a4360
26 changed files with 703 additions and 134 deletions
22
backend/src/errors.rs
Normal file
22
backend/src/errors.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use common::adaptor::Adaptor;
|
||||
|
||||
pub enum ApiError<A: Adaptor> {
|
||||
AdaptorError(A::Error),
|
||||
NotFound,
|
||||
// NotAuthorized,
|
||||
}
|
||||
|
||||
// Define what the error types above should return
|
||||
impl<A: Adaptor> IntoResponse for ApiError<A> {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
ApiError::AdaptorError(e) => {
|
||||
tracing::error!(?e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
ApiError::NotFound => StatusCode::NOT_FOUND.into_response(),
|
||||
// ApiError::NotAuthorized => StatusCode::UNAUTHORIZED.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{routing::get, Router, Server};
|
||||
use data::adaptor::Adaptor;
|
||||
use sql_adaptor::PostgresAdaptor;
|
||||
use axum::{
|
||||
extract,
|
||||
routing::{get, post},
|
||||
Router, Server,
|
||||
};
|
||||
use routes::*;
|
||||
use sql_adaptor::SqlAdaptor;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
mod errors;
|
||||
mod payloads;
|
||||
mod routes;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MODE: &str = "debug";
|
||||
|
|
@ -10,14 +19,29 @@ const MODE: &str = "debug";
|
|||
#[cfg(not(debug_assertions))]
|
||||
const MODE: &str = "release";
|
||||
|
||||
pub struct ApiState<A> {
|
||||
adaptor: A,
|
||||
}
|
||||
|
||||
pub type State<A> = extract::State<Arc<Mutex<ApiState<A>>>>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Load env
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
PostgresAdaptor::new().await;
|
||||
let shared_state = Arc::new(Mutex::new(ApiState {
|
||||
adaptor: SqlAdaptor::new().await,
|
||||
}));
|
||||
|
||||
let app = Router::new().route("/", get(get_root));
|
||||
let app = Router::new()
|
||||
.route("/", get(get_root))
|
||||
.route("/stats", get(get_stats))
|
||||
.route("/event/:event_id", get(get_event))
|
||||
.route("/event", post(create_event))
|
||||
.with_state(shared_state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
|
|
|
|||
43
backend/src/payloads.rs
Normal file
43
backend/src/payloads.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use axum::Json;
|
||||
use common::event::Event;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
|
||||
pub type ApiResult<T, A> = Result<Json<T>, ApiError<A>>;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EventInput {
|
||||
pub name: String,
|
||||
pub times: Vec<String>,
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EventResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub times: Vec<String>,
|
||||
pub timezone: String,
|
||||
pub created: i64,
|
||||
}
|
||||
|
||||
impl From<Event> for EventResponse {
|
||||
fn from(value: Event) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
times: value.times,
|
||||
timezone: value.timezone,
|
||||
created: value.created_at.timestamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all(serialize = "camelCase"))]
|
||||
pub struct StatsResponse {
|
||||
pub event_count: i32,
|
||||
pub person_count: i32,
|
||||
pub version: String,
|
||||
}
|
||||
201
backend/src/res/adjectives.json
Normal file
201
backend/src/res/adjectives.json
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
[
|
||||
"Adorable",
|
||||
"Adventurous",
|
||||
"Aggressive",
|
||||
"Agreeable",
|
||||
"Alert",
|
||||
"Alive",
|
||||
"Amused",
|
||||
"Angry",
|
||||
"Annoyed",
|
||||
"Annoying",
|
||||
"Anxious",
|
||||
"Arrogant",
|
||||
"Ashamed",
|
||||
"Attractive",
|
||||
"Average",
|
||||
"Beautiful",
|
||||
"Better",
|
||||
"Bewildered",
|
||||
"Blue",
|
||||
"Blushing",
|
||||
"Bored",
|
||||
"Brainy",
|
||||
"Brave",
|
||||
"Breakable",
|
||||
"Bright",
|
||||
"Busy",
|
||||
"Calm",
|
||||
"Careful",
|
||||
"Cautious",
|
||||
"Charming",
|
||||
"Cheerful",
|
||||
"Clean",
|
||||
"Clear",
|
||||
"Clever",
|
||||
"Cloudy",
|
||||
"Clumsy",
|
||||
"Colorful",
|
||||
"Comfortable",
|
||||
"Concerned",
|
||||
"Confused",
|
||||
"Cooperative",
|
||||
"Courageous",
|
||||
"Crazy",
|
||||
"Creepy",
|
||||
"Crowded",
|
||||
"Curious",
|
||||
"Cute",
|
||||
"Dangerous",
|
||||
"Dark",
|
||||
"Defiant",
|
||||
"Delightful",
|
||||
"Depressed",
|
||||
"Determined",
|
||||
"Different",
|
||||
"Difficult",
|
||||
"Disgusted",
|
||||
"Distinct",
|
||||
"Disturbed",
|
||||
"Dizzy",
|
||||
"Doubtful",
|
||||
"Drab",
|
||||
"Dull",
|
||||
"Eager",
|
||||
"Easy",
|
||||
"Elated",
|
||||
"Elegant",
|
||||
"Embarrassed",
|
||||
"Enchanting",
|
||||
"Encouraging",
|
||||
"Energetic",
|
||||
"Enthusiastic",
|
||||
"Envious",
|
||||
"Evil",
|
||||
"Excited",
|
||||
"Expensive",
|
||||
"Exuberant",
|
||||
"Fair",
|
||||
"Faithful",
|
||||
"Famous",
|
||||
"Fancy",
|
||||
"Fantastic",
|
||||
"Fierce",
|
||||
"Fine",
|
||||
"Foolish",
|
||||
"Fragile",
|
||||
"Frail",
|
||||
"Frantic",
|
||||
"Friendly",
|
||||
"Frightened",
|
||||
"Funny",
|
||||
"Gentle",
|
||||
"Gifted",
|
||||
"Glamorous",
|
||||
"Gleaming",
|
||||
"Glorious",
|
||||
"Good",
|
||||
"Gorgeous",
|
||||
"Graceful",
|
||||
"Grumpy",
|
||||
"Handsome",
|
||||
"Happy",
|
||||
"Healthy",
|
||||
"Helpful",
|
||||
"Hilarious",
|
||||
"Homely",
|
||||
"Hungry",
|
||||
"Important",
|
||||
"Impossible",
|
||||
"Inexpensive",
|
||||
"Innocent",
|
||||
"Inquisitive",
|
||||
"Itchy",
|
||||
"Jealous",
|
||||
"Jittery",
|
||||
"Jolly",
|
||||
"Joyous",
|
||||
"Kind",
|
||||
"Lazy",
|
||||
"Light",
|
||||
"Lively",
|
||||
"Lonely",
|
||||
"Long",
|
||||
"Lovely",
|
||||
"Lucky",
|
||||
"Magnificent",
|
||||
"Misty",
|
||||
"Modern",
|
||||
"Motionless",
|
||||
"Muddy",
|
||||
"Mushy",
|
||||
"Mysterious",
|
||||
"Naughty",
|
||||
"Nervous",
|
||||
"Nice",
|
||||
"Nutty",
|
||||
"Obedient",
|
||||
"Obnoxious",
|
||||
"Odd",
|
||||
"Old-fashioned",
|
||||
"Open",
|
||||
"Outrageous",
|
||||
"Outstanding",
|
||||
"Panicky",
|
||||
"Perfect",
|
||||
"Plain",
|
||||
"Pleasant",
|
||||
"Poised",
|
||||
"Powerful",
|
||||
"Precious",
|
||||
"Prickly",
|
||||
"Proud",
|
||||
"Puzzled",
|
||||
"Quaint",
|
||||
"Real",
|
||||
"Relieved",
|
||||
"Scary",
|
||||
"Selfish",
|
||||
"Shiny",
|
||||
"Shy",
|
||||
"Silly",
|
||||
"Sleepy",
|
||||
"Smiling",
|
||||
"Smoggy",
|
||||
"Sparkling",
|
||||
"Splendid",
|
||||
"Spotless",
|
||||
"Stormy",
|
||||
"Strange",
|
||||
"Successful",
|
||||
"Super",
|
||||
"Talented",
|
||||
"Tame",
|
||||
"Tasty",
|
||||
"Tender",
|
||||
"Tense",
|
||||
"Terrible",
|
||||
"Thankful",
|
||||
"Thoughtful",
|
||||
"Thoughtless",
|
||||
"Tired",
|
||||
"Tough",
|
||||
"Uninterested",
|
||||
"Unsightly",
|
||||
"Unusual",
|
||||
"Upset",
|
||||
"Uptight",
|
||||
"Vast",
|
||||
"Victorious",
|
||||
"Vivacious",
|
||||
"Wandering",
|
||||
"Weary",
|
||||
"Wicked",
|
||||
"Wide-eyed",
|
||||
"Wild",
|
||||
"Witty",
|
||||
"Worried",
|
||||
"Worrisome",
|
||||
"Zany",
|
||||
"Zealous"
|
||||
]
|
||||
47
backend/src/res/crabs.json
Normal file
47
backend/src/res/crabs.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[
|
||||
"American Horseshoe",
|
||||
"Atlantic Ghost",
|
||||
"Baja Elbow",
|
||||
"Big Claw Purple Hermit",
|
||||
"Coldwater Mole",
|
||||
"Cuata Swim",
|
||||
"Deepwater Frog",
|
||||
"Dwarf Teardrop",
|
||||
"Elegant Hermit",
|
||||
"Flat Spider",
|
||||
"Ghost",
|
||||
"Globe Purse",
|
||||
"Green",
|
||||
"Halloween",
|
||||
"Harbor Spider",
|
||||
"Inflated Spider",
|
||||
"Left Clawed Hermit",
|
||||
"Lumpy Claw",
|
||||
"Magnificent Hermit",
|
||||
"Mexican Spider",
|
||||
"Mouthless Land",
|
||||
"Northern Lemon Rock",
|
||||
"Pacific Arrow",
|
||||
"Pacific Mole",
|
||||
"Paco Box",
|
||||
"Panamic Spider",
|
||||
"Purple Shore",
|
||||
"Red Rock",
|
||||
"Red Swim",
|
||||
"Red-leg Hermit",
|
||||
"Robust Swim",
|
||||
"Rough Swim",
|
||||
"Sand Swim",
|
||||
"Sally Lightfoot",
|
||||
"Shamed-face Box",
|
||||
"Shamed-face Heart Box",
|
||||
"Shell",
|
||||
"Small Arched Box",
|
||||
"Southern Kelp",
|
||||
"Spotted Box",
|
||||
"Striated Mole",
|
||||
"Striped Shore",
|
||||
"Tropical Mole",
|
||||
"Walking Rock",
|
||||
"Yellow Shore"
|
||||
]
|
||||
92
backend/src/routes/create_event.rs
Normal file
92
backend/src/routes/create_event.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use axum::{extract, Json};
|
||||
use common::{adaptor::Adaptor, event::Event};
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
payloads::{ApiResult, EventInput, EventResponse},
|
||||
State,
|
||||
};
|
||||
|
||||
pub async fn create_event<A: Adaptor>(
|
||||
extract::State(state): State<A>,
|
||||
Json(input): Json<EventInput>,
|
||||
) -> ApiResult<EventResponse, A> {
|
||||
let adaptor = &state.lock().await.adaptor;
|
||||
|
||||
// Get the current timestamp
|
||||
let now = chrono::offset::Utc::now();
|
||||
|
||||
// Generate a name if none provided
|
||||
let name = match input.name.trim() {
|
||||
"" => generate_name(),
|
||||
x => x.to_string(),
|
||||
};
|
||||
|
||||
// Generate an ID
|
||||
let mut id = generate_id(&name);
|
||||
|
||||
// Check the ID doesn't already exist
|
||||
while (adaptor
|
||||
.get_event(id.clone())
|
||||
.await
|
||||
.map_err(ApiError::AdaptorError)?)
|
||||
.is_some()
|
||||
{
|
||||
id = generate_id(&name);
|
||||
}
|
||||
|
||||
let event = adaptor
|
||||
.create_event(Event {
|
||||
id,
|
||||
name,
|
||||
created_at: now,
|
||||
visited_at: now,
|
||||
times: input.times,
|
||||
timezone: input.timezone,
|
||||
})
|
||||
.await
|
||||
.map_err(ApiError::AdaptorError)?;
|
||||
|
||||
// Update stats
|
||||
adaptor
|
||||
.increment_stat_event_count()
|
||||
.await
|
||||
.map_err(ApiError::AdaptorError)?;
|
||||
|
||||
Ok(Json(event.into()))
|
||||
}
|
||||
|
||||
// Generate a random name based on an adjective and a crab species
|
||||
fn generate_name() -> String {
|
||||
let adjectives: Vec<String> =
|
||||
serde_json::from_slice(include_bytes!("../res/adjectives.json")).unwrap();
|
||||
let crabs: Vec<String> = serde_json::from_slice(include_bytes!("../res/crabs.json")).unwrap();
|
||||
|
||||
format!(
|
||||
"{} {} Crab",
|
||||
adjectives.choose(&mut thread_rng()).unwrap(),
|
||||
crabs.choose(&mut thread_rng()).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
// Generate a slug for the crab fit
|
||||
fn generate_id(name: &str) -> String {
|
||||
let mut id = encode_name(name.to_string());
|
||||
if id.replace('-', "").is_empty() {
|
||||
id = encode_name(generate_name());
|
||||
}
|
||||
let number = thread_rng().gen_range(100000..=999999);
|
||||
format!("{}-{}", id, number)
|
||||
}
|
||||
|
||||
// Use punycode to encode the name
|
||||
fn encode_name(name: String) -> String {
|
||||
let pc = punycode::encode(&name.trim().to_lowercase())
|
||||
.unwrap_or(String::from(""))
|
||||
.trim()
|
||||
.replace(|c: char| !c.is_ascii_alphanumeric() && c != ' ', "");
|
||||
let re = Regex::new(r"\s+").unwrap();
|
||||
re.replace_all(&pc, "-").to_string()
|
||||
}
|
||||
34
backend/src/routes/get_event.rs
Normal file
34
backend/src/routes/get_event.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use axum::{
|
||||
extract::{self, Path},
|
||||
Json,
|
||||
};
|
||||
use common::adaptor::Adaptor;
|
||||
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
payloads::{ApiResult, EventResponse},
|
||||
State,
|
||||
};
|
||||
|
||||
pub async fn get_event<A: Adaptor>(
|
||||
extract::State(state): State<A>,
|
||||
Path(event_id): Path<String>,
|
||||
) -> ApiResult<EventResponse, A> {
|
||||
let adaptor = &state.lock().await.adaptor;
|
||||
|
||||
let event = adaptor
|
||||
.get_event(event_id)
|
||||
.await
|
||||
.map_err(ApiError::AdaptorError)?;
|
||||
|
||||
match event {
|
||||
Some(event) => Ok(Json(EventResponse {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
times: event.times,
|
||||
timezone: event.timezone,
|
||||
created: event.created_at.timestamp(),
|
||||
})),
|
||||
None => Err(ApiError::NotFound),
|
||||
}
|
||||
}
|
||||
20
backend/src/routes/get_stats.rs
Normal file
20
backend/src/routes/get_stats.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use axum::{extract, Json};
|
||||
use common::adaptor::Adaptor;
|
||||
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
payloads::{ApiResult, StatsResponse},
|
||||
State,
|
||||
};
|
||||
|
||||
pub async fn get_stats<A: Adaptor>(extract::State(state): State<A>) -> ApiResult<StatsResponse, A> {
|
||||
let adaptor = &state.lock().await.adaptor;
|
||||
|
||||
let stats = adaptor.get_stats().await.map_err(ApiError::AdaptorError)?;
|
||||
|
||||
Ok(Json(StatsResponse {
|
||||
event_count: stats.event_count,
|
||||
person_count: stats.person_count,
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
}))
|
||||
}
|
||||
8
backend/src/routes/mod.rs
Normal file
8
backend/src/routes/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
mod get_event;
|
||||
pub use get_event::get_event;
|
||||
|
||||
mod get_stats;
|
||||
pub use get_stats::get_stats;
|
||||
|
||||
mod create_event;
|
||||
pub use create_event::create_event;
|
||||
Loading…
Add table
Add a link
Reference in a new issue