Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 */
use serde::{Deserialize, Serialize};
// Configuration options for initializing a `CuratedRecommendationsClient`
#[derive(Debug, Serialize, PartialEq, Deserialize, uniffi::Record)]
pub struct CuratedRecommendationsConfig {
    pub base_host: Option<String>,
    pub user_agent_header: String,
}
// Locales supported by Merino Curated Recommendations
#[derive(Debug, Serialize, PartialEq, Deserialize, uniffi::Enum)]
pub enum CuratedRecommendationLocale {
    #[serde(rename = "fr")]
    Fr,
    #[serde(rename = "fr-FR")]
    FrFr,
    #[serde(rename = "es")]
    Es,
    #[serde(rename = "es-ES")]
    EsEs,
    #[serde(rename = "it")]
    It,
    #[serde(rename = "it-IT")]
    ItIt,
    #[serde(rename = "en")]
    En,
    #[serde(rename = "en-CA")]
    EnCa,
    #[serde(rename = "en-GB")]
    EnGb,
    #[serde(rename = "en-US")]
    EnUs,
    #[serde(rename = "de")]
    De,
    #[serde(rename = "de-DE")]
    DeDe,
    #[serde(rename = "de-AT")]
    DeAt,
    #[serde(rename = "de-CH")]
    DeCh,
}
impl CuratedRecommendationLocale {
    /// Returns all supported locale strings (e.g. `"en-US"`, `"fr-FR"`).
    ///
    /// These strings are the canonical serialized values of the enum variants.
    pub fn all_locales() -> Vec<String> {
        vec![
            CuratedRecommendationLocale::Fr,
            CuratedRecommendationLocale::FrFr,
            CuratedRecommendationLocale::Es,
            CuratedRecommendationLocale::EsEs,
            CuratedRecommendationLocale::It,
            CuratedRecommendationLocale::ItIt,
            CuratedRecommendationLocale::En,
            CuratedRecommendationLocale::EnCa,
            CuratedRecommendationLocale::EnGb,
            CuratedRecommendationLocale::EnUs,
            CuratedRecommendationLocale::De,
            CuratedRecommendationLocale::DeDe,
            CuratedRecommendationLocale::DeAt,
            CuratedRecommendationLocale::DeCh,
        ]
        .into_iter()
        .map(|l| {
            serde_json::to_string(&l)
                .unwrap()
                .trim_matches('"')
                .to_string()
        })
        .collect()
    }
    /// Parses a locale string (e.g. `"en-US"`) into a `CuratedRecommendationLocale` enum variant.
    ///
    /// Returns `None` if the string does not match a known variant.
    pub fn from_locale_string(locale: String) -> Option<CuratedRecommendationLocale> {
        serde_json::from_str(&format!("\"{}\"", locale)).ok()
    }
}
// Configuration settings for a Section
#[derive(Debug, Serialize, Deserialize, PartialEq, uniffi::Record)]
pub struct SectionSettings {
    #[serde(rename = "sectionId")]
    pub section_id: String,
    #[serde(rename = "isFollowed")]
    pub is_followed: bool,
    #[serde(rename = "isBlocked")]
    pub is_blocked: bool,
}
// Information required to request curated recommendations
#[derive(Debug, Serialize, PartialEq, Deserialize, uniffi::Record)]
pub struct CuratedRecommendationsRequest {
    pub locale: CuratedRecommendationLocale,
    #[uniffi(default = None)]
    pub region: Option<String>,
    #[uniffi(default = Some(100))]
    pub count: Option<i32>,
    #[uniffi(default = None)]
    pub topics: Option<Vec<String>>,
    #[uniffi(default = None)]
    pub feeds: Option<Vec<String>>,
    #[uniffi(default = None)]
    pub sections: Option<Vec<SectionSettings>>,
    #[serde(rename = "experimentName")]
    #[uniffi(default = None)]
    pub experiment_name: Option<String>,
    #[serde(rename = "experimentBranch")]
    #[uniffi(default = None)]
    pub experiment_branch: Option<String>,
    #[serde(rename = "enableInterestPicker", default)]
    #[uniffi(default = false)]
    pub enable_interest_picker: bool,
}
// Response schema for a list of curated recommendations
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct CuratedRecommendationsResponse {
    #[serde(rename = "recommendedAt")]
    pub recommended_at: i64,
    pub data: Vec<RecommendationDataItem>,
    #[uniffi(default = None)]
    pub feeds: Option<Feeds>,
    #[serde(rename = "interestPicker")]
    #[uniffi(default = None)]
    pub interest_picker: Option<InterestPicker>,
}
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
// Specifies the display order (receivedFeedRank) and a list of sections (referenced by sectionId) for interest bubbles.
pub struct InterestPicker {
    #[serde(rename = "receivedFeedRank")]
    pub received_feed_rank: i32,
    pub title: String,
    pub subtitle: String,
    pub sections: Vec<InterestPickerSection>,
}
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct InterestPickerSection {
    #[serde(rename = "sectionId")]
    pub section_id: String,
}
// Multiple lists of curated recommendations
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct Feeds {
    #[uniffi(default = None)]
    pub need_to_know: Option<CuratedRecommendationsBucket>,
    #[uniffi(default = None)]
    pub fakespot: Option<FakespotFeed>,
    #[uniffi(default = None)]
    pub top_stories_section: Option<FeedSection>,
    #[uniffi(default = None)]
    pub business: Option<FeedSection>,
    #[uniffi(default = None)]
    pub career: Option<FeedSection>,
    #[uniffi(default = None)]
    pub arts: Option<FeedSection>,
    #[uniffi(default = None)]
    pub food: Option<FeedSection>,
    #[uniffi(default = None)]
    pub health: Option<FeedSection>,
    #[uniffi(default = None)]
    pub home: Option<FeedSection>,
    #[uniffi(default = None)]
    pub finance: Option<FeedSection>,
    #[uniffi(default = None)]
    pub government: Option<FeedSection>,
    #[uniffi(default = None)]
    pub sports: Option<FeedSection>,
    #[uniffi(default = None)]
    pub tech: Option<FeedSection>,
    #[uniffi(default = None)]
    pub travel: Option<FeedSection>,
    #[uniffi(default = None)]
    pub education: Option<FeedSection>,
    #[uniffi(default = None)]
    pub hobbies: Option<FeedSection>,
    #[serde(rename = "society-parenting")]
    #[uniffi(default = None)]
    pub society_parenting: Option<FeedSection>,
    #[serde(rename = "education-science")]
    #[uniffi(default = None)]
    pub education_science: Option<FeedSection>,
    #[uniffi(default = None)]
    pub society: Option<FeedSection>,
}
// Curated Recommendation Information
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct RecommendationDataItem {
    #[serde(rename = "corpusItemId")]
    pub corpus_item_id: String,
    #[serde(rename = "scheduledCorpusItemId")]
    pub scheduled_corpus_item_id: String,
    pub url: String,
    pub title: String,
    pub excerpt: String,
    #[uniffi(default = None)]
    pub topic: Option<String>,
    pub publisher: String,
    #[serde(rename = "isTimeSensitive")]
    pub is_time_sensitive: bool,
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    #[serde(rename = "iconUrl")]
    pub icon_url: Option<String>,
    #[serde(rename = "tileId")]
    pub tile_id: i64,
    #[serde(rename = "receivedRank")]
    pub received_rank: i64,
}
// Ranked list of curated recommendations
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct CuratedRecommendationsBucket {
    pub recommendations: Vec<RecommendationDataItem>,
    #[uniffi(default = None)]
    pub title: Option<String>,
}
// Fakespot product recommendations
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct FakespotFeed {
    pub products: Vec<FakespotProduct>,
    #[serde(rename = "defaultCategoryName")]
    pub default_category_name: String,
    #[serde(rename = "headerCopy")]
    pub header_copy: String,
    #[serde(rename = "footerCopy")]
    pub footer_copy: String,
    pub cta: FakespotCta,
}
// Fakespot product details
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct FakespotProduct {
    id: String,
    title: String,
    category: String,
    #[serde(rename = "imageUrl")]
    image_url: String,
    url: String,
}
// Fakespot CTA
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct FakespotCta {
    #[serde(rename = "ctaCopy")]
    pub cta_copy: String,
    pub url: String,
}
// Ranked list of curated recommendations with responsive layout configs
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct FeedSection {
    #[serde(rename = "receivedFeedRank")]
    pub received_feed_rank: i32,
    pub recommendations: Vec<RecommendationDataItem>,
    pub title: String,
    #[uniffi(default = None)]
    pub subtitle: Option<String>,
    pub layout: Layout,
    #[serde(rename = "isFollowed")]
    pub is_followed: bool,
    #[serde(rename = "isBlocked")]
    pub is_blocked: bool,
}
// Representation of a responsive layout configuration with multiple column layouts
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct Layout {
    pub name: String,
    #[serde(rename = "responsiveLayouts")]
    pub responsive_layouts: Vec<ResponsiveLayout>,
}
// Layout configurations within a column
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct ResponsiveLayout {
    #[serde(rename = "columnCount")]
    pub column_count: i32,
    pub tiles: Vec<Tile>,
}
// Properties for a single tile in a responsive layout
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
pub struct Tile {
    pub size: String,
    pub position: i32,
    #[serde(rename = "hasAd")]
    pub has_ad: bool,
    #[serde(rename = "hasExcerpt")]
    pub has_excerpt: bool,
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_from_locale_string_valid_cases() {
        assert_eq!(
            CuratedRecommendationLocale::from_locale_string("en-US".into()),
            Some(CuratedRecommendationLocale::EnUs)
        );
        assert_eq!(
            CuratedRecommendationLocale::from_locale_string("fr".into()),
            Some(CuratedRecommendationLocale::Fr)
        );
    }
    #[test]
    fn test_from_locale_string_invalid_cases() {
        assert_eq!(
            CuratedRecommendationLocale::from_locale_string("en_US".into()),
            None
        );
        assert_eq!(
            CuratedRecommendationLocale::from_locale_string("zz-ZZ".into()),
            None
        );
    }
    #[test]
    fn test_all_locales_contains_expected_values() {
        let locales = CuratedRecommendationLocale::all_locales();
        assert!(locales.contains(&"en-US".to_string()));
        assert!(locales.contains(&"de-CH".to_string()));
        assert!(locales.contains(&"fr".to_string()));
    }
    #[test]
    fn test_all_locales_round_trip() {
        for locale_str in CuratedRecommendationLocale::all_locales() {
            let parsed = CuratedRecommendationLocale::from_locale_string(locale_str.clone());
            assert!(parsed.is_some(), "Failed to parse locale: {}", locale_str);
            let reserialized = serde_json::to_string(&parsed.unwrap()).unwrap();
            let clean = reserialized.trim_matches('"');
            assert_eq!(
                clean, locale_str,
                "Round-trip mismatch: {} => {}",
                locale_str, clean
            );
        }
    }
}