webhookloader 0.1.1

Trigger HTTP webhooks by name from a TOML config. Small async library with retries and automatic Content-Type detection.
Documentation

webhookloader

webhookloader — Trigger webhooks by a simple nameA small, focused Rust library for firing HTTP webhooks by name. You keep a TOML file that maps a human-friendly name to a destination URL and optional request body. Your code then calls one async function with the name, and webhookloader takes care of:•Loading the mapping from a TOML file•Choosing the right request Content-Type automatically•Posting the request with a small retry loop•Returning descriptive errors when something goes wrongThis helps you decouple network endpoints and payloads from your application code, keeping them in config instead of hardcoding.Features at a glance•Name → URL (and optional body) mapping via TOML•Async API based on reqwest + tokio•Automatic Content-Type detection:•application/json if the body begins with “{” or “[”•text/plain; charset=utf-8 otherwise•Basic retry logic: up to 10 attempts on request errors or non-2xx responses•Clear errors for missing hook names or empty URLsInstallationAdd to your Cargo.toml (one of the following):•From crates.io (when published):•webhookloader = "0.1"•Local path (during development):•webhookloader = { path = "../webhookloader" }Your application must use tokio since the API is async:•tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Configurationwebhookloader reads a TOML file into a simple map. Each key is the webhook name. Each value is either:•A URL, e.g. 'https://example.com/hook'•A URL followed by ":::" and a body, e.g. 'https://example.com/hook:::{"json":"payload"}'Use single quotes around strings that contain double-quotes to avoid heavy escaping in TOML.Example config.toml:light_on = 'https://example.com/api/light/on' light_off = 'https://example.com/api/light/off' nightlight = 'https://example.com/api/nightlight'

With an inline JSON body:

local_json = 'http://127.0.0.1:8080/echo:::{"spicy":"meatball"}'

With a plain-text body (content-type will be text/plain; charset=utf-8):

plain_note = 'http://127.0.0.1:8080/notify:::Hello from webhookloader!'Config path resolution within the library:1)If the environment variable WEBHOOKLOADER_CONFIG is set, it is used.2)Otherwise, a file named config.toml in the current working directory is used.Setting the environment variable:•macOS/Linux: export WEBHOOKLOADER_CONFIG=/path/to/config.toml•Windows (PowerShell): $env:WEBHOOKLOADER_CONFIG = "C:\path\to\config.toml"Public API•send_webhook_by_name(name: &str) -> Result<(), WebHookError>•Loads the config using the resolution order above and sends the named webhook.•send_webhook_by_name_with_config(config_path: &str, name: &str) -> Result<(), WebHookError>•Uses an explicit config path.•WebHookLoader::new(config_path: &str) -> Result<WebHookLoader, WebHookError>•Loads the mapping; allows reuse for multiple sends.•WebHookLoader::list_hooks(&self) -> Result<Vec, WebHookError>•Returns the configured hook names.•WebHookLoader::fire_hook_result(&self, name: &str) -> Result<(), WebHookError>•Fires a hook by name and returns a Result.Behavior details:•Body is optional. If present and starts with '{' or '[', the request uses Content-Type: application/json; otherwise text/plain; charset=utf-8.•10 retry attempts on errors or non-success HTTP status codes.•Errors include unknown webhook names and empty URLs.Code examples1) Minimal usage with default config resolutionuse webhookloader::send_webhook_by_name;

#[tokio::main] async fn main() -> Result<(), Box> { // Optionally choose a config path via env var: // std::env::set_var("WEBHOOKLOADER_CONFIG", "./my_config.toml");

// Fire the webhook named "light_on"
send_webhook_by_name("light_on").await?;
Ok(())

}2) Using an explicit config pathuse webhookloader::send_webhook_by_name_with_config;

#[tokio::main] async fn main() -> Result<(), Box> { send_webhook_by_name_with_config("./config.toml", "local_json").await?; Ok(()) }3) Reusing a loader in a long-running appuse webhookloader::WebHookLoader;

#[tokio::main] async fn main() -> Result<(), Box> { let loader = WebHookLoader::new("./config.toml")?;

// List what’s available
let names = loader.list_hooks()?;
println!("Available hooks: {}", names.join(", "));

// Fire several hooks without reloading the file each time
loader.fire_hook_result("light_on").await?;
loader.fire_hook_result("nightlight").await?;

Ok(())

}4) Handling errors explicitlyuse webhookloader::{send_webhook_by_name, WebHookError};

#[tokio::main] async fn main() { match send_webhook_by_name("not_configured").await { Ok(()) => println!("Webhook sent"), Err(WebHookError::ConfigError(e)) => eprintln!("Config error: {e}"), Err(WebHookError::ExecutionError(msg)) => eprintln!("Send failed: {msg}"), } }5) Example TOML with JSON vs. plain-text bodies# JSON body ⇒ Content-Type: application/json create_issue = 'https://api.example.com/issues:::{"title":"Bug","labels":["p1","bug"]}'

Plain-text body ⇒ Content-Type: text/plain; charset=utf-8

ping = 'https://hooks.example.com/ping:::ping'How Content-Type is chosenwebhookloader uses a simple heuristic:•If the body starts with '{' or '[', it sends Content-Type: application/json•Otherwise, Content-Type: text/plain; charset=utf-8If you need other content types or custom headers, consider placing your server behind an adapter that accepts these defaults, or fork/extend the library to support custom headers.Retries and timeouts•Retries: Up to 10 attempts are made if the request fails or if the response status is not 2xx. The last error is returned.•Timeouts: The library uses reqwest’s default client. If you need specific timeouts, consider wrapping calls in tokio::time::timeout in your application, or extend the library to accept a configured Reqwest client.Tips and best practices•Keep secrets out of your repo. Avoid committing production URLs or tokens. Instead, point WEBHOOKLOADER_CONFIG to a secure location at deploy time.•Validate your config early at startup by constructing WebHookLoader and checking list_hooks().•Prefer single quotes around TOML strings that contain JSON bodies to avoid tedious escaping.•For high-throughput services, build a single WebHookLoader once and reuse it.FAQ•How do I send a POST with no body?•Just map a name to a plain URL (no ":::"). The library will send an empty body.•What happens on HTTP 500 or 429?•It retries up to 10 times. If all attempts fail or remain non-2xx, you get a WebHookError::ExecutionError with the last observed error or status.•Can I pass custom headers or authentication tokens?•Not currently. A simple Content-Type header is set automatically based on body detection. For more advanced scenarios, extend the library to support headers or templating.Minimal end-to-end test idea (local)Run a temporary local HTTP server (Node, Python, etc.) to confirm behavior. For example, with Python 3 and a quick Flask app:# save as echo.py from flask import Flask, request app = Flask(name) @app.post('/echo') def echo(): print('CT:', request.headers.get('Content-Type')) print('Body:', request.data.decode('utf-8')) return 'ok', 200

if name == 'main': app.run(port=8080)Then set in config.toml:local_json = 'http://127.0.0.1:8080/echo:::{"hello":"world"}' local_plain = 'http://127.0.0.1:8080/echo:::hello'Call from Rust using send_webhook_by_name("local_json") or ("local_plain"). Observe the printed Content-Type and body.Compatibility•Rust edition: 2024 (as in Cargo.toml)•Runtime: tokio 1.x with multi-threaded runtime recommended•HTTP client: reqwest 0.12.xIf you need additional examples or extensions (custom headers, templated payloads, per-hook retry/timeout), let me know what you prefer and I can sketch an API that fits this crate.

A tiny Rust library to trigger webhooks by name. It pulls the destination URL and optional request body from a TOML config file, and POSTs when you call a single function with the webhook's name.

What you get:

  • Name → URL (and optional body) mapping loaded from config.
  • Simple async API to send by name.
  • Automatic Content-Type selection: JSON for bodies starting with { or [; otherwise text/plain.
  • Basic retry logic (10 attempts) and error reporting.

Install

In your app's Cargo.toml (use one of the following):

Crates.io (when published):

  • webhookloader = "0.1"

Local path (while developing locally):

  • webhookloader = { path = "../webhookloader" }

You'll also need Tokio in your application if you call the async API:

  • tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Config

webhookloader reads a TOML file into a simple map. Each key is the webhook name. Each value is either:

Note: Use single quotes in TOML if your body contains double quotes to avoid escaping.

Example config.toml:

light_on = 'https://example.com/api/light/on' light_off = 'https://example.com/api/light/off' nightlight = 'https://example.com/api/nightlight' local = 'http://127.0.0.1:8080/echo:::{"spicy":"meatball"}'

Config path resolution inside this crate:

  1. If the environment variable WEBHOOKLOADER_CONFIG is set, it is used.
  2. Otherwise, "config.toml" in the current working directory is used.

Library API

  • send_webhook_by_name(name: &str) -> Result<(), WebHookError>
    • Loads config using the resolution order above and sends the named webhook.
  • send_webhook_by_name_with_config(config_path: &str, name: &str) -> Result<(), WebHookError>
    • Uses a specific config file path.
  • WebHookLoader::new(config_path) -> Result<WebHookLoader, WebHookError>
    • Loads the mapping and lets you call loader.fire_hook_result(name).await.
  • WebHookLoader::list_hooks() -> Result<Vec, WebHookError>

Behavior details:

  • Body is optional. If present and starts with '{' or '[', Content-Type is application/json; otherwise text/plain.
  • 10 retry attempts on non-success status codes or request errors.
  • Descriptive errors when a webhook name is missing or URL is empty.

Usage examples

Async main:

use webhookloader::send_webhook_by_name;

#[tokio::main] async fn main() -> Result<(), Box> { // Optional: override config path // std::env::set_var("WEBHOOKLOADER_CONFIG", "./my_config.toml");

// Send a configured webhook by name
send_webhook_by_name("light_on").await?;
Ok(())

}

Explicit config path:

use webhookloader::send_webhook_by_name_with_config;

#[tokio::main] async fn main() -> Result<(), Box> { send_webhook_by_name_with_config("./config.toml", "local").await?; Ok(()) }

Using the loader directly and listing hooks:

use webhookloader::WebHookLoader;

#[tokio::main] async fn main() -> Result<(), Box> { let loader = WebHookLoader::new("./config.toml")?; println!("Available hooks: {:?}", loader.list_hooks()?); loader.fire_hook_result("nightlight").await?; Ok(()) }

Notes for integration in another Junie project

  • Ensure your project depends on tokio (macros + rt-multi-thread) and on this crate (path or version).
  • Place a config.toml in your runtime working directory or set WEBHOOKLOADER_CONFIG to point to it.
  • Call send_webhook_by_name("your_hook").await whenever you need to trigger a webhook, keeping your network details in config, not in code.
  • If you need custom behavior, construct a WebHookLoader once and reuse it.