rustymilky 0.1.0

Milky 协议的 Rust SDK
Documentation
use anyhow::anyhow;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;

use crate::{Result, protocol::ApiEndpoint};

pub(crate) fn build_event_url(base_url: &reqwest::Url) -> Result<reqwest::Url> {
  extend_url(base_url, &["event"])
}

pub(crate) fn build_api_url<T: ApiEndpoint>(base_url: &reqwest::Url) -> Result<reqwest::Url> {
  extend_url(base_url, &["api", T::NAME])
}

fn extend_url(base_url: &reqwest::Url, segments: &[&str]) -> Result<reqwest::Url> {
  let mut url = base_url.clone();
  let mut path_segments = url
    .path_segments_mut()
    .map_err(|()| anyhow!("Base URL cannot be used for path joins"))?;
  path_segments.pop_if_empty();

  for segment in segments {
    path_segments.push(segment);
  }

  drop(path_segments);

  Ok(url)
}

pub(crate) fn websocket_url(event_url: &reqwest::Url) -> Result<reqwest::Url> {
  let mut websocket_url = event_url.clone();

  match websocket_url.scheme() {
    "http" => websocket_url
      .set_scheme("ws")
      .map_err(|()| anyhow!("Failed to convert event URL to ws scheme"))?,
    "https" => websocket_url
      .set_scheme("wss")
      .map_err(|()| anyhow!("Failed to convert event URL to wss scheme"))?,
    scheme => {
      return Err(anyhow!(
        "Unsupported URL scheme for WebSocket transport: {scheme}"
      ));
    }
  }

  Ok(websocket_url)
}

pub(crate) fn websocket_request(
  event_url: &reqwest::Url,
  authorization: Option<&reqwest::header::HeaderValue>,
) -> Result<tokio_tungstenite::tungstenite::handshake::client::Request> {
  let websocket_url = websocket_url(event_url)?;
  let mut request = websocket_url.as_str().into_client_request()?;

  if let Some(authorization) = authorization {
    request
      .headers_mut()
      .insert(reqwest::header::AUTHORIZATION, authorization.clone());
  }

  Ok(request)
}

pub(crate) fn build_sse_request(
  client: &reqwest::Client,
  url: reqwest::Url,
  last_event_id: Option<&str>,
) -> reqwest::RequestBuilder {
  let mut request = client.get(url);

  if let Some(last_event_id) = last_event_id {
    request = request.header("last-event-id", last_event_id);
  }

  request
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::protocol::GetImplInfoInput;

  #[test]
  fn converts_http_event_url_to_ws() {
    let base_url = reqwest::Url::parse("http://127.0.0.1:3100/").unwrap();
    let event_url = build_event_url(&base_url).unwrap();
    let ws_url = websocket_url(&event_url).unwrap();

    assert_eq!(ws_url.as_str(), "ws://127.0.0.1:3100/event");
  }

  #[test]
  fn converts_https_event_url_to_wss() {
    let base_url = reqwest::Url::parse("https://milky.example/").unwrap();
    let event_url = build_event_url(&base_url).unwrap();
    let ws_url = websocket_url(&event_url).unwrap();

    assert_eq!(ws_url.as_str(), "wss://milky.example/event");
  }

  #[test]
  fn rejects_unsupported_websocket_url_schemes() {
    let url = reqwest::Url::parse("ftp://milky.example/event").unwrap();

    let error = websocket_url(&url).unwrap_err();

    assert!(error.to_string().contains("Unsupported URL scheme"));
  }

  #[test]
  fn builds_websocket_handshake_with_authorization_header() {
    let event_url = reqwest::Url::parse("http://127.0.0.1:3100/event").unwrap();
    let authorization = reqwest::header::HeaderValue::from_static("Bearer token");

    let request = websocket_request(&event_url, Some(&authorization)).unwrap();

    assert_eq!(request.uri().to_string(), "ws://127.0.0.1:3100/event");
    assert_eq!(
      request.headers().get(reqwest::header::AUTHORIZATION),
      Some(&authorization)
    );
  }

  #[test]
  fn preserves_base_path_for_api_urls_without_trailing_slash() {
    let base_url = reqwest::Url::parse("https://milky.example/base").unwrap();

    let api_url = build_api_url::<GetImplInfoInput>(&base_url).unwrap();

    assert_eq!(
      api_url.as_str(),
      "https://milky.example/base/api/get_impl_info"
    );
  }

  #[test]
  fn preserves_base_path_for_event_urls_without_trailing_slash() {
    let base_url = reqwest::Url::parse("https://milky.example/base").unwrap();

    let event_url = build_event_url(&base_url).unwrap();

    assert_eq!(event_url.as_str(), "https://milky.example/base/event");
  }
}