apibrasil 1.0.0

SDK oficial Rust da plataforma APIBrasil: WhatsApp, SMS, consultas de CPF/CNPJ, veiculos, CEP, correios, pagamentos PIX/boleto e mais.
Documentation
//! Gera `src/generated/catalog.rs` a partir do catálogo público do
//! gateway APIBrasil (`GET /api/v2/documentations`).
//!
//! ```bash
//! cargo run --features blocking --bin codegen
//! APIBRASIL_BASE_URL=... cargo run --features blocking --bin codegen   # outra base
//! cargo run --features blocking --bin codegen -- caminho/catalog.rs
//! ```

use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;

use apibrasil::blocking::ApiBrasil;
use apibrasil::{Config, Error, ErrorKind, Json, Result};
use serde_json::Value;

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("codegen falhou: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> Result<()> {
    let output = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .unwrap_or_else(|| ["src", "generated", "catalog.rs"].iter().collect());

    let api = ApiBrasil::new(Config::new().timeout(Duration::from_secs(120)))?;
    let source = format!("{}/documentations", api.http().base_url());

    println!("Baixando catálogo de {source} ...");
    let response = api.catalog().documentations()?;
    let documentations = extract_documentations(&response);
    if documentations.is_empty() {
        return Err(Error::new(
            ErrorKind::Api,
            "O catálogo respondeu sem documentações.",
        ));
    }

    let mut service_actions: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
    let mut consulta_servicos: BTreeSet<String> = BTreeSet::new();
    let mut consulta_tipos: BTreeMap<String, (String, BTreeSet<String>)> = BTreeMap::new();
    let mut endpoint_count = 0usize;

    for documentation in &documentations {
        let Some(endpoints) = documentation.get("endpoints").and_then(Value::as_array) else {
            continue;
        };

        for endpoint in endpoints {
            let Some(path) = endpoint
                .get("url")
                .and_then(Value::as_str)
                .and_then(api_path)
            else {
                continue;
            };
            endpoint_count += 1;

            let (service, action) = match path.split_once('/') {
                Some((service, action)) => (service, action),
                None => (path.as_str(), ""),
            };
            if service.is_empty() {
                continue;
            }

            let actions = service_actions.entry(service.to_string()).or_default();
            if !action.is_empty() {
                actions.insert(action.to_string());
            }

            let Some(consulta) = consulta_service(&path) else {
                continue;
            };
            consulta_servicos.insert(consulta.clone());

            let Some(body) = endpoint.get("body").and_then(Value::as_object) else {
                continue;
            };
            let Some(tipo) = body.get("tipo").and_then(Value::as_str) else {
                continue;
            };
            if tipo.is_empty() {
                continue;
            }

            let fields = body
                .keys()
                .filter(|key| *key != "tipo" && *key != "homolog")
                .cloned()
                .collect();
            consulta_tipos.insert(tipo.to_string(), (consulta, fields));
        }
    }

    let rendered = render(
        &source,
        documentations.len(),
        endpoint_count,
        &service_actions,
        &consulta_servicos,
        &consulta_tipos,
    );

    if let Some(parent) = output.parent() {
        std::fs::create_dir_all(parent).map_err(io_error)?;
    }
    std::fs::write(&output, rendered).map_err(io_error)?;

    println!(
        "OK: {} ({} docs, {} endpoints, {} tipos)",
        output.display(),
        documentations.len(),
        endpoint_count,
        consulta_tipos.len()
    );
    Ok(())
}

fn io_error(error: std::io::Error) -> Error {
    Error::new(
        ErrorKind::Api,
        format!("Falha ao escrever o catálogo: {error}"),
    )
    .with_source(error)
}

/// O gateway responde `{"documentations": [...]}` ou uma lista direta —
/// que a SDK normaliza em `{"data": [...]}`.
fn extract_documentations(response: &Json) -> Vec<Json> {
    for key in ["documentations", "data"] {
        if let Some(items) = response.get(key).and_then(Value::as_array) {
            return items.clone();
        }
    }
    Vec::new()
}

/// Extrai o caminho depois de `/api/v2/`.
fn api_path(url: &str) -> Option<String> {
    let path = url.split_once("/api/v2/")?.1;
    let path = path.trim_matches('/');
    if path.is_empty() {
        None
    } else {
        Some(path.to_string())
    }
}

/// Devolve o serviço de uma rota `consulta/{servico}/credits`.
fn consulta_service(path: &str) -> Option<String> {
    let rest = path.strip_prefix("consulta/")?;
    let service = rest.strip_suffix("/credits")?;
    if service.is_empty() || service.contains('/') {
        None
    } else {
        Some(service.to_string())
    }
}

fn render(
    source: &str,
    doc_count: usize,
    endpoint_count: usize,
    service_actions: &BTreeMap<String, BTreeSet<String>>,
    consulta_servicos: &BTreeSet<String>,
    consulta_tipos: &BTreeMap<String, (String, BTreeSet<String>)>,
) -> String {
    let empty = BTreeSet::new();
    let mut out = String::with_capacity(64 * 1024);

    let _ = write!(
        out,
        r#"//! Dados do catálogo — ARQUIVO GERADO AUTOMATICAMENTE, não edite.
//!
//! Fonte: <{source}>
//! Regenerar: `cargo run --features blocking --bin codegen`
//!
//! {doc_count} documentações, {endpoint_count} endpoints, {tipo_count} tipos de
//! consulta conhecidos.

use super::ConsultaTipoMeta;

/// Fonte do catálogo.
pub const SOURCE: &str = "{source}";
"#,
        tipo_count = consulta_tipos.len(),
    );

    write_slice(
        &mut out,
        "WHATSAPP_ACTIONS",
        "Actions conhecidas da API de WhatsApp (`POST /whatsapp/{action}`).",
        service_actions.get("whatsapp").unwrap_or(&empty),
    );
    write_slice(
        &mut out,
        "EVOLUTION_PATHS",
        "Caminhos conhecidos da Evolution API\n/// (`POST /evolution/{controller}/{action}`).",
        service_actions.get("evolution").unwrap_or(&empty),
    );
    write_slice(
        &mut out,
        "WHATSMEOW_ACTIONS",
        "Actions conhecidas do WhatsMeow (`POST /whatsmeow/{action}`).",
        service_actions.get("whatsmeow").unwrap_or(&empty),
    );
    write_slice(
        &mut out,
        "CONSULTA_SERVICOS",
        "Serviços de consulta por crédito\n/// (`POST /consulta/{servico}/credits`).",
        consulta_servicos,
    );

    out.push_str(
        "\n/// Metadados por tipo de consulta (campo `tipo` do body), ordenados\n\
         /// por tipo.\n\
         pub static CONSULTA_TIPOS: &[(&str, ConsultaTipoMeta)] = &[\n",
    );
    for (tipo, (service, fields)) in consulta_tipos {
        let _ = writeln!(
            out,
            "    ({}, ConsultaTipoMeta {{ service: {}, fields: &[{}] }}),",
            quote(tipo),
            quote(service),
            quote_join(fields)
        );
    }
    out.push_str("];\n");

    out.push_str(
        "\n/// Actions documentadas por serviço do gateway, ordenadas por\n\
         /// serviço.\n\
         pub static SERVICE_ACTIONS: &[(&str, &[&str])] = &[\n",
    );
    for (service, actions) in service_actions {
        let _ = writeln!(out, "    ({}, &[{}]),", quote(service), quote_join(actions));
    }
    out.push_str("];\n");

    out
}

fn write_slice(out: &mut String, name: &str, doc: &str, values: &BTreeSet<String>) {
    let _ = write!(out, "\n/// {doc}\npub static {name}: &[&str] = &[\n");
    for value in values {
        let _ = writeln!(out, "    {},", quote(value));
    }
    out.push_str("];\n");
}

fn quote(value: &str) -> String {
    let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
    format!("\"{escaped}\"")
}

fn quote_join(values: &BTreeSet<String>) -> String {
    values
        .iter()
        .map(|value| quote(value))
        .collect::<Vec<_>>()
        .join(", ")
}