context69-sdk 0.5.0

Async Rust SDK for the Context69 HTTP API.
Documentation
use std::sync::Arc;

use axum::{
    Router,
    body::Bytes,
    extract::{Request, State},
    http::{HeaderMap, Method, StatusCode, Uri, header},
    response::{IntoResponse, Response},
    routing::any,
};
use serde::Serialize;
use tokio::{
    net::TcpListener,
    sync::{Mutex, oneshot},
};

use super::super::Context69Client;

pub const TEST_PAT: &str = "ctx_pat_test_token";

#[derive(Debug)]
pub struct CapturedRequest {
    pub method: Method,
    pub uri: Uri,
    pub headers: HeaderMap,
    pub body: Bytes,
}

#[derive(Clone)]
struct CaptureState {
    sender: Arc<Mutex<Option<oneshot::Sender<CapturedRequest>>>>,
    status: StatusCode,
    body: String,
}

pub async fn spawn_json<T: Serialize>(
    status: StatusCode,
    value: &T,
) -> (String, oneshot::Receiver<CapturedRequest>) {
    spawn(
        status,
        serde_json::to_string(value).expect("serialize response"),
    )
    .await
}

pub async fn spawn_empty(status: StatusCode) -> (String, oneshot::Receiver<CapturedRequest>) {
    spawn(status, String::new()).await
}

async fn spawn(status: StatusCode, body: String) -> (String, oneshot::Receiver<CapturedRequest>) {
    let (sender, receiver) = oneshot::channel();
    let state = CaptureState {
        sender: Arc::new(Mutex::new(Some(sender))),
        status,
        body,
    };
    let app = Router::new()
        .fallback(any(capture_handler))
        .with_state(state);
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("bind listener");
    let addr = listener.local_addr().expect("local addr");
    tokio::spawn(async move {
        axum::serve(listener, app).await.expect("serve app");
    });
    (format!("http://{addr}"), receiver)
}

async fn capture_handler(State(state): State<CaptureState>, request: Request) -> Response {
    let (parts, body) = request.into_parts();
    let body = axum::body::to_bytes(body, usize::MAX)
        .await
        .expect("read request body");
    if let Some(sender) = state.sender.lock().await.take() {
        let _ = sender.send(CapturedRequest {
            method: parts.method,
            uri: parts.uri,
            headers: parts.headers,
            body,
        });
    }
    (
        state.status,
        [(header::CONTENT_TYPE, "application/json")],
        state.body,
    )
        .into_response()
}

pub fn client(base_url: &str) -> Context69Client {
    Context69Client::builder()
        .base_url(base_url)
        .expect("base url")
        .with_personal_access_token(TEST_PAT)
        .expect("pat")
        .build()
        .expect("client")
}

pub fn assert_authorized(request: &CapturedRequest) {
    assert_eq!(
        request.headers.get(header::AUTHORIZATION).unwrap(),
        format!("Bearer {TEST_PAT}").as_str()
    );
}