balls
This commit is contained in:
323
src/main.rs
Normal file
323
src/main.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use axum::{
|
||||
extract::Query,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::time::Duration;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LookupParams {
|
||||
query: Option<String>,
|
||||
country: Option<String>,
|
||||
lat: Option<f64>,
|
||||
lon: Option<f64>,
|
||||
}
|
||||
|
||||
async fn search_with_google(
|
||||
client: &reqwest::Client,
|
||||
query: &str,
|
||||
api_key: &str,
|
||||
country: Option<String>,
|
||||
lat: Option<f64>,
|
||||
lon: Option<f64>,
|
||||
) -> Result<Value, String> {
|
||||
let url = "https://maps.googleapis.com/maps/api/geocode/json";
|
||||
|
||||
let mut params = vec![
|
||||
("address", query.to_string()),
|
||||
("key", api_key.to_string()),
|
||||
];
|
||||
|
||||
if let Some(c) = country {
|
||||
params.push(("components", format!("country:{}", c)));
|
||||
}
|
||||
|
||||
// If lat/lon provided, create a bounding box to bias results toward that location
|
||||
// Using ~0.5 degree radius (~55km at equator) for the bounds
|
||||
if let (Some(lat), Some(lon)) = (lat, lon) {
|
||||
let lat_delta = 0.5;
|
||||
let lon_delta = 0.5;
|
||||
let bounds = format!(
|
||||
"{},{}|{},{}",
|
||||
lat - lat_delta,
|
||||
lon - lon_delta,
|
||||
lat + lat_delta,
|
||||
lon + lon_delta
|
||||
);
|
||||
params.push(("bounds", bounds));
|
||||
}
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.query(¶ms)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed reading response: {}", e))?;
|
||||
|
||||
println!("Google Geocoding raw response → {}", text);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(format!("HTTP {} → {}", status, text));
|
||||
}
|
||||
|
||||
let json: Value =
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|e| format!("JSON parse error: {}", e))?;
|
||||
|
||||
if json["status"] != "OK" && json["status"] != "ZERO_RESULTS" {
|
||||
return Err(format!("Google API error → {}", json));
|
||||
}
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn search_with_places(
|
||||
client: &reqwest::Client,
|
||||
query: &str,
|
||||
api_key: &str,
|
||||
lat: Option<f64>,
|
||||
lon: Option<f64>,
|
||||
) -> Result<Value, String> {
|
||||
let url = "https://maps.googleapis.com/maps/api/place/textsearch/json";
|
||||
|
||||
let mut params = vec![
|
||||
("query", query.to_string()),
|
||||
("key", api_key.to_string()),
|
||||
];
|
||||
|
||||
// If lat/lon provided, use them to bias results with a 50km radius
|
||||
if let (Some(lat), Some(lon)) = (lat, lon) {
|
||||
params.push(("location", format!("{},{}", lat, lon)));
|
||||
params.push(("radius", "50000".to_string())); // 50km radius
|
||||
}
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.query(¶ms)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed reading response: {}", e))?;
|
||||
|
||||
println!("Google Places raw response → {}", text);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(format!("HTTP {} → {}", status, text));
|
||||
}
|
||||
|
||||
let json: Value =
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|e| format!("JSON parse error: {}", e))?;
|
||||
|
||||
if json["status"] != "OK" && json["status"] != "ZERO_RESULTS" {
|
||||
return Err(format!("Google Places API error → {}", json));
|
||||
}
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn get_place_details(
|
||||
client: &reqwest::Client,
|
||||
place_id: &str,
|
||||
api_key: &str,
|
||||
) -> Result<Value, String> {
|
||||
let url = "https://maps.googleapis.com/maps/api/place/details/json";
|
||||
|
||||
let params = vec![
|
||||
("place_id", place_id.to_string()),
|
||||
("key", api_key.to_string()),
|
||||
// Request specific fields to get contact information
|
||||
("fields", "formatted_phone_number,international_phone_number,website,opening_hours,url".to_string()),
|
||||
];
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.query(¶ms)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed reading response: {}", e))?;
|
||||
|
||||
println!("Place Details raw response for {} → {}", place_id, text);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(format!("HTTP {} → {}", status, text));
|
||||
}
|
||||
|
||||
let json: Value =
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|e| format!("JSON parse error: {}", e))?;
|
||||
|
||||
if json["status"] != "OK" {
|
||||
return Err(format!("Place Details API error → {}", json));
|
||||
}
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn lookup(Query(params): Query<LookupParams>) -> impl IntoResponse {
|
||||
let query = match params.query {
|
||||
Some(q) => q,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "Missing query parameter"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let api_key = env::var("GOOGLE_API_KEY")
|
||||
.expect("GOOGLE_API_KEY env var not set");
|
||||
|
||||
|
||||
// Force IPv4-only for all outbound requests
|
||||
let client = reqwest::Client::builder()
|
||||
.local_address(Some(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Call both Geocoding and Places APIs in parallel
|
||||
let (geocoding_result, places_result) = tokio::join!(
|
||||
search_with_google(
|
||||
&client,
|
||||
&query,
|
||||
&api_key,
|
||||
params.country.clone(),
|
||||
params.lat,
|
||||
params.lon,
|
||||
),
|
||||
search_with_places(
|
||||
&client,
|
||||
&query,
|
||||
&api_key,
|
||||
params.lat,
|
||||
params.lon,
|
||||
)
|
||||
);
|
||||
|
||||
// Extract geocoding results
|
||||
let geocoding_data = match geocoding_result {
|
||||
Ok(data) => {
|
||||
data.get("results")
|
||||
.and_then(|r| r.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Geocoding error: {}", e);
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
// Extract places results
|
||||
let mut places_data = match places_result {
|
||||
Ok(data) => {
|
||||
data.get("results")
|
||||
.and_then(|r| r.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Places error: {}", e);
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
// Enrich places with contact details (limit to first 5 to avoid too many API calls)
|
||||
let places_to_enrich: Vec<_> = places_data
|
||||
.iter()
|
||||
.take(5)
|
||||
.filter_map(|place| {
|
||||
place.get("place_id")
|
||||
.and_then(|id| id.as_str())
|
||||
.map(|id| id.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Fetch place details for each place sequentially (could be parallelized with tokio::spawn)
|
||||
for (idx, place_id) in places_to_enrich.iter().enumerate() {
|
||||
if let Ok(detail) = get_place_details(&client, place_id, &api_key).await {
|
||||
if let Some(result) = detail.get("result") {
|
||||
// Get the place object to enrich
|
||||
if let Some(place) = places_data.get_mut(idx) {
|
||||
if let Some(place_obj) = place.as_object_mut() {
|
||||
// Add contact details
|
||||
if let Some(phone) = result.get("formatted_phone_number") {
|
||||
place_obj.insert("phone".to_string(), phone.clone());
|
||||
}
|
||||
if let Some(intl_phone) = result.get("international_phone_number") {
|
||||
place_obj.insert("international_phone".to_string(), intl_phone.clone());
|
||||
}
|
||||
if let Some(website) = result.get("website") {
|
||||
place_obj.insert("website".to_string(), website.clone());
|
||||
}
|
||||
if let Some(url) = result.get("url") {
|
||||
place_obj.insert("google_maps_url".to_string(), url.clone());
|
||||
}
|
||||
if let Some(hours) = result.get("opening_hours") {
|
||||
place_obj.insert("opening_hours".to_string(), hours.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return both results
|
||||
(StatusCode::OK, Json(json!({
|
||||
"geocoding": {
|
||||
"count": geocoding_data.len(),
|
||||
"results": geocoding_data
|
||||
},
|
||||
"places": {
|
||||
"count": places_data.len(),
|
||||
"results": places_data
|
||||
}
|
||||
}))).into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
let app = Router::new().route("/lookup", get(lookup));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
|
||||
.await
|
||||
.expect("Failed to bind port");
|
||||
|
||||
println!("Server running → http://0.0.0.0:3000");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("Server failed");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user