use std::fmt::Write as _;
use crate::error::{HonchoError, Result};
pub(crate) const API_BASE_PATH: &str = "v3";
fn encode(s: &str) -> String {
let mut encoded = String::with_capacity(s.len() * 3);
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
encoded.push(byte as char);
}
_ => {
let _ = write!(encoded, "%{byte:02X}");
}
}
}
encoded
}
fn validate_id(id: &str, name: &str) -> Result<()> {
if id.trim().is_empty() {
return Err(HonchoError::Validation(format!(
"{name} must not be empty or whitespace-only"
)));
}
let lower = id.to_ascii_lowercase();
if matches!(
lower.as_str(),
"." | ".." | "%2e" | "%2e%2e" | ".%2e" | "%2e."
) {
return Err(HonchoError::Validation(format!(
"{name} must not be a dot-segment ('.' or '..')"
)));
}
Ok(())
}
macro_rules! route {
(
$(#[doc = $doc:literal])*
$vis:vis fn $name:ident(
$($param:ident : $param_name:literal),* $(,)?
) -> Result<String> {
$template:literal
}
) => {
$(#[doc = $doc])*
$vis fn $name($($param: &str),*) -> Result<String> {
$(validate_id($param, $param_name)?;)*
Ok(format!(
$template,
$($param = encode($param)),*
))
}
};
}
pub(crate) fn workspaces() -> String {
format!("/{API_BASE_PATH}/workspaces")
}
pub(crate) fn workspaces_list() -> String {
format!("/{API_BASE_PATH}/workspaces/list")
}
route! {
pub(crate) fn workspace(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}"
}
}
route! {
pub(crate) fn workspace_search(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/search"
}
}
route! {
pub(crate) fn workspace_queue_status(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/queue/status"
}
}
route! {
pub(crate) fn workspace_schedule_dream(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/schedule_dream"
}
}
route! {
pub(crate) fn peers(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers"
}
}
route! {
pub(crate) fn peers_list(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/list"
}
}
route! {
pub(crate) fn peer(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}"
}
}
route! {
pub(crate) fn peer_chat(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/chat"
}
}
route! {
pub(crate) fn peer_representation(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/representation"
}
}
route! {
pub(crate) fn peer_card(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/card"
}
}
route! {
pub(crate) fn peer_context(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/context"
}
}
route! {
pub(crate) fn peer_search(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/search"
}
}
route! {
pub(crate) fn peer_sessions_list(
workspace_id: "workspace_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/peers/{peer_id}/sessions"
}
}
route! {
pub(crate) fn sessions(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions"
}
}
route! {
pub(crate) fn sessions_list(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/list"
}
}
route! {
pub(crate) fn session(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}"
}
}
route! {
pub(crate) fn session_clone(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/clone"
}
}
route! {
pub(crate) fn session_context(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/context"
}
}
route! {
pub(crate) fn session_summaries(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/summaries"
}
}
route! {
pub(crate) fn session_search(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/search"
}
}
route! {
pub(crate) fn session_peers(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/peers"
}
}
route! {
pub(crate) fn session_peer_config(
workspace_id: "workspace_id",
session_id: "session_id",
peer_id: "peer_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/peers/{peer_id}/config"
}
}
route! {
pub(crate) fn messages(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/messages"
}
}
route! {
pub(crate) fn messages_list(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/messages/list"
}
}
route! {
pub(crate) fn message(
workspace_id: "workspace_id",
session_id: "session_id",
message_id: "message_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/messages/{message_id}"
}
}
route! {
pub(crate) fn messages_upload(
workspace_id: "workspace_id",
session_id: "session_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/sessions/{session_id}/messages/upload"
}
}
route! {
pub(crate) fn conclusions(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/conclusions"
}
}
route! {
pub(crate) fn conclusions_list(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/conclusions/list"
}
}
route! {
pub(crate) fn conclusions_query(
workspace_id: "workspace_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/conclusions/query"
}
}
route! {
pub(crate) fn conclusion(
workspace_id: "workspace_id",
conclusion_id: "conclusion_id",
) -> Result<String> {
"/{API_BASE_PATH}/workspaces/{workspace_id}/conclusions/{conclusion_id}"
}
}
#[inline]
pub(crate) fn conclusion_delete(workspace_id: &str, conclusion_id: &str) -> Result<String> {
conclusion(workspace_id, conclusion_id)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn test_workspaces() {
assert_eq!(workspaces(), "/v3/workspaces");
}
#[test]
fn test_workspaces_list() {
assert_eq!(workspaces_list(), "/v3/workspaces/list");
}
#[test]
fn test_workspace() {
assert_eq!(workspace("ws1").unwrap(), "/v3/workspaces/ws1");
}
#[test]
fn test_workspace_search() {
assert_eq!(
workspace_search("ws1").unwrap(),
"/v3/workspaces/ws1/search"
);
}
#[test]
fn test_workspace_queue_status() {
assert_eq!(
workspace_queue_status("ws1").unwrap(),
"/v3/workspaces/ws1/queue/status"
);
}
#[test]
fn test_workspace_schedule_dream() {
assert_eq!(
workspace_schedule_dream("ws1").unwrap(),
"/v3/workspaces/ws1/schedule_dream"
);
}
#[test]
fn test_peers() {
assert_eq!(peers("ws1").unwrap(), "/v3/workspaces/ws1/peers");
}
#[test]
fn test_peers_list() {
assert_eq!(peers_list("ws1").unwrap(), "/v3/workspaces/ws1/peers/list");
}
#[test]
fn test_peer() {
assert_eq!(
peer("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice"
);
}
#[test]
fn test_peer_chat() {
assert_eq!(
peer_chat("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/chat"
);
}
#[test]
fn test_peer_representation() {
assert_eq!(
peer_representation("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/representation"
);
}
#[test]
fn test_peer_card() {
assert_eq!(
peer_card("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/card"
);
}
#[test]
fn test_peer_context() {
assert_eq!(
peer_context("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/context"
);
}
#[test]
fn test_peer_search() {
assert_eq!(
peer_search("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/search"
);
}
#[test]
fn test_peer_sessions_list() {
assert_eq!(
peer_sessions_list("ws1", "alice").unwrap(),
"/v3/workspaces/ws1/peers/alice/sessions"
);
}
#[test]
fn test_sessions() {
assert_eq!(sessions("ws1").unwrap(), "/v3/workspaces/ws1/sessions");
}
#[test]
fn test_sessions_list() {
assert_eq!(
sessions_list("ws1").unwrap(),
"/v3/workspaces/ws1/sessions/list"
);
}
#[test]
fn test_session() {
assert_eq!(
session("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1"
);
}
#[test]
fn test_session_clone() {
assert_eq!(
session_clone("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/clone"
);
}
#[test]
fn test_session_context() {
assert_eq!(
session_context("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/context"
);
}
#[test]
fn test_session_summaries() {
assert_eq!(
session_summaries("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/summaries"
);
}
#[test]
fn test_session_search() {
assert_eq!(
session_search("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/search"
);
}
#[test]
fn test_session_peers() {
assert_eq!(
session_peers("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/peers"
);
}
#[test]
fn test_session_peer_config() {
assert_eq!(
session_peer_config("ws1", "sess1", "alice").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/peers/alice/config"
);
}
#[test]
fn test_messages() {
assert_eq!(
messages("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/messages"
);
}
#[test]
fn test_messages_list() {
assert_eq!(
messages_list("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/messages/list"
);
}
#[test]
fn test_message() {
assert_eq!(
message("ws1", "sess1", "msg1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/messages/msg1"
);
}
#[test]
fn test_messages_upload() {
assert_eq!(
messages_upload("ws1", "sess1").unwrap(),
"/v3/workspaces/ws1/sessions/sess1/messages/upload"
);
}
#[test]
fn test_conclusions() {
assert_eq!(
conclusions("ws1").unwrap(),
"/v3/workspaces/ws1/conclusions"
);
}
#[test]
fn test_conclusions_list() {
assert_eq!(
conclusions_list("ws1").unwrap(),
"/v3/workspaces/ws1/conclusions/list"
);
}
#[test]
fn test_conclusions_query() {
assert_eq!(
conclusions_query("ws1").unwrap(),
"/v3/workspaces/ws1/conclusions/query"
);
}
#[test]
fn test_conclusion() {
assert_eq!(
conclusion("ws1", "conc1").unwrap(),
"/v3/workspaces/ws1/conclusions/conc1"
);
}
#[test]
fn test_workspace_empty_id_returns_validation_error() {
let err = workspace("").unwrap_err();
assert!(matches!(err, HonchoError::Validation(_)));
assert!(format!("{err}").contains("workspace_id"));
}
#[test]
fn test_peer_empty_peer_id_returns_validation_error() {
let err = peer("ws1", "").unwrap_err();
assert!(matches!(err, HonchoError::Validation(_)));
assert!(format!("{err}").contains("peer_id"));
}
#[test]
fn test_session_empty_session_id_returns_validation_error() {
let err = session("ws1", "").unwrap_err();
assert!(matches!(err, HonchoError::Validation(_)));
assert!(format!("{err}").contains("session_id"));
}
#[test]
fn test_message_empty_message_id_returns_validation_error() {
let err = message("ws1", "sess1", "").unwrap_err();
assert!(matches!(err, HonchoError::Validation(_)));
assert!(format!("{err}").contains("message_id"));
}
#[test]
fn test_conclusion_empty_id_returns_validation_error() {
let err = conclusion("ws1", "").unwrap_err();
assert!(matches!(err, HonchoError::Validation(_)));
assert!(format!("{err}").contains("conclusion_id"));
}
#[test]
fn test_workspace_dot_segment_rejected() {
assert!(matches!(
workspace(".").unwrap_err(),
HonchoError::Validation(_)
));
assert!(matches!(
workspace("..").unwrap_err(),
HonchoError::Validation(_)
));
}
#[test]
fn test_workspace_percent_encoded_dot_rejected() {
assert!(matches!(
workspace("%2e").unwrap_err(),
HonchoError::Validation(_)
));
assert!(matches!(
workspace("%2e%2e").unwrap_err(),
HonchoError::Validation(_)
));
assert!(matches!(
workspace("%2E").unwrap_err(),
HonchoError::Validation(_)
));
}
#[test]
fn test_workspace_mixed_encoded_dot_rejected() {
for id in [".%2e", "%2e.", ".%2E", "%2E."] {
assert!(
matches!(workspace(id).unwrap_err(), HonchoError::Validation(_)),
"mixed dot-segment {id:?} should be rejected"
);
}
}
#[test]
fn test_workspace_whitespace_only_rejected() {
assert!(matches!(
workspace(" ").unwrap_err(),
HonchoError::Validation(_)
));
}
#[test]
fn test_encode_space() {
assert_eq!(encode("hello world"), "hello%20world");
}
#[test]
fn test_encode_slash() {
assert_eq!(encode("a/b"), "a%2Fb");
}
#[test]
fn test_encode_unicode() {
assert_eq!(encode("café"), "caf%C3%A9");
}
#[test]
fn test_encode_percent() {
assert_eq!(encode("100%"), "100%25");
}
#[test]
fn test_encode_unreserved() {
assert_eq!(encode("A-Z.0-9_-~"), "A-Z.0-9_-~");
}
}