use std::collections::BTreeMap;
use hyper::{
header,
http::response,
service::{make_service_fn, service_fn},
Body, Method, Request, Response, Server,
};
use once_cell::sync::Lazy;
use serde_json::json;
use crate::{calc_next_update, error::Result, AUCTIONS, CONFIG, SOURCE, SPONSOR};
static NOTFOUND: &[u8] = b"{\"error\": \"not found\"}";
pub async fn start_server() -> Result<()> {
let addr = format!("{}:{}", CONFIG.host, CONFIG.port)
.parse()
.expect("Failed to parse addr");
let make_service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(response)) });
let server = Server::bind(&addr).serve(make_service);
tracing::info!("Listening on http://{}", addr);
if let Err(e) = server.await {
tracing::error!("server error: {}", e);
}
Ok(())
}
fn response_base() -> response::Builder {
let update = calc_next_update();
Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.header(header::ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS")
.header(header::ACCESS_CONTROL_ALLOW_HEADERS, "*")
.header(header::CACHE_CONTROL, format!("max-age={update}, s-maxage={update}"))
.header(header::ACCESS_CONTROL_MAX_AGE, "86400")
.header("funding", SPONSOR)
}
async fn response(req: Request<Body>) -> Result<Response<Body>> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/lowestbins.json") | (&Method::GET, "/lowestbins") | (&Method::GET, "/auctions/lowestbins") => {
let bytes = serde_json::to_vec(&*AUCTIONS.lock().unwrap())?;
Ok(response_base().body(Body::from(bytes))?)
}
(&Method::GET, "/lowestbins.txt") => {
let mut res = String::new();
for (key, value) in &*AUCTIONS.lock().unwrap() {
res.push_str(&format!("{key} {value}\n"));
}
Ok(response_base().body(Body::from(res))?)
}
(&Method::GET, route) if route.starts_with("/auction/") || route.starts_with("/lowestbin/") => {
let id = route.trim_start_matches("/auction/").trim_start_matches("/lowestbin/");
let auctions = AUCTIONS.lock().unwrap();
let value = auctions.get(id);
if let Some(auction) = value {
let bytes = serde_json::to_vec(&auction)?;
Ok(response_base().body(Body::from(bytes))?)
} else {
Ok(not_found())
}
}
(&Method::GET, "/metrics") => {
static DISPLAY_NAMES: Lazy<BTreeMap<String, String>> = Lazy::new(|| {
let bytes = include_bytes!("../resources/display-names.bin");
rmp_serde::from_slice(bytes).unwrap()
});
let mut res = "# HELP price Price of each item\n# TYPE price gauge".to_owned();
for (item, price) in &*AUCTIONS.lock().unwrap() {
let display_name = DISPLAY_NAMES.get(item).unwrap_or(item);
res.push_str(&format!(
"\nlowestbin_price{{item=\"{}\", display=\"{}\"}} {}",
item, display_name, price,
));
}
Ok(response_base().body(Body::from(res)).unwrap())
}
(_, "/") => Ok(response_base()
.header(header::CACHE_CONTROL, "max-age=2, s-maxage=2")
.body(Body::from(serde_json::to_vec_pretty(&json!({
"message": "Welcome to the lowestbins API",
"endpoint": "/lowestbins",
"updates_in": calc_next_update(),
"funding": SPONSOR,
"source": SOURCE
}))?))?),
_ => Ok(not_found()),
}
}
fn not_found() -> Response<Body> {
response_base().status(404).body(NOTFOUND.into()).unwrap()
}