use arcstr::ArcStr;
use itertools::Itertools;
use reqwest::header::HeaderName;
use reqwest::Method;
mod error;
use error::Error;
mod model;
pub use model::*;
pub struct Client {
http: reqwest::Client,
api_key: ArcStr
}
const API_ROOT: &str = "https://api.foursquare.com/v3/";
const PLACE_SEARCH_PATH: &str = "places/search";
impl Client {
pub fn new(api_key: &str) -> Self {
Self{
http: reqwest::Client::new(),
api_key: api_key.into()
}
}
async fn request(&self, method: Method, path: &str) -> Result<String, reqwest::Error> {
let url = {
let mut s = String::from(API_ROOT);
s.push_str(path);
s
};
let response = self.http.request(method, &url)
.header(HeaderName::from_static("authorization"), self.api_key.as_str())
.send()
.await?
.text()
.await?;
Ok(response)
}
#[inline]
async fn get(&self, path: &str) -> Result<String, reqwest::Error> {
self.request(Method::GET, path).await
}
#[inline]
pub async fn search_places(&self, params: &[(&'static str, String)]) -> Result<PlaceResponse, Error> {
let path = {
let mut s = String::from(PLACE_SEARCH_PATH);
s.push('?');
s.push_str(&serde_urlencoded::to_string(params)?);
s
};
let body = self.get(&path).await?;
Ok(serde_json::from_str(&body)?)
}
pub async fn get_places_near(&self, latitude: f32, longitude: f32, radius_in_meters: u16) -> Result<PlaceResponse, Error> {
self.search_places(&[
("ll", format!("{},{}", latitude, longitude)),
("radius", radius_in_meters.to_string())
]).await
}
pub async fn get_places_near_with_category(&self, latitude: f32, longitude: f32, radius_in_meters: u16, categories: &[u16]) -> Result<PlaceResponse, Error> {
self.search_places(&[
("ll", format!("{},{}", latitude, longitude)),
("radius", radius_in_meters.to_string()),
("categories", categories.iter().map(|v| v.to_string()).join(","))
]).await
}
}
#[cfg(test)]
mod tests {
use super::Client;
fn new_client() -> Client {
Client{
http: reqwest::Client::new(),
api_key: arcstr::literal!(env!("FOURSQUARE_API_KEY"))
}
}
#[tokio::test]
async fn places_near_udairy() {
let client = new_client();
let places = client.get_places_near(39.66509724690995, -75.75047266260954, 100).await.unwrap();
println!("{}", serde_json::to_string_pretty(&places).unwrap());
assert!(places.results().iter().any(|p| p.id() == "80b74fd504fc4e6fd23d1d53"));
assert!(places.results().iter().any(|p| p.name() == "University of Delaware Ice Arena"));
}
#[tokio::test]
async fn places_near_rummur_lounge() {
let client = new_client();
let places = client.get_places_near(39.52702076582365, -75.81214777855061, 10).await.unwrap();
println!("{}", serde_json::to_string_pretty(&places).unwrap());
assert!(places.results().iter().any(|p| p.id() == "17b21bcfc2b94276b7654ec3"));
assert!(places.results().iter().any(|p| p.name() == "Rummur Lounge"));
}
#[tokio::test]
async fn city_halls_near_philly() {
let client = new_client();
let places = client.get_places_near_with_category(39.957231008535786, -75.18099479230888, 10000, &[12066]).await.unwrap();
println!("{}", serde_json::to_string_pretty(&places).unwrap());
assert!(places.results().iter().any(|p| p.name() == "Philadelphia City Hall"));
assert!(places.results().iter().any(|p| p.name() == "Camden City Hall"));
assert!(places.results().iter().any(|p| p.name() == "Upper Darby Township Building"));
assert!(places.results().iter().any(|p| p.name() == "East Lansdowne Borough Hall"));
}
}