lark-channel 0.5.0

Lark/Feishu Channel SDK for Rust
Documentation
use std::collections::{BTreeMap, VecDeque};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex, MutexGuard};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

use serde_json::{Value, json};
use url::Url;

use crate::Result;
use crate::lark_openapi::{BoxFuture, HttpMethod, HttpRequest, HttpResponse, OpenApiTransport};

pub(super) fn assert_card_content_update(call: &FakeCall, sequence: u32, content: &str) {
    assert_eq!(call.method, HttpMethod::Put);
    assert!(call.url.as_str().contains("/elements/stream_md/content"));
    assert_eq!(call.body["sequence"], sequence);
    assert_eq!(call.body["content"], content);
    assert!(
        call.body["uuid"]
            .as_str()
            .is_some_and(|uuid| { uuid.starts_with("lc-") && uuid.chars().count() <= 64 })
    );
}

pub(super) fn token_response() -> HttpResponse {
    HttpResponse::json(
        200,
        json!({
            "code": 0,
            "msg": "ok",
            "tenant_access_token": "tenant-token-1",
            "expire": 7200
        }),
    )
}

pub(super) fn card_response(card_id: &str) -> HttpResponse {
    HttpResponse::json(
        200,
        json!({ "code": 0, "msg": "ok", "data": { "card_id": card_id } }),
    )
}

pub(super) fn message_response(message_id: &str) -> HttpResponse {
    HttpResponse::json(
        200,
        json!({ "code": 0, "msg": "ok", "data": { "message_id": message_id } }),
    )
}

pub(super) fn ok_response() -> HttpResponse {
    HttpResponse::json(200, json!({ "code": 0, "msg": "ok", "data": {} }))
}

#[derive(Debug, Clone)]
pub(super) struct FakeTransport {
    state: Arc<Mutex<FakeState>>,
}

impl FakeTransport {
    pub(super) fn new(responses: Vec<Result<HttpResponse>>) -> Self {
        Self {
            state: Arc::new(Mutex::new(FakeState {
                responses: responses.into_iter().map(FakeResponse::from).collect(),
                calls: Vec::new(),
            })),
        }
    }

    pub(super) fn http(responses: Vec<HttpResponse>) -> Self {
        Self::new(responses.into_iter().map(Ok).collect())
    }

    pub(super) fn scripted(responses: Vec<FakeResponse>) -> Self {
        Self {
            state: Arc::new(Mutex::new(FakeState {
                responses: responses.into(),
                calls: Vec::new(),
            })),
        }
    }

    pub(super) fn calls(&self) -> Vec<FakeCall> {
        self.state().calls.clone()
    }

    fn state(&self) -> MutexGuard<'_, FakeState> {
        self.state.lock().expect("fake transport state poisoned")
    }
}

impl OpenApiTransport for FakeTransport {
    fn send_json(&self, request: HttpRequest) -> BoxFuture<'static, Result<HttpResponse>> {
        let response = {
            let mut state = self.state();
            state.calls.push(FakeCall {
                method: request.method,
                url: request.url,
                headers: request.headers,
                body: request.body,
            });
            state.responses.pop_front().expect("fake response")
        };

        match response {
            FakeResponse::Ready(response) => Box::pin(async move { response }),
            FakeResponse::PendingOnce(response) => Box::pin(PendingOnceResponse {
                response: Some(response),
                polled: false,
            }),
        }
    }
}

#[derive(Debug)]
struct FakeState {
    responses: VecDeque<FakeResponse>,
    calls: Vec<FakeCall>,
}

#[derive(Debug)]
pub(super) enum FakeResponse {
    Ready(Result<HttpResponse>),
    PendingOnce(Result<HttpResponse>),
}

impl FakeResponse {
    pub(super) fn ready(response: HttpResponse) -> Self {
        Self::Ready(Ok(response))
    }

    pub(super) fn pending_once(response: HttpResponse) -> Self {
        Self::PendingOnce(Ok(response))
    }
}

impl From<Result<HttpResponse>> for FakeResponse {
    fn from(response: Result<HttpResponse>) -> Self {
        Self::Ready(response)
    }
}

struct PendingOnceResponse {
    response: Option<Result<HttpResponse>>,
    polled: bool,
}

impl Future for PendingOnceResponse {
    type Output = Result<HttpResponse>;

    fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
        if !self.polled {
            self.polled = true;
            return Poll::Pending;
        }
        Poll::Ready(
            self.response
                .take()
                .expect("pending response consumed once"),
        )
    }
}

#[derive(Clone, Debug)]
pub(super) struct FakeCall {
    pub(super) method: HttpMethod,
    pub(super) url: Url,
    #[allow(dead_code)]
    headers: BTreeMap<String, String>,
    pub(super) body: Value,
}

pub(super) fn block_on<F>(future: F) -> F::Output
where
    F: Future,
{
    let waker = noop_waker();
    let mut context = Context::from_waker(&waker);
    let mut future = Box::pin(future);

    match future.as_mut().poll(&mut context) {
        Poll::Ready(output) => output,
        Poll::Pending => panic!("test future unexpectedly pending"),
    }
}

pub(super) fn assert_pending_once<F>(future: F)
where
    F: Future,
{
    let waker = noop_waker();
    let mut context = Context::from_waker(&waker);
    let mut future = Box::pin(future);

    assert!(matches!(future.as_mut().poll(&mut context), Poll::Pending));
}

fn noop_waker() -> Waker {
    unsafe { Waker::from_raw(noop_raw_waker()) }
}

fn noop_raw_waker() -> RawWaker {
    fn clone(_: *const ()) -> RawWaker {
        noop_raw_waker()
    }

    fn wake(_: *const ()) {}
    fn wake_by_ref(_: *const ()) {}
    fn drop(_: *const ()) {}

    RawWaker::new(
        std::ptr::null(),
        &RawWakerVTable::new(clone, wake, wake_by_ref, drop),
    )
}