diff --git a/src/v1/kcomebacks/filter/mod.rs b/src/v1/kcomebacks/filter/mod.rs new file mode 100644 index 0000000..797d9bc --- /dev/null +++ b/src/v1/kcomebacks/filter/mod.rs @@ -0,0 +1,384 @@ +use std::{collections::{HashMap, HashSet}, vec}; + +use chrono::NaiveDate; +use serde_json::{Value, json}; +use warp::Filter; + +use crate::{error_responses::{BadRequestError, InternalServerError}, v1::kcomebacks::{fetch_data, create_json_response, parse_item, Item as EntryItem}}; + +pub fn get_kcomebacks_filter_routes() -> impl warp::Filter + Clone { + warp::path("filter") + .and((warp::path("id").and(warp::get()).and(warp::query::>()).and_then(filter_id_handler)) + .or(warp::path("getall").and(warp::get()).and(warp::query::>()).and_then(filter_getall_handler)) + .or(warp::path("daterange").and(warp::get()).and(warp::query::>()).and_then(filter_daterange_handler)) + .or(warp::path("artist").and(warp::get()).and(warp::query::>()).and_then(filter_artist_handler)) + .or(warp::path("first").and(warp::get()).and_then(filter_first_handler)) + .or(warp::path("last").and(warp::get()).and_then(filter_last_handler)) + .or(warp::path("title").and(warp::get()).and(warp::query::>()).and_then(filter_title_handler)) + .or(warp::path("type").and(warp::get()).and(warp::query::>()).and_then(filter_type_handler)) + .or(warp::path("gettypes").and(warp::get()).and_then(filter_gettypes_handler)) + .or(warp::path("getinfo").and(warp::get().and_then(filter_getinfo_handler)))) +} + +async fn filter_id_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let id = params.get("id").unwrap_or(&"".to_string()).to_string(); + + // check if the parameters are valid + if !id.parse::().is_ok() || id.parse::().unwrap() < 0 || id.is_empty() || params.len() > 1 { + return Err(warp::reject::custom(BadRequestError)); + } + + let id = match id.parse::() { + Ok(id) => id, + Err(_) => 1, + }; + + // fetch the data + let data = fetch_data().await.unwrap(); + + let item = data[id as usize + 1].clone(); + + if item.is_null() { + return Err(warp::reject::custom(BadRequestError)); + } + + + // return the data + Ok(warp::reply::json(&create_json_response(vec![&parse_item(&item)], 1))) +} + +async fn filter_getall_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let limit = params.get("limit").unwrap_or(&"".to_string()).to_string(); + let offset = params.get("offset").unwrap_or(&"".to_string()).to_string(); + + // check if the parameters are valid + if params.len() > 2 + || !limit.parse::().is_ok() + || !offset.parse::().is_ok() + || limit.parse::().unwrap() < 0 + || limit.parse::().unwrap() > 50 + || offset.parse::().unwrap() < 0 + || limit.is_empty() + || offset.is_empty() + { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let filtered_data = match data { + Value::Array(items) => { + items + .iter() + .filter_map(|item| serde_json::from_value::(item.clone()).ok()) + .collect() + } + _ => Vec::new(), + }; + + // get the total number of results + let total_results = filtered_data.len(); + + // apply the limit and offset + let filtered_data = filtered_data.iter().skip(offset.parse::().unwrap()).take(limit.parse::().unwrap()).collect::>(); + + // return the data + Ok(warp::reply::json(&create_json_response(filtered_data, total_results))) +} + +pub async fn filter_daterange_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let start = params.get("start").unwrap_or(&"".to_string()).to_string(); + let end = params.get("end").unwrap_or(&"".to_string()).to_string(); + let limit = params.get("limit").unwrap_or(&"".to_string()).to_string(); + let offset = params.get("offset").unwrap_or(&"".to_string()).to_string(); + + // Parse the start and end dates into NaiveDate + let start_date_parsed = NaiveDate::parse_from_str(&start, "%Y-%m-%d"); + let end_date_parsed = NaiveDate::parse_from_str(&end, "%Y-%m-%d"); + + // check if the parameters are valid + if !start_date_parsed.is_ok() + || !end_date_parsed.is_ok() + || start_date_parsed.unwrap() > end_date_parsed.unwrap() + || start.is_empty() + || end.is_empty() + || params.len() > 4 + || !limit.parse::().is_ok() + || !offset.parse::().is_ok() + || limit.parse::().unwrap() < 0 + || limit.parse::().unwrap() > 50 + || offset.parse::().unwrap() < 0 + || limit.is_empty() + || offset.is_empty() + { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let filtered_data = match (start_date_parsed, end_date_parsed) { + (Ok(start), Ok(end)) => { + match data { + Value::Array(items) => { + items + .iter() + .filter_map(|item| serde_json::from_value::(item.clone()).ok()) + .filter(|item| { + if let Ok(item_date) = NaiveDate::parse_from_str(&item.date, "%Y-%m-%d") { + item_date >= start && item_date <= end + } else { + false + } + }) + .collect() + } + _ => Vec::new(), + } + } + _ => Vec::new(), + }; + + // get the total number of results + let total_results = filtered_data.len(); + + // apply the limit and offset + let filtered_data = filtered_data.iter().skip(offset.parse::().unwrap()).take(limit.parse::().unwrap()).collect::>(); + + // return the data + Ok(warp::reply::json(&create_json_response(filtered_data, total_results))) +} + +async fn filter_artist_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let artist = params.get("artist").unwrap_or(&"".to_string()).to_string(); + let limit = params.get("limit").unwrap_or(&"".to_string()).to_string(); + let offset = params.get("offset").unwrap_or(&"".to_string()).to_string(); + + // check if the parameters are valid + if artist.is_empty() + || params.len() > 3 + || !limit.parse::().is_ok() + || !offset.parse::().is_ok() + || limit.parse::().unwrap() < 0 + || limit.parse::().unwrap() > 50 + || offset.parse::().unwrap() < 0 + || limit.is_empty() || offset.is_empty() + { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let filtered_data = match data { + Value::Array(items) => { + items + .iter() + .filter_map(|item| { + // Attempt to deserialize the item as an Item + serde_json::from_value::(item.clone()).ok() + }) + .filter(|item| item.artist.to_lowercase().contains(artist.to_lowercase().as_str())) + .collect() + } + _ => Vec::new(), + }; + + // get the total number of results + let total_results = filtered_data.len(); + + // apply the limit and offset + let filtered_data = filtered_data.iter().skip(offset.parse::().unwrap()).take(limit.parse::().unwrap()).collect::>(); + + // return the data + Ok(warp::reply::json(&create_json_response(filtered_data, total_results))) +} + +async fn filter_first_handler() -> Result { + + // fetch the data + let item = fetch_data().await.unwrap()[1].clone(); + + if item.is_null() { + return Err(warp::reject::custom(InternalServerError)); + } + + // return the data + Ok(warp::reply::json(&create_json_response(vec![&parse_item(&item)], 1))) +} + +async fn filter_last_handler() -> Result { + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let item = data[data.as_array().unwrap().len() - 1].clone(); + + // return the data + Ok(warp::reply::json(&create_json_response(vec![&parse_item(&item)], 1))) +} + +async fn filter_title_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let title = params.get("title").unwrap_or(&"".to_string()).to_string(); + let limit = params.get("limit").unwrap_or(&"".to_string()).to_string(); + let offset = params.get("offset").unwrap_or(&"".to_string()).to_string(); + + // check if the parameters are valid + if title.is_empty() + || params.len() > 3 + || !limit.parse::().is_ok() + || !offset.parse::().is_ok() + || limit.parse::().unwrap() < 0 + || limit.parse::().unwrap() > 50 + || offset.parse::().unwrap() < 0 + || limit.is_empty() + || offset.is_empty() + { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let filtered_data = match data { + Value::Array(items) => { + items + .iter() + .filter_map(|item| { + // Attempt to deserialize the item as an Item + serde_json::from_value::(item.clone()).ok() + }) + .filter(|item| item.title.to_lowercase().contains(title.to_lowercase().as_str())) + .collect() + } + _ => Vec::new(), + }; + + // get the total number of results + let total_results = filtered_data.len(); + + // apply the limit and offset + let filtered_data = filtered_data.iter().skip(offset.parse::().unwrap()).take(limit.parse::().unwrap()).collect::>(); + + // return the data + Ok(warp::reply::json(&create_json_response(filtered_data, total_results))) +} + +async fn filter_type_handler(params: HashMap) -> Result { + // Access the parameters from the HashMap + let type_ = params.get("type").unwrap_or(&"".to_string()).to_string(); + let limit = params.get("limit").unwrap_or(&"".to_string()).to_string(); + let offset = params.get("offset").unwrap_or(&"".to_string()).to_string(); + + // check if the parameters are valid + if type_.is_empty() + || params.len() > 3 + || !limit.parse::().is_ok() + || !offset.parse::().is_ok() + || limit.parse::().unwrap() < 0 + || limit.parse::().unwrap() > 50 + || offset.parse::().unwrap() < 0 + || limit.is_empty() + || offset.is_empty() + { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let filtered_data = match data { + Value::Array(items) => { + items + .iter() + .filter_map(|item| { + serde_json::from_value::(item.clone()).ok() + }) + .filter(|item| item.types.iter().any(|t| t == type_.as_str())) + .collect() + } + _ => Vec::new(), + }; + + // get the total number of results + let total_results = filtered_data.len(); + + // apply the limit and offset + let filtered_data = filtered_data.iter().skip(offset.parse::().unwrap()).take(limit.parse::().unwrap()).collect::>(); + + // return the data + Ok(warp::reply::json(&create_json_response(filtered_data, total_results))) +} + +async fn filter_gettypes_handler() -> Result { + println!("Received parameters - parameters: gettypes"); + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + let mut types: Vec = Vec::new(); + for i in 1..data.as_array().unwrap().len() { + for j in 0..data[i]["types"].as_array().unwrap().len() { + types.push(data[i]["types"][j].as_str().unwrap().to_string()); + } + } + + // remove duplicates + types.sort(); + types.dedup(); + + // get the total number of results + let total_results = types.len(); + + // json response + let json_response = json!({ + "results": types, + "total_results": total_results, + }); + + // return the data + Ok(warp::reply::json(&json_response)) +} + +async fn filter_getinfo_handler() -> Result { + + // fetch the data + let data = fetch_data().await.unwrap(); + + // get the number of items + let num_items = data.as_array().unwrap().len() - 1; + + // get the number of artists + let mut artists: HashSet = HashSet::new(); + + for i in 1..data.as_array().unwrap().len() { + artists.insert(data[i]["artist"].as_str().unwrap().to_string()); + } + + let num_artists = artists.len(); + + // get the start and end dates + let start_date = data[1]["date"].as_str().unwrap().to_string(); + let end_date = data[num_items]["date"].as_str().unwrap().to_string(); + + // return the data + Ok(warp::reply::json(&serde_json::json!({ + "num_items": num_items, + "num_artists": num_artists, + "start_date": start_date, + "end_date": end_date + }))) +} \ No newline at end of file diff --git a/src/v1/kcomebacks/mod.rs b/src/v1/kcomebacks/mod.rs index d81e421..22926f2 100644 --- a/src/v1/kcomebacks/mod.rs +++ b/src/v1/kcomebacks/mod.rs @@ -1,28 +1,29 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; use warp::Filter; use reqwest::Error; +mod filter; +mod upcoming; + +use filter::get_kcomebacks_filter_routes; +use upcoming::get_kcomebacks_upcoming_routes; +use crate::error_responses::{InternalServerError, BadRequestError, NotFoundError}; + pub fn get_kcomebacks_routes() -> impl warp::Filter + Clone { warp::path("v1").and(warp::path("kcomebacks")) - // - /v1/kcomebacks/last_update - // - /v1/kcomebacks/start_update with token - // - /v1/kcomebacks/upcoming/today?limit={0-50}&offset={n} - // - /v1/kcomebacks/upcoming/week?limit={0-50}&offset={n} - // - /v1/kcomebacks/upcoming/month?limit={0-50}&offset={n} - // - /v1/kcomebacks/filter/id?id={n} - // - /v1/kcomebacks/filter/daterange?start={date: YYYY-MM-DD}&end={date: YYYY-MM-DD}&limit={0-50}&offset={n} - // - /v1/kcomebacks/filter/artist?artist={artist}&limit={0-50}&offset={n} - // - /v1/kcomebacks/filter/first - // - /v1/kcomebacks/filter/last - // - /v1/kcomebacks/filter/title?title={title}&limit={0-50}&offset={n} - // - /v1/kcomebacks/filter/type?type={type}&limit={0-50}&offset={n} - // - /v1/kcomebacks/filter/gettypes - .and(warp::path("last_update").and(warp::get()).and_then(last_update_handler) - .or(warp::path("start_update").map(|| "Not implemented yet"))) + .and(warp::path("last_update").and(warp::get()).and_then(last_update) + .or(warp::path("start_update").map(|| "Not implemented yet")) + .or(get_kcomebacks_upcoming_routes()) + .or(get_kcomebacks_filter_routes()) + ) } // get json data from https://cdn.jonasjones.dev/api/kcomebacks/rkpop_data.json -async fn fetch_data() -> Result { +pub async fn fetch_data() -> Result { let url = "https://cdn.jonasjones.dev/api/kcomebacks/rkpop_data.json"; let response = reqwest::get(url).await?; @@ -36,22 +37,46 @@ async fn fetch_data() -> Result { } } -async fn last_update_handler() -> Result { +async fn last_update() -> Result { - match last_update().await { - Ok(last_update_value) => Ok(warp::reply::json(&last_update_value)), - Err(_) => { - #[derive(Debug)] - struct InternalServerError; + // get the value of last_update of the first element of the json that fetch_data() returns + let last_update_value = fetch_data().await.unwrap()[0]["last_update"].clone(); - impl warp::reject::Reject for InternalServerError {} - Err(warp::reject::custom(InternalServerError)) - } + // get the value from last_update_value and return it as a json if it's Ok, otherwise return an InternalServerError + match last_update_value { + serde_json::Value::String(last_update) => Ok(warp::reply::json(&last_update)), + _ => Err(warp::reject::custom(InternalServerError)), } } -async fn last_update() -> Result { - // get the value of last_update of the first element of the json that fetch_data() returns - let last_update_value = fetch_data().await?.get(0).unwrap().get("last_update").unwrap().clone(); - return Ok(last_update_value); +#[derive(Debug, Deserialize, Serialize)] +pub struct Item { + artist: String, + date: String, + #[serde(default)] + links: Vec, + time: String, + title: String, + types: Vec, +} + +pub fn create_json_response(items: Vec<&Item>, total_results: usize) -> Value { + // Serialize the vector of items to a JSON array + let results_array: Vec = items.into_iter().map(|item| json!(item)).collect(); + + // Build the final JSON object with "results" and "total_results" fields + let json_response = json!({ + "results": results_array, + "total_results": total_results, + }); + + json_response +} + +pub fn parse_item(item: &Value) -> Item { + // Parse the item into a struct + let item: Item = serde_json::from_value(item.clone()).unwrap(); + + // Return the parsed item + item } diff --git a/src/v1/kcomebacks/upcoming/mod.rs b/src/v1/kcomebacks/upcoming/mod.rs index bde5116..813f7bc 100644 --- a/src/v1/kcomebacks/upcoming/mod.rs +++ b/src/v1/kcomebacks/upcoming/mod.rs @@ -1,8 +1,51 @@ +use std::{collections::HashMap, ops::Add}; +use warp::Filter; + +use crate::{v1::kcomebacks::filter::filter_daterange_handler, error_responses::BadRequestError}; pub fn get_kcomebacks_upcoming_routes() -> impl warp::Filter + Clone { warp::path("upcoming") - .and(warp::path("today").and(warp::get()).and_then(upcoming_today_handler)) - .or(warp::path("week").and(warp::get()).and_then(upcoming_week_handler)) - .or(warp::path("month").and(warp::get()).and_then(upcoming_month_handler)) + .and((warp::path("today").and(warp::get()).and(warp::query::>()).and_then(upcoming_today_handler)) + .or(warp::path("week").and(warp::get()).and(warp::query::>()).and_then(upcoming_week_handler)) + .or(warp::path("month").and(warp::get()).and(warp::query::>()).and_then(upcoming_month_handler))) +} + +async fn upcoming_today_handler(mut params: HashMap) -> Result { + + let today = chrono::Local::now().format("%Y-%m-%d").to_string(); + let start = today.clone(); + let end = today.clone(); + + // add start and end to the params + params.insert("start".to_string(), start); + params.insert("end".to_string(), end); + + filter_daterange_handler(params).await +} + +async fn upcoming_week_handler(mut params: HashMap) -> Result { + + let today = chrono::Local::now().format("%Y-%m-%d").to_string(); + let start = today.clone(); + let end = chrono::Local::now().add(chrono::Duration::days(7)).format("%Y-%m-%d").to_string(); + + // add start and end to the params + params.insert("start".to_string(), start); + params.insert("end".to_string(), end); + + filter_daterange_handler(params).await +} + +async fn upcoming_month_handler(mut params: HashMap) -> Result { + + let today = chrono::Local::now().format("%Y-%m-%d").to_string(); + let start = today.clone(); + let end = chrono::Local::now().add(chrono::Duration::days(30)).format("%Y-%m-%d").to_string(); + + // add start and end to the params + params.insert("start".to_string(), start); + params.insert("end".to_string(), end); + + filter_daterange_handler(params).await } \ No newline at end of file