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");
}
}