Add basic poll support

This commit is contained in:
Joshua Goins 2025-01-18 14:19:46 -05:00
parent 71e6e2cbef
commit e256a9db25
2 changed files with 228 additions and 11 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target
.idea/
votes.json
polls.json
pollvotes.json

View file

@ -6,6 +6,9 @@ use axum::{
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Page {
@ -13,18 +16,50 @@ struct Page {
votes: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct VoteRecord {
pages: Vec<Page>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct PollChoice {
id: i32,
votes: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Poll {
id: i32,
choices: Vec<PollChoice>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct PollRecord {
polls: Vec<Poll>,
}
#[derive(Clone)]
struct AppState {
record: Arc<Mutex<VoteRecord>>,
poll_votes: Arc<Mutex<PollRecord>>
}
impl AppState {
fn save(&self) {
self.save_polls();
self.save_votes();
}
fn save_polls(&self) {
let st = self.poll_votes.lock().unwrap().to_owned();
serde_json::to_writer(
&std::fs::File::create("pollvotes.json").unwrap(),
&st,
)
.expect("failed to write poll votes!");
}
fn save_votes(&self) {
let st = self.record.lock().unwrap().to_owned();
serde_json::to_writer(
&std::fs::File::create("votes.json").unwrap(),
@ -32,6 +67,77 @@ impl AppState {
)
.expect("failed to write votes!");
}
fn vote_poll(&self, poll_id: i32, choice_id: i32) {
let mut poll_votes = self.poll_votes.lock().unwrap();
// Ensure we even have a valid poll id
let Some(poll_config) = AppState::get_poll_config_by_id(poll_id) else {
return;
};
let mut poll: Option<&mut Poll> = None;
for existing_poll in &mut poll_votes.polls {
if existing_poll.id == poll_id {
poll = Some(existing_poll);
break;
}
}
if poll == None {
let mut choices = vec![];
for choice in poll_config.choices {
choices.push(PollChoice {
id: choice.id,
votes: 0,
});
}
poll_votes.polls.push(Poll {
id: poll_id,
choices,
});
poll = poll_votes.polls.last_mut();
}
let poll = poll.unwrap();
for choice in &mut poll.choices {
if choice.id == choice_id {
choice.votes += 1;
break;
}
}
}
fn get_poll_config_by_id(id: i32) -> Option<PollConfig> {
if let Ok(data) = std::fs::read_to_string("polls.json") {
if let Ok(config) = serde_json::from_str::<PollsConfig>(&data) {
if let Some(current_poll) = config.current_poll {
let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect();
if !current_poll.is_empty() {
return Some(<&PollConfig>::clone(current_poll.first().unwrap()).clone());
}
}
}
}
None
}
fn get_poll_votes_by_poll_id(&self, poll_id: i32) -> Option<Poll> {
let mut poll_votes = self.poll_votes.lock().unwrap();
let Some(poll_config) = AppState::get_poll_config_by_id(poll_id) else {
return None;
};
for existing_poll in &poll_votes.polls {
if existing_poll.id == poll_id {
return Some(existing_poll.clone());
}
}
None
}
}
async fn view_votes(State(state): State<AppState>, Path(slug): Path<String>) -> Json<Page> {
@ -66,28 +172,137 @@ async fn leaderboard_submit_score(State(state): State<AppState>, Path(slug): Pat
.push(Page { slug, votes: 1 });
}
state.save();
state.save_votes();
}
#[derive(Debug, Clone, Deserialize)]
struct PollChoiceConfig {
id: i32,
name: String
}
#[derive(Debug, Clone, Deserialize)]
struct PollConfig {
id: i32,
title: String,
choices: Vec<PollChoiceConfig>
}
#[derive(Debug, Clone, Deserialize)]
struct PollsConfig {
current_poll: Option<i32>,
polls: Vec<PollConfig>,
}
#[derive(Debug, Clone, Serialize)]
struct PollChoiceResponse {
id: i32,
name: String,
votes: i32
}
#[derive(Debug, Clone, Serialize)]
struct PollResponse {
title: String,
choices: Vec<PollChoiceResponse>
}
async fn view_current_poll(State(state): State<AppState>) -> Response {
tracing::info!("Requesting current poll");
if let Ok(data) = std::fs::read_to_string("polls.json") {
if let Ok(config) = serde_json::from_str::<PollsConfig>(&data) {
if let Some(current_poll) = config.current_poll {
let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect();
if !current_poll.is_empty() {
if let Some(poll) = state.get_poll_votes_by_poll_id(current_poll[0].id) {
let poll_config = current_poll.first().unwrap();
let mut choices = vec![];
let find_vote_choice = |id: i32| {
for choice in &poll.choices {
if choice.id == id {
return Some(choice);
}
}
return None;
};
for choice in &poll_config.choices {
if let Some(vote_choice) = find_vote_choice(choice.id) {
choices.push(PollChoiceResponse {
id: choice.id,
name: choice.name.clone(),
votes: vote_choice.votes,
});
} else {
choices.push(PollChoiceResponse {
id: choice.id,
name: choice.name.clone(),
votes: 0,
});
}
}
return Json(PollResponse {
title: current_poll.first().unwrap().title.clone(),
choices
}).into_response();
}
}
}
}
}
(
StatusCode::NOT_FOUND,
"No poll is open right now!",
).into_response()
}
async fn vote_current_poll(State(state): State<AppState>, Path(id): Path<i32>) {
tracing::info!("Submitting vote for choice {id}");
if let Ok(data) = std::fs::read_to_string("polls.json") {
if let Ok(config) = serde_json::from_str::<PollsConfig>(&data) {
if let Some(current_poll) = config.current_poll {
let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect();
if !current_poll.is_empty() {
state.vote_poll(current_poll.first().unwrap().id, id);
state.save_polls();
}
}
}
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let initial_state = if let Ok(data) = std::fs::read_to_string("votes.json") {
AppState {
record: Arc::new(Mutex::new(
serde_json::from_str(&data).expect("Failed to parse"),
)),
}
let vote_record = if let Ok(data) = std::fs::read_to_string("votes.json") {
serde_json::from_str(&data).expect("Failed to parse votes data")
} else {
AppState {
record: Arc::new(Mutex::new(VoteRecord { pages: vec![] })),
}
VoteRecord::default()
};
let poll_record = if let Ok(data) = std::fs::read_to_string("pollvotes.json") {
serde_json::from_str(&data).expect("Failed to parse polls data")
} else {
PollRecord::default()
};
let initial_state = AppState {
record: Arc::new(Mutex::new(vote_record)),
poll_votes: Arc::new(Mutex::new(poll_record)),
};
let app = Router::new()
.route("/votes/{slug}", post(leaderboard_submit_score))
.route("/votes/{slug}", get(view_votes))
.route("/polls/current", get(view_current_poll))
.route("/polls/vote/{id}", post(vote_current_poll))
.with_state(initial_state.clone());
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));