use crate::{
core::{Result, types::ApiKey, types::BaseUrl},
http::{SerperHttpClient, TransportConfig},
search::{SearchQuery, SearchQueryBuilder, SearchResponse},
};
use std::time::Duration;
#[derive(Debug)]
pub struct SearchService {
http_client: SerperHttpClient,
}
impl SearchService {
pub fn new(api_key: String) -> Result<Self> {
let api_key = ApiKey::new(api_key)?;
let http_client = SerperHttpClient::new(api_key)?;
Ok(Self { http_client })
}
pub fn with_config(api_key: String, base_url: String, config: TransportConfig) -> Result<Self> {
let api_key = ApiKey::new(api_key)?;
let base_url = BaseUrl::new(base_url);
let http_client = SerperHttpClient::with_config(api_key, base_url, config)?;
Ok(Self { http_client })
}
pub async fn search(&self, query: &SearchQuery) -> Result<SearchResponse> {
self.http_client.search(query).await
}
pub async fn search_simple(&self, query_string: &str) -> Result<SearchResponse> {
let query = SearchQuery::new(query_string.to_string())?;
self.search(&query).await
}
pub async fn search_multiple(&self, queries: &[SearchQuery]) -> Result<Vec<SearchResponse>> {
self.http_client.search_multiple(queries).await
}
pub async fn search_concurrent(
&self,
queries: &[SearchQuery],
max_concurrent: Option<usize>,
) -> Result<Vec<SearchResponse>> {
let max_concurrent = max_concurrent.unwrap_or(5);
self.http_client
.search_concurrent(queries, max_concurrent)
.await
}
pub fn query_builder(&self) -> SearchQueryBuilder {
SearchQueryBuilder::new()
}
pub async fn search_with<F>(&self, builder_fn: F) -> Result<SearchResponse>
where
F: FnOnce(SearchQueryBuilder) -> SearchQueryBuilder,
{
let query = builder_fn(self.query_builder()).build()?;
self.search(&query).await
}
pub fn info(&self) -> SearchServiceInfo {
SearchServiceInfo {
base_url: self.http_client.base_url().as_str().to_string(),
timeout: self.http_client.transport_config().timeout,
user_agent: self.http_client.transport_config().user_agent.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct SearchServiceInfo {
pub base_url: String,
pub timeout: Duration,
pub user_agent: String,
}
pub struct SearchServiceBuilder {
api_key: Option<String>,
base_url: Option<String>,
transport_config: TransportConfig,
}
impl SearchServiceBuilder {
pub fn new() -> Self {
Self {
api_key: None,
base_url: None,
transport_config: TransportConfig::new(),
}
}
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.transport_config = self.transport_config.with_timeout(timeout);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.transport_config = self.transport_config.with_header(key.into(), value.into());
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.transport_config = self.transport_config.with_user_agent(user_agent.into());
self
}
pub fn build(self) -> Result<SearchService> {
let api_key = self
.api_key
.ok_or_else(|| crate::core::SerperError::config_error("API key is required"))?;
match self.base_url {
Some(base_url) => SearchService::with_config(api_key, base_url, self.transport_config),
None => {
let api_key_obj = ApiKey::new(api_key)?;
let http_client = SerperHttpClient::with_config(
api_key_obj,
BaseUrl::default(),
self.transport_config,
)?;
Ok(SearchService { http_client })
}
}
}
}
impl Default for SearchServiceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_builder() {
let builder = SearchServiceBuilder::new()
.api_key("test-key")
.timeout(Duration::from_secs(60))
.user_agent("test-agent");
let service = builder.build().unwrap();
let info = service.info();
assert_eq!(info.timeout, Duration::from_secs(60));
assert_eq!(info.user_agent, "test-agent");
assert_eq!(info.base_url, "https://google.serper.dev");
}
#[test]
fn test_service_creation() {
let service = SearchService::new("test-key".to_string()).unwrap();
let info = service.info();
assert_eq!(info.base_url, "https://google.serper.dev");
assert_eq!(info.timeout, Duration::from_secs(30));
}
#[test]
fn test_builder_missing_api_key() {
let builder = SearchServiceBuilder::new();
let result = builder.build();
assert!(result.is_err());
}
#[test]
fn test_query_builder() {
let service = SearchService::new("test-key".to_string()).unwrap();
let builder = service.query_builder();
let query = builder
.query("test")
.location("Paris")
.page(1)
.build()
.unwrap();
assert_eq!(query.query(), "test");
assert_eq!(query.location, Some("Paris".to_string()));
assert_eq!(query.page, Some(1));
}
}