Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 41 additions & 21 deletions backend/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,59 @@
use serde::Deserialize;
use std::{collections::HashMap, error::Error, println, string::String, vec::Vec};
use core::prelude::v1::derive;
use std::{collections::HashMap, error::Error, fs, iter::{IntoIterator, Iterator}, option::Option::{self, None, Some}, path::PathBuf, string::String, vec::Vec};

#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Problem {
pub id: String,
pub contest_id: String,
pub name: String,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct ProblemModelRaw {
pub difficulty: Option<i32>,
}

#[derive(Debug, Clone)]
pub struct ProblemModel {
pub difficulty: Option<i32>,
pub difficulty: Option<f64>,
}

fn adjust_difficulty(difficulty: Option<i32>) -> Option<f64> {
match difficulty {
Some(d) if d >= 400 => Some(d as f64),
Some(d) => Some(400.0 / f64::exp(1.0 - d as f64 / 400.0)),
None => None,
}
}

pub async fn fetch_problem() -> Result<(Vec<Problem>, HashMap<String, ProblemModel>), Box<dyn Error + Send + Sync>> {
// let problems_url = "https://kenkoooo.com/atcoder/resources/problems.json";
// let problem_models_url = "https://kenkoooo.com/atcoder/resources/problem-models.json";
let problems_url = "https://github.com/Twil3akine/atcoder-random-picker/blob/master/.gitconfig";
let problem_models_url = "https://github.com/Twil3akine/atcoder-random-picker/blob/master/.gitconfig";
// カレントディレクトリ基準で src/data を指す
let mut problems_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
problems_path.push("src/data/problems.json");

let client = reqwest::Client::builder()
.user_agent("atcoder-random-picker/0.1 (twil3; contact: twil3akine@gmail.com)")
.build()?;
let mut problem_models_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
problem_models_path.push("src/data/problem-models.json");

// 問題一覧
let problems_text = client.get(problems_url).send().await?.text().await?;
// ファイル読み込み
let problems_text = fs::read_to_string(problems_path)?;
let problems: Vec<Problem> = serde_json::from_str(&problems_text)?;

// 問題モデル
let problem_models_text = client.get(problem_models_url).send().await?.text().await?;
let problem_models: HashMap<String, ProblemModel> = serde_json::from_str(&problem_models_text)?;

for (id, model) in &problem_models {
println!("{}: {:?}", id, model.difficulty);
}
let problem_models_text = fs::read_to_string(problem_models_path)?;
let raw_models: HashMap<String, ProblemModelRaw> = serde_json::from_str(&problem_models_text)?;

// 補正式を適用させる
let problem_models: HashMap<String, ProblemModel> = raw_models
.into_iter()
.map(|(id, raw)| {
(
id,
ProblemModel {
difficulty: adjust_difficulty(raw.difficulty),
},
)
})
.collect();

Ok((problems, problem_models))
}
}
3 changes: 2 additions & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod routing;
pub mod api;

pub use routing::router;
pub use routing::{router, AppState};
pub use api::fetch_problem;
46 changes: 27 additions & 19 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
use backend::{api, router, AppState};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Server};
use hyper::Server;
use std::net::SocketAddr;
use std::convert::Infallible;
use std::convert::{From, Infallible};
use std::{println};
use std::result::Result::Err;

use backend::{api, router};
use std::result::Result::{Err, Ok};
use std::sync::Arc;

#[tokio::main]
async fn main() {
match api::fetch_problem().await {
Ok(_ps) => println!("Successed to fetch problems"),
Err(e) => eprintln!("Failed to failed problems: {}", e),
}

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
Ok((problems, problem_models)) => {
println!("Succeeded to fetch problems");

let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(router))
});
let state = Arc::new(AppState { problems, problem_models });
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(move |_conn| {
let state = state.clone();
async move {
Ok::<_, Infallible>(service_fn(move |req| {
let state = state.clone();
router(req, state)
}))
}
});

let server = Server::bind(&addr).serve(make_svc);
let server = Server::bind(&addr).serve(make_svc);

println!("Running server on : http://{}.", addr);
println!("Running server on http://{}.", addr);

if let Err(e) = server.await {
eprintln!("error on {}.", e);
} else {
println!("error was shut down.");
if let Err(e) = server.await {
eprintln!("error on {}.", e);
} else {
println!("server shut down.");
}
}
Err(e) => eprintln!("Failed to fetch problems: {}", e),
}
}
86 changes: 57 additions & 29 deletions backend/src/routing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
use hyper::{Body, Request, Response, StatusCode, header};
use std::convert::{From, Infallible};
use std::collections::HashMap;
use std::format;
use std::option::Option::None;
use std::result::Result::Ok;
use url::form_urlencoded;
use std::{collections::HashMap};
use std::convert::{From, Infallible};
use std::sync::Arc;
use chrono::{DateTime, Local};
use rand::Rng;
use rand;
use rand::seq::IteratorRandom;
use serde::Serialize;

async fn get_parameter(req: Request<Body>) -> HashMap<String, String> {
let query = req.uri().query().unwrap_or("");
let params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect();
use crate::api::{Problem, ProblemModel};

params
#[derive(Clone)]
pub struct AppState {
pub problems: Vec<Problem>,
pub problem_models: HashMap<String, ProblemModel>,
}

fn get_random_number(under: u32, over: u32) -> u32 {
let mut rng = rand::thread_rng();
rng.gen_range(under..=over)
#[derive(Serialize)]
struct ProblemResponse {
id: String,
contest_id: String,
name: String,
difficulty: Option<f64>,
}

async fn get_parameter(req: &Request<Body>) -> HashMap<String, String> {
let query = req.uri().query().unwrap_or("");
url::form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect()
}

fn log(now: DateTime<Local>, method: &str, path: &str, status: StatusCode) {
Expand All @@ -42,9 +52,8 @@ fn with_cors_headers(mut res: Response<Body>) -> Response<Body> {
res
}

pub async fn router(req: Request<Body>) -> Result<Response<Body>, Infallible> {
pub async fn router(req: Request<Body>, state: Arc<AppState>) -> Result<Response<Body>, Infallible> {
let now= Local::now();

let path = req.uri().path().to_string();
let method = req.method().to_string();

Expand All @@ -55,27 +64,46 @@ pub async fn router(req: Request<Body>) -> Result<Response<Body>, Infallible> {
}

(&hyper::Method::GET, "/") => {
let params: HashMap<String, String> = get_parameter(req).await;
let params: HashMap<String, String> = get_parameter(&req).await;

let under: u32 = match params.get("under").and_then(|s| s.parse().ok()) {
Some(v) => v,
None => 0,
};

let over: u32 = match params.get("over").and_then(|s| s.parse().ok()) {
Some(v) => v,
None => 3854,
};
let under: f64 = params.get("under").and_then(|s| s.parse().ok()).unwrap_or(0.0);
let over: f64 = params.get("over").and_then(|s| s.parse().ok()).unwrap_or(3854.0);

if under > over {
let mut bad_request = Response::new(Body::from("'under' cannot bt greater than 'over'."));
*bad_request.status_mut() = StatusCode::BAD_REQUEST;
return Ok(bad_request);
}

let random_number = get_random_number(under, over);
let response_body = format!("{}", random_number);
Ok(with_cors_headers(Response::new(Body::from(response_body))))
let mut rng = rand::thread_rng();
let selected = state.problems.iter().filter_map(|p| {
state.problem_models.get(&p.id).and_then(|m| {
m.difficulty.and_then(|diff| {
if under <= diff && diff <= over {
Some(ProblemResponse {
id: p.id.clone(),
contest_id: p.contest_id.clone(),
name: p.name.clone(),
difficulty: Some(diff),
})
} else {
None
}
})
})
}).choose(&mut rng);

match selected {
Some(problem) => {
let body = serde_json::to_string(&problem).unwrap();
Ok(with_cors_headers(Response::new(Body::from(body))))
}
None => {
let mut not_found = Response::new(Body::from("No problem found in given range."));
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(with_cors_headers(not_found))
}
}
}

_ => {
Expand Down
22 changes: 22 additions & 0 deletions backend/tests/api_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![allow(unused)]

use backend::api::{fetch_problem, Problem, ProblemModel};
use std::collections::HashMap;

#[tokio::test]
async fn test_fetch_problem_returns_data() {
let result = fetch_problem().await;

assert!(result.is_ok());
let (problems, problem_models): (Vec<Problem>, HashMap<String, ProblemModel>) = result.unwrap();

// 空でないことを確認
assert!(!problems.is_empty());
assert!(!problem_models.is_empty());

// Problem の中身確認
let first_problem = &problems[0];
assert!(!first_problem.id.is_empty());
assert!(!first_problem.contest_id.is_empty());
assert!(!first_problem.name.is_empty());
}
Loading