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

81
backend/Cargo.lock generated
View file

@ -30,6 +30,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
@ -492,6 +501,14 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"async-trait",
"chrono",
]
[[package]]
name = "concurrent-queue"
version = "2.2.0"
@ -537,11 +554,18 @@ name = "crabfit_backend"
version = "1.1.0"
dependencies = [
"axum",
"data",
"chrono",
"common",
"dotenv",
"punycode",
"rand",
"regex",
"serde",
"serde_json",
"sql-adaptor",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
@ -637,14 +661,6 @@ dependencies = [
"syn 2.0.15",
]
[[package]]
name = "data"
version = "0.1.0"
dependencies = [
"async-trait",
"chrono",
]
[[package]]
name = "der"
version = "0.5.1"
@ -1305,6 +1321,16 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
@ -1453,6 +1479,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.1.0"
@ -1649,6 +1681,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "punycode"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe"
[[package]]
name = "quote"
version = "1.0.27"
@ -1723,6 +1761,8 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.1",
]
@ -2204,7 +2244,7 @@ dependencies = [
"async-std",
"async-trait",
"chrono",
"data",
"common",
"sea-orm",
"sea-orm-migration",
"serde",
@ -2596,6 +2636,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
@ -2605,12 +2657,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -2684,6 +2739,12 @@ dependencies = [
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"

View file

@ -4,12 +4,19 @@ version = "1.1.0"
edition = "2021"
[workspace]
members = ["data", "adaptors/*"]
members = ["common", "adaptors/*"]
[dependencies]
axum = "0.6.18"
serde = { version = "1.0.162", features = ["derive"] }
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] }
data = { path = "data" }
common = { path = "common" }
sql-adaptor = { path = "adaptors/sql" }
dotenv = "0.15.0"
serde_json = "1.0.96"
rand = "0.8.5"
punycode = "0.4.1"
regex = "1.8.1"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
chrono = "0.4.24"

View file

@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
async-trait = "0.1.68"
data = { path = "../../data" }
common = { path = "../../common" }
sea-orm = { version = "0.11.3", features = [ "macros", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-native-tls" ] }
serde = { version = "1.0.162", features = [ "derive" ] }
async-std = { version = "1", features = ["attributes", "tokio1"] }

View file

@ -1,7 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use chrono::{DateTime as ChronoDateTime, Utc};
use data::event::Event;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@ -11,6 +9,7 @@ pub struct Model {
pub id: String,
pub name: String,
pub created_at: DateTime,
pub visited_at: DateTime,
pub times: Json,
pub timezone: String,
}
@ -28,15 +27,3 @@ impl Related<super::person::Entity> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Event {
fn from(value: Model) -> Self {
Self {
id: value.id,
name: value.name,
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
times: serde_json::from_value(value.times).unwrap_or(vec![]),
timezone: value.timezone,
}
}
}

View file

@ -1,7 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use chrono::{DateTime as ChronoDateTime, Utc};
use data::person::Person;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@ -35,14 +33,3 @@ impl Related<super::event::Entity> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Person {
fn from(value: Model) -> Self {
Self {
name: value.name,
password_hash: value.password_hash,
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
availability: serde_json::from_value(value.availability).unwrap_or(vec![]),
}
}
}

View file

@ -1,6 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use data::stats::Stats;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@ -16,12 +15,3 @@ pub struct Model {
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Stats {
fn from(value: Model) -> Self {
Self {
event_count: value.event_count,
person_count: value.person_count,
}
}
}

View file

@ -1,7 +1,8 @@
use std::{env, error::Error};
use async_trait::async_trait;
use data::{
use chrono::{DateTime as ChronoDateTime, Utc};
use common::{
adaptor::Adaptor,
event::{Event, EventDeletion},
person::Person,
@ -10,54 +11,48 @@ use data::{
use entity::{event, person, stats};
use migration::{Migrator, MigratorTrait};
use sea_orm::{
strum::Display,
ActiveModelTrait,
ActiveValue::{NotSet, Set},
ColumnTrait, Database, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter,
TransactionTrait, TryIntoModel,
TransactionError, TransactionTrait, TryIntoModel,
};
use serde_json::json;
mod entity;
mod migration;
pub struct PostgresAdaptor {
pub struct SqlAdaptor {
db: DatabaseConnection,
}
#[async_trait]
impl Adaptor for PostgresAdaptor {
async fn new() -> Self {
let connection_string = env::var("DATABASE_URL").unwrap();
impl Adaptor for SqlAdaptor {
type Error = SqlAdaptorError;
// Connect to the database
let db = Database::connect(&connection_string).await.unwrap();
println!("Connected to database at {}", connection_string);
// Setup tables
Migrator::up(&db, None).await.unwrap();
Self { db }
async fn get_stats(&self) -> Result<Stats, Self::Error> {
let stats_row = get_stats_row(&self.db).await?;
Ok(Stats {
event_count: stats_row.event_count.unwrap(),
person_count: stats_row.person_count.unwrap(),
})
}
async fn get_stats(&self) -> Result<Stats, Box<dyn Error>> {
Ok(get_stats_row(&self.db).await?.try_into_model()?.into())
}
async fn increment_stat_event_count(&self) -> Result<i32, Box<dyn Error>> {
async fn increment_stat_event_count(&self) -> Result<i32, Self::Error> {
let mut current_stats = get_stats_row(&self.db).await?;
current_stats.event_count = Set(current_stats.event_count.unwrap() + 1);
Ok(current_stats.save(&self.db).await?.event_count.unwrap())
}
async fn increment_stat_person_count(&self) -> Result<i32, Box<dyn Error>> {
async fn increment_stat_person_count(&self) -> Result<i32, Self::Error> {
let mut current_stats = get_stats_row(&self.db).await?;
current_stats.person_count = Set(current_stats.person_count.unwrap() + 1);
Ok(current_stats.save(&self.db).await?.person_count.unwrap())
}
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Box<dyn Error>> {
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error> {
// TODO: optimize into one query
let event_row = event::Entity::find_by_id(event_id).one(&self.db).await?;
@ -75,11 +70,7 @@ impl Adaptor for PostgresAdaptor {
})
}
async fn upsert_person(
&self,
event_id: String,
person: Person,
) -> Result<Person, Box<dyn Error>> {
async fn upsert_person(&self, event_id: String, person: Person) -> Result<Person, Self::Error> {
Ok(person::ActiveModel {
name: Set(person.name),
password_hash: Set(person.password_hash),
@ -93,28 +84,29 @@ impl Adaptor for PostgresAdaptor {
.into())
}
async fn get_event(&self, id: String) -> Result<Option<Event>, Box<dyn Error>> {
async fn get_event(&self, id: String) -> Result<Option<Event>, Self::Error> {
Ok(event::Entity::find_by_id(id)
.one(&self.db)
.await?
.map(|model| model.into()))
}
async fn create_event(&self, event: Event) -> Result<Event, Box<dyn Error>> {
async fn create_event(&self, event: Event) -> Result<Event, Self::Error> {
Ok(event::ActiveModel {
id: Set(event.id),
name: Set(event.name),
created_at: Set(event.created_at.naive_utc()),
visited_at: Set(event.visited_at.naive_utc()),
times: Set(serde_json::to_value(event.times).unwrap_or(json!([]))),
timezone: Set(event.timezone),
}
.save(&self.db)
.insert(&self.db)
.await?
.try_into_model()?
.into())
}
async fn delete_event(&self, id: String) -> Result<EventDeletion, Box<dyn Error>> {
async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error> {
let event_id = id.clone();
let person_count = self
.db
@ -141,6 +133,7 @@ impl Adaptor for PostgresAdaptor {
// Get the current stats as an ActiveModel
async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, DbErr> {
let current_stats = stats::Entity::find().one(db).await?;
Ok(match current_stats {
Some(model) => model.into(),
None => stats::ActiveModel {
@ -150,3 +143,61 @@ async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, Db
},
})
}
impl SqlAdaptor {
pub async fn new() -> Self {
let connection_string = env::var("DATABASE_URL").unwrap();
// Connect to the database
let db = Database::connect(&connection_string).await.unwrap();
println!("Connected to database at {}", connection_string);
// Setup tables
Migrator::up(&db, None).await.unwrap();
Self { db }
}
}
impl From<event::Model> for Event {
fn from(value: event::Model) -> Self {
Self {
id: value.id,
name: value.name,
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
visited_at: ChronoDateTime::<Utc>::from_utc(value.visited_at, Utc),
times: serde_json::from_value(value.times).unwrap_or(vec![]),
timezone: value.timezone,
}
}
}
impl From<person::Model> for Person {
fn from(value: person::Model) -> Self {
Self {
name: value.name,
password_hash: value.password_hash,
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
availability: serde_json::from_value(value.availability).unwrap_or(vec![]),
}
}
}
#[derive(Display, Debug)]
pub enum SqlAdaptorError {
DbErr(DbErr),
TransactionError(TransactionError<DbErr>),
}
impl Error for SqlAdaptorError {}
impl From<DbErr> for SqlAdaptorError {
fn from(value: DbErr) -> Self {
Self::DbErr(value)
}
}
impl From<TransactionError<DbErr>> for SqlAdaptorError {
fn from(value: TransactionError<DbErr>) -> Self {
Self::TransactionError(value)
}
}

View file

@ -36,6 +36,7 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Event::Id).string().not_null().primary_key())
.col(ColumnDef::new(Event::Name).string().not_null())
.col(ColumnDef::new(Event::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Event::VisitedAt).timestamp().not_null())
.col(ColumnDef::new(Event::Times).json().not_null())
.col(ColumnDef::new(Event::Timezone).string().not_null())
.to_owned(),
@ -105,6 +106,7 @@ enum Event {
Id,
Name,
CreatedAt,
VisitedAt,
Times,
Timezone,
}

View file

@ -1,12 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod setup_tables;
mod m01_setup_tables;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(setup_tables::Migration)]
vec![Box::new(m01_setup_tables::Migration)]
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "common"
description = "Shared structs and traits for the data storage and transfer of Crab Fit"
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1.68"
chrono = "0.4.24"

View file

@ -0,0 +1,29 @@
use std::error::Error;
use async_trait::async_trait;
use crate::{
event::{Event, EventDeletion},
person::Person,
stats::Stats,
};
/// Data storage adaptor, all methods on an adaptor can return an error if
/// something goes wrong, or potentially None if the data requested was not found.
#[async_trait]
pub trait Adaptor: Send + Sync {
type Error: Error;
async fn get_stats(&self) -> Result<Stats, Self::Error>;
async fn increment_stat_event_count(&self) -> Result<i32, Self::Error>;
async fn increment_stat_person_count(&self) -> Result<i32, Self::Error>;
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error>;
async fn upsert_person(&self, event_id: String, person: Person) -> Result<Person, Self::Error>;
async fn get_event(&self, id: String) -> Result<Option<Event>, Self::Error>;
async fn create_event(&self, event: Event) -> Result<Event, Self::Error>;
/// Delete an event as well as all related people
async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error>;
}

View file

@ -4,6 +4,7 @@ pub struct Event {
pub id: String,
pub name: String,
pub created_at: DateTime<Utc>,
pub visited_at: DateTime<Utc>,
pub times: Vec<String>,
pub timezone: String,
}

View file

@ -1,9 +0,0 @@
[package]
name = "data"
description = "Structs and traits for the data storage and transfer of Crab Fit"
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1.68"
chrono = "0.4.24"

View file

@ -1,37 +0,0 @@
use std::error::Error;
use async_trait::async_trait;
use crate::{
event::{Event, EventDeletion},
person::Person,
stats::Stats,
};
/// Data storage adaptor, all methods on an adaptor can return an error if
/// something goes wrong, or potentially None if the data requested was not found.
#[async_trait]
pub trait Adaptor {
/// Creates a new adaptor and performs all setup required
///
/// # Panics
/// If an error occurs while setting up the adaptor
async fn new() -> Self;
async fn get_stats(&self) -> Result<Stats, Box<dyn Error>>;
async fn increment_stat_event_count(&self) -> Result<i32, Box<dyn Error>>;
async fn increment_stat_person_count(&self) -> Result<i32, Box<dyn Error>>;
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Box<dyn Error>>;
async fn upsert_person(
&self,
event_id: String,
person: Person,
) -> Result<Person, Box<dyn Error>>;
async fn get_event(&self, id: String) -> Result<Option<Event>, Box<dyn Error>>;
async fn create_event(&self, event: Event) -> Result<Event, Box<dyn Error>>;
/// Delete an event as well as all related people
async fn delete_event(&self, id: String) -> Result<EventDeletion, Box<dyn Error>>;
}

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;