Set up some routes using new Rust API

This commit is contained in:
Ben Grant 2023-05-13 13:46:23 +10:00
parent fdc58b428b
commit fc8e2a4360
26 changed files with 703 additions and 134 deletions

22
backend/src/errors.rs Normal file
View 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(),
}
}
}

View file

@ -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
View 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,
}

View 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"
]

View 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"
]

View 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()
}

View 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),
}
}

View 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(),
}))
}

View 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;