mod config; mod models; mod search; use axum::{ extract::Query, http::Method, routing::get, Json, Router, }; use config::Config; use models::{SearchQuery, SearchResponse}; use reqwest::Client; use std::net::SocketAddr; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::ServeDir; use tracing::info; #[derive(Clone)] struct AppState { client: Client, config: Config, } #[tokio::main] async fn main() { let config = Config::from_env(); tracing_subscriber::fmt() .with_env_filter(&config.log_level) .init(); info!("Starting App Lookup Rust backend..."); let client = Client::builder() .timeout(std::time::Duration::from_secs(config.http_timeout_secs)) .build() .expect("Failed to build HTTP client"); let state = AppState { client, config: config.clone(), }; let cors = CorsLayer::new() .allow_origin(Any) .allow_methods([Method::GET, Method::POST]) .allow_headers(Any); let app = Router::new() .route("/api/search", get(handle_search)) .nest_service("/", ServeDir::new("static")) .layer(cors) .with_state(state); let addr = SocketAddr::new( config.host.parse().unwrap(), config.port, ); info!("Listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr) .await .expect("Failed to bind"); axum::serve(listener, app) .await .expect("Server error"); } async fn handle_search( state: axum::extract::State, Query(params): Query, ) -> Json { info!("API request | q={} | store={}", params.q, params.store); let results = search::orchestrator::search_all( &state.client, ¶ms.q, ¶ms.store, params.limit, ) .await; Json(SearchResponse { query: params.q, count: results.len(), results, }) }