tele 0.1.19

Ergonomic Telegram Bot API SDK for Rust, built on reqx
Documentation
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use serde::Deserialize;

const EMBEDDED_METHOD_SPEC: &str = include_str!("fixtures/bot_api_all_methods.txt");

#[derive(Debug, Deserialize)]
struct MethodCoverageSpec {
    all_methods: Vec<String>,
}

fn parse_expected_methods(text: &str, path: &Path) -> Option<BTreeSet<String>> {
    if path.extension().is_some_and(|ext| ext == "json") {
        let spec: MethodCoverageSpec = serde_json::from_str(text).ok()?;
        if spec.all_methods.is_empty() {
            return None;
        }
        return Some(spec.all_methods.into_iter().collect());
    }

    let methods = text
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(ToOwned::to_owned)
        .collect::<BTreeSet<_>>();

    (!methods.is_empty()).then_some(methods)
}

fn collect_rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
    let entries = match fs::read_dir(dir) {
        Ok(entries) => entries,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_rust_files(&path, out);
            continue;
        }
        if path.extension().is_some_and(|ext| ext == "rs") {
            out.push(path);
        }
    }
}

#[test]
fn telegram_bot_api_methods_are_fully_covered() -> Result<(), Box<dyn std::error::Error>> {
    let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let workspace_root = crate_root
        .parent()
        .and_then(|path| path.parent())
        .map(Path::to_path_buf)
        .unwrap_or_else(|| crate_root.clone());

    let mut candidate_paths = Vec::new();
    if let Ok(path) = env::var("TELE_METHOD_COVERAGE_SPEC_PATH") {
        candidate_paths.push(PathBuf::from(path));
    }
    candidate_paths.push(workspace_root.join("crates/tele-codegen/spec/bot_api.json"));

    let mut expected_methods = None;
    for path in &candidate_paths {
        if let Ok(text) = fs::read_to_string(path)
            && let Some(methods) = parse_expected_methods(&text, path)
        {
            expected_methods = Some(methods);
            break;
        }
    }

    let expected_methods = match expected_methods {
        Some(methods) => methods,
        None => {
            assert!(
                !EMBEDDED_METHOD_SPEC.trim().is_empty(),
                "embedded method spec fixture is empty"
            );
            parse_expected_methods(EMBEDDED_METHOD_SPEC, Path::new("bot_api_all_methods.txt"))
                .ok_or_else(|| {
                    std::io::Error::other("embedded method spec fixture must contain methods")
                })?
        }
    };

    let api_dir = crate_root.join("src/api");
    let mut api_files = Vec::new();
    collect_rust_files(&api_dir, &mut api_files);

    let mut api_sources = Vec::new();
    let mut unreadable_api_files = Vec::new();
    for path in api_files {
        match fs::read_to_string(&path) {
            Ok(source) => api_sources.push(source),
            Err(_) => unreadable_api_files.push(path),
        }
    }
    assert!(
        unreadable_api_files.is_empty(),
        "failed to read api source files: {unreadable_api_files:?}"
    );

    let mut covered_methods = BTreeSet::new();
    for method in &expected_methods {
        let needle = format!("\"{method}\"");
        if api_sources.iter().any(|source| source.contains(&needle)) {
            covered_methods.insert(method.clone());
        }
    }

    let missing_methods: Vec<String> = expected_methods
        .difference(&covered_methods)
        .cloned()
        .collect();

    assert!(
        missing_methods.is_empty(),
        "missing Telegram Bot API methods in service layer: {missing_methods:?}"
    );

    Ok(())
}