use std::time::Duration;
pub mod ai_ml;
pub mod automotive;
pub mod banking;
pub mod business;
pub mod crypto;
pub mod cybersecurity;
pub mod data_science;
pub mod ecommerce;
pub mod education;
pub mod energy;
pub mod finance;
pub mod food;
pub mod gaming;
pub mod government;
pub mod legal;
pub mod marketing;
pub mod medical;
pub mod open_source;
pub mod programming;
pub mod reddit;
pub mod research;
pub mod science;
pub mod sports;
pub mod tech;
pub mod travel;
#[derive(Debug, Clone, Copy)]
pub struct SeedSource {
pub url: &'static str,
pub recrawl: Recrawl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Recrawl {
Daily,
Weekly,
Monthly,
}
impl Recrawl {
pub fn interval(&self) -> Duration {
match self {
Recrawl::Daily => Duration::from_secs(24 * 3600),
Recrawl::Weekly => Duration::from_secs(7 * 24 * 3600),
Recrawl::Monthly => Duration::from_secs(30 * 24 * 3600),
}
}
}
#[derive(Debug, Clone)]
pub struct CuratedDomain {
pub slug: &'static str,
pub name: &'static str,
pub topics: &'static [&'static str],
pub sources: &'static [SeedSource],
}
pub const DOMAINS: &[CuratedDomain] = &[
research::DOMAIN,
programming::DOMAIN,
medical::DOMAIN,
finance::DOMAIN,
banking::DOMAIN,
tech::DOMAIN,
cybersecurity::DOMAIN,
legal::DOMAIN,
science::DOMAIN,
energy::DOMAIN,
ai_ml::DOMAIN,
data_science::DOMAIN,
marketing::DOMAIN,
business::DOMAIN,
ecommerce::DOMAIN,
education::DOMAIN,
government::DOMAIN,
gaming::DOMAIN,
automotive::DOMAIN,
sports::DOMAIN,
travel::DOMAIN,
food::DOMAIN,
open_source::DOMAIN,
crypto::DOMAIN,
reddit::DOMAIN,
];
pub fn domain_by_slug(slug: &str) -> Option<&'static CuratedDomain> {
DOMAINS.iter().find(|d| d.slug == slug)
}
pub fn all_source_urls() -> Vec<(&'static str, Recrawl)> {
DOMAINS
.iter()
.flat_map(|d| d.sources.iter().map(|s| (s.url, s.recrawl)))
.collect()
}
pub fn source_count() -> usize {
all_source_urls().len()
}
pub fn catalog_summary() -> String {
let mut out = String::from("Curated seed catalog (non-Wikipedia, official sources)\n");
for d in DOMAINS {
out.push_str(&format!(
" {:>14} {:>4} sources — {}\n",
d.slug,
d.sources.len(),
d.name
));
}
out.push_str(&format!(" {:>14} {:>4} total\n", "total", source_count()));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_catalog_has_many_domains() {
assert!(
DOMAINS.len() >= 20,
"catalog should be broad, got {}",
DOMAINS.len()
);
assert!(
source_count() >= 100,
"catalog should be deep, got {}",
source_count()
);
}
#[test]
fn test_no_wikipedia_sources() {
let bad: Vec<&str> = all_source_urls()
.iter()
.filter(|(u, _)| u.contains("wikipedia.org"))
.map(|(u, _)| *u)
.collect();
assert!(
bad.is_empty(),
"catalog must never seed Wikipedia, found: {:?}",
bad
);
}
#[test]
fn test_sources_are_https() {
let bad: Vec<&str> = all_source_urls()
.iter()
.filter(|(u, _)| !u.starts_with("https://"))
.map(|(u, _)| *u)
.collect();
assert!(
bad.is_empty(),
"all sources must be https, found: {:?}",
bad
);
}
#[test]
fn test_all_slugs_unique() {
let mut seen = std::collections::HashSet::new();
for d in DOMAINS {
assert!(seen.insert(d.slug), "duplicate slug: {}", d.slug);
assert!(!d.sources.is_empty(), "domain {} has no sources", d.slug);
}
}
#[test]
fn test_domain_by_slug() {
assert!(domain_by_slug("finance").is_some());
assert!(domain_by_slug("medical").is_some());
assert!(domain_by_slug("nonexistent").is_none());
}
}