hooksmith-core 0.1.3

Shared HTTP client and WebhookSender trait for the hooksmith crates
Documentation

hooksmith-core

Shared building blocks for the hooksmith family of webhook crates.

This crate is not a webhook client itself. It provides the common trait, HTTP client, error type, and utilities that every *_hook service crate builds on.


What's inside

WebhookSender trait

The single abstraction every service crate implements. Application code can be generic over the backend:

use hooksmith_core::WebhookSender;

async fn notify<S>(sender: &S, msg: &S::Message) -> Result<(), S::Error>
where
    S: WebhookSender,
{
    sender.send(msg).await
}

send_batch

A default method that fans out a slice of messages concurrently using [futures::future::join_all]. One failure does not abort the others — you get back one Result per message.

let results = sender.send_batch(&[&msg_a, &msg_b, &msg_c]).await;
for result in results {
    result?;
}

HttpClient

A thin wrapper around [reqwest::Client] with a sensible 30-second default timeout. Every *_hook crate holds one and calls post_json to fire requests.

use hooksmith_core::HttpClient;

let client = HttpClient::new();
let resp = client.post_json("https://hooks.example.com/...", &payload).await?;

Pass your own reqwest::Client to share a connection pool or customise TLS:

let inner = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(10))
    .build()?;
let client = HttpClient::with_reqwest(inner);

post_json_with_retry

Retries failed requests with exponential backoff according to a [RetryPolicy]. Pair it with the tracing feature to get per-attempt observability at no extra effort.

use hooksmith_core::{HttpClient, RetryPolicy};
use std::time::Duration;

let policy = RetryPolicy {
    max_attempts: 4,
    base_delay:   Duration::from_millis(250),
    jitter:       true,
};

let resp = client.post_json_with_retry(url, &payload, &policy).await?;

RetryPolicy

Controls how post_json_with_retry behaves.

Field Type Default Description
max_attempts u32 3 Total tries including the first (clamped to ≥ 1)
base_delay Duration 500 ms Delay before the first retry (doubles each time)
jitter bool true Add a random sub-delay to spread concurrent retries
// Use defaults
let policy = RetryPolicy::default();

// Customise
let policy = RetryPolicy { max_attempts: 5, ..Default::default() };

CoreError

A transport-level error enum that service-specific errors should wrap via #[from], so generic code can match on network or JSON failures without knowing which service is in use.

#[derive(Debug, thiserror::Error)]
pub enum DiscordError {
    #[error(transparent)]
    Core(#[from] hooksmith_core::CoreError),

    #[error("Discord API error {status}: {body}")]
    Api { status: u16, body: String },
}

Variants:

Variant Wraps When
Network reqwest::Error HTTP or connection-level failure
Json serde_json::Error Serialisation / deserialisation

Feature flags

Flag What it enables
mock Exposes hooksmith_core::mock::MockSender for use in tests
tracing Emits tracing spans around every post_json call (URL, status, latency)

mockMockSender

A WebhookSender that captures messages in memory instead of hitting a real endpoint. Add it to dev-dependencies so it is only compiled for tests:

[dev-dependencies]
hooksmith-core = { version = "*", features = ["mock"] }
use hooksmith_core::mock::MockSender;

let sender: MockSender<MyMessage> = MockSender::new();
sender.send(&my_message).await.unwrap();

assert_eq!(sender.len(), 1);
assert_eq!(sender.messages()[0], my_message);

tracing — observability spans

Enable the tracing feature to have every post_json call automatically instrumented with an info_span named hooksmith.post_json. The span records:

  • url — the request URL
  • status — HTTP status code (on success)
  • latency_ms — wall-clock time for the request
  • error — error string (on failure)
[dependencies]
hooksmith-core = { version = "*", features = ["tracing"] }

No other code changes are required — wire up your tracing subscriber as normal and the spans appear automatically.


Using hooksmith-core in a service crate

  1. Add it to your Cargo.toml:

    [dependencies]
    hooksmith-core = { version = "*" }
    
  2. Implement WebhookSender:

    use hooksmith_core::{HttpClient, WebhookSender, CoreError};
    
    pub struct MyClient {
        http: HttpClient,
        url:  String,
    }
    
    impl WebhookSender for MyClient {
        type Message = MyMessage;
        type Error   = MyError; // wraps CoreError
    
        fn send(&self, msg: &MyMessage) -> impl Future<Output = Result<(), MyError>> + Send {
            async move {
                self.http.post_json(&self.url, msg).await
                    .map_err(CoreError::from)
                    .map_err(MyError::Core)?;
                Ok(())
            }
        }
    }
    
  3. send_batch is inherited for free from the default trait implementation.