use crate::error::{SerpError, SerpResult};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SearchQuery {
#[serde(rename = "q")]
query: String,
#[serde(rename = "hl", skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(rename = "gl", skip_serializing_if = "Option::is_none")]
geolocation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
google_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
num: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
device: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
safe: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tbm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
location: Option<String>,
#[serde(skip)]
api_key: String,
}
impl SearchQuery {
#[allow(clippy::new_ret_no_self)]
pub fn new(query: impl Into<String>) -> SearchQueryBuilder {
SearchQueryBuilder::new(query)
}
pub fn query(&self) -> &str {
&self.query
}
#[allow(dead_code)]
pub(crate) fn api_key(&self) -> &str {
&self.api_key
}
pub fn to_query_string(&self) -> SerpResult<String> {
let mut params = serde_urlencoded::to_string(self)?;
params.push_str(&format!("&api_key={}", self.api_key));
Ok(params)
}
}
#[derive(Clone)]
pub struct SearchQueryBuilder {
inner: SearchQuery,
}
impl SearchQueryBuilder {
pub fn new(query: impl Into<String>) -> Self {
Self {
inner: SearchQuery {
query: query.into(),
language: None,
geolocation: None,
google_domain: None,
num: None,
start: None,
device: None,
safe: None,
tbm: None,
location: None,
api_key: String::new(),
},
}
}
pub fn language(mut self, hl: impl Into<String>) -> Self {
self.inner.language = Some(hl.into());
self
}
pub fn country(mut self, gl: impl Into<String>) -> Self {
self.inner.geolocation = Some(gl.into());
self
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.inner.google_domain = Some(domain.into());
self
}
pub fn limit(mut self, num: u32) -> SerpResult<Self> {
if num == 0 || num > 100 {
return Err(SerpError::InvalidParameter(
"limit must be between 1 and 100".to_string(),
));
}
self.inner.num = Some(num);
Ok(self)
}
pub fn offset(mut self, start: u32) -> Self {
self.inner.start = Some(start);
self
}
pub fn device(mut self, device: impl Into<String>) -> Self {
self.inner.device = Some(device.into());
self
}
pub fn safe_search(mut self, safe: impl Into<String>) -> Self {
self.inner.safe = Some(safe.into());
self
}
pub fn search_type(mut self, tbm: impl Into<String>) -> Self {
self.inner.tbm = Some(tbm.into());
self
}
pub fn location(mut self, location: impl Into<String>) -> Self {
self.inner.location = Some(location.into());
self
}
pub(crate) fn build(mut self, api_key: String) -> SearchQuery {
self.inner.api_key = api_key;
self.inner
}
}
impl SearchQueryBuilder {
pub fn images(self) -> Self {
self.search_type("isch")
}
pub fn videos(self) -> Self {
self.search_type("vid")
}
pub fn news(self) -> Self {
self.search_type("nws")
}
pub fn shopping(self) -> Self {
self.search_type("shop")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_builder() {
let query = SearchQuery::new("rust programming")
.language("en")
.country("us")
.limit(10)
.unwrap()
.build("test-key".to_string());
assert_eq!(query.query(), "rust programming");
assert_eq!(query.language.as_ref().unwrap(), "en");
assert_eq!(query.geolocation.as_ref().unwrap(), "us");
assert_eq!(query.num.unwrap(), 10);
}
#[test]
fn test_limit_validation() {
let result = SearchQuery::new("test").limit(101);
assert!(result.is_err());
let result = SearchQuery::new("test").limit(0);
assert!(result.is_err());
}
#[test]
fn test_specialized_builders() {
let query = SearchQuery::new("cats")
.images()
.build("test-key".to_string());
assert_eq!(query.tbm.as_ref().unwrap(), "isch");
}
}