gtrend-rs 0.1.1

Unofficial client for interacting with Google Trend API
Documentation
//! This module is responsible for fetching widgets from Google Trends.

use serde::Deserialize;
use std::collections::HashMap;

use crate::{
    error::{Error, Result},
    trends_client::{
        RelatedQueries, TrendsClient, geo_map::GeoMap, sanitize_google_json, timeseries::Timeseries,
    },
};

/// Google trend Widget categories
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WidgetCategory {
    Timeseries,
    GeoMap,
    RelatedTopics,
    RelatedQueries,
}

impl TryFrom<&str> for WidgetCategory {
    fn try_from(value: &str) -> Result<Self> {
        if value.contains("TIMESERIES") {
            Ok(WidgetCategory::Timeseries)
        } else if value.contains("GEO_MAP") {
            Ok(WidgetCategory::GeoMap)
        } else if value.contains("RELATED_TOPICS") {
            Ok(WidgetCategory::RelatedTopics)
        } else if value.contains("RELATED_QUERIES") {
            Ok(WidgetCategory::RelatedQueries)
        } else {
            Err(Error::UnexpectedResponse(format!(
                "Irregular widget category: {value}"
            )))
        }
    }

    type Error = Error;
}

/// Google trend Widget keywords
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
pub enum WidgetKeyword {
    /// Main keyword if only one is given, otherwise all keywords
    All,

    /// Specific keyword when given multiple in the request
    Keyword(String),
}

#[derive(Debug, Deserialize)]
pub struct Widget {
    token: String,
    request: serde_json::Value,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ExploreResult {
    widgets: Vec<serde_json::Value>,
    //skipping many items here
}

/// Client meant to fetch widgets from Google Trends
#[derive(Debug)]
pub struct ExploreClient {
    trends_client: TrendsClient,
    widgets: HashMap<(WidgetKeyword, WidgetCategory), Widget>,
}

impl ExploreClient {
    pub(crate) fn new(trends_client: TrendsClient, explore_result: ExploreResult) -> Result<Self> {
        let mut widgets = HashMap::new();
        let mut keyword = WidgetKeyword::All;

        for widget in &explore_result.widgets {
            match widget
                .get("template")
                .ok_or_else(|| Error::UnexpectedResponse(format!("Irregular widget: {widget}")))?
                .as_str()
                .ok_or_else(|| Error::UnexpectedResponse(format!("Irregular widget: {widget}")))?
            {
                "fe" => {
                    let id = widget
                        .get("id")
                        .ok_or_else(|| {
                            Error::UnexpectedResponse(format!("Irregular widget: {widget}"))
                        })?
                        .as_str()
                        .ok_or_else(|| {
                            Error::UnexpectedResponse(format!("Irregular widget: {widget}"))
                        })?;

                    if id.contains("_note") {
                        continue;
                    }

                    let category = WidgetCategory::try_from(id)?;

                    widgets.insert(
                        (keyword.clone(), category),
                        serde_json::from_value(widget.clone())?,
                    );
                }
                "fe_explore" => {
                    keyword = WidgetKeyword::Keyword(
                        widget
                            .get("text")
                            .ok_or_else(|| {
                                Error::UnexpectedResponse(format!("Irregular widget: {widget}"))
                            })?
                            .get("text")
                            .ok_or_else(|| {
                                Error::UnexpectedResponse(format!("Irregular widget: {widget}"))
                            })?
                            .as_str()
                            .ok_or_else(|| {
                                Error::UnexpectedResponse(format!("Irregular widget: {widget}"))
                            })?
                            .to_string(),
                    );
                }
                _ => {
                    return Err(Error::UnexpectedResponse(format!(
                        "Irregular widget: {widget}"
                    )));
                }
            }
        }

        Ok(Self {
            trends_client,
            widgets,
        })
    }

    /// Returns all available widgets
    pub fn available_widgets(&self) -> Vec<(WidgetKeyword, WidgetCategory)> {
        self.widgets.keys().cloned().collect()
    }

    async fn get_widget(&self, keyword: WidgetKeyword, category: WidgetCategory) -> Result<String> {
        let widget = self
            .widgets
            .get(&(keyword.clone(), category))
            .ok_or_else(|| {
                Error::Params(format!(
                    "No widget for category {category:?} and keyword {keyword:?}"
                ))
            })?;
        let end_url = match category {
            WidgetCategory::Timeseries => "trends/api/widgetdata/multiline",
            WidgetCategory::GeoMap => "trends/api/widgetdata/comparedgeo",
            WidgetCategory::RelatedTopics => "trends/api/widgetdata/relatedsearches",
            WidgetCategory::RelatedQueries => "trends/api/widgetdata/relatedsearches",
        };
        self.trends_client
            .get(
                end_url,
                &serde_json::to_string(&widget.request)?,
                Some(&widget.token),
            )
            .await
    }

    /// Returns the widget as a JSON object
    pub async fn get_widget_as_json(
        &self,
        keyword: WidgetKeyword,
        category: WidgetCategory,
    ) -> Result<serde_json::Value> {
        let widget = self.get_widget(keyword, category).await?;
        let cleaned_widget = sanitize_google_json(&widget);
        serde_json::from_str(cleaned_widget).map_err(Error::from)
    }

    /// Returns the timeseries as a [`Timeseries`] object
    pub async fn get_timeseries(&self, keyword: WidgetKeyword) -> Result<Timeseries> {
        let content = self.get_widget(keyword, WidgetCategory::Timeseries).await?;
        serde_json::from_str(sanitize_google_json(&content)).map_err(Error::from)
    }

    /// Returns the geomap as a [`GeoMap`] object
    pub async fn get_geomap(&self, keyword: WidgetKeyword) -> Result<GeoMap> {
        let content = self.get_widget(keyword, WidgetCategory::GeoMap).await?;
        serde_json::from_str(sanitize_google_json(&content)).map_err(Error::from)
    }

    /// Returns the related queries as a [`RelatedQueries`] object
    pub async fn get_related_queries(&self, keyword: WidgetKeyword) -> Result<RelatedQueries> {
        let content = self
            .get_widget(keyword, WidgetCategory::RelatedQueries)
            .await?;
        serde_json::from_str(sanitize_google_json(&content)).map_err(Error::from)
    }
}