use std::time::Duration;
use reqwest::{Client, Method, RequestBuilder};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::{Error, Result};
pub const DEFAULT_USERNAME: &str = "opencode";
pub const SERVER_PASSWORD_ENV: &str = "OPENCODE_SERVER_PASSWORD";
#[derive(Clone, Debug)]
pub struct BasicAuth {
pub username: String,
pub password: String,
}
impl BasicAuth {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: username.into(),
password: password.into(),
}
}
pub fn from_env() -> Option<Self> {
std::env::var(SERVER_PASSWORD_ENV)
.ok()
.filter(|p| !p.is_empty())
.map(|password| Self {
username: DEFAULT_USERNAME.to_string(),
password,
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Scope {
pub directory: Option<String>,
pub workspace: Option<String>,
}
impl Scope {
pub fn is_empty(&self) -> bool {
self.directory.is_none() && self.workspace.is_none()
}
}
fn push_query(url: &mut String, sep: &mut char, key: &str, value: &str) {
url.push(*sep);
url.push_str(key);
url.push('=');
url.push_str(&encode_segment(value));
*sep = '&';
}
fn encode_segment(segment: &str) -> String {
const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(segment.len());
for &byte in segment.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(byte as char);
}
_ => {
out.push('%');
out.push(UPPER_HEX[(byte >> 4) as usize] as char);
out.push(UPPER_HEX[(byte & 0xf) as usize] as char);
}
}
}
out
}
#[derive(Clone, Debug)]
pub struct HttpTransport {
client: Client,
base_url: String,
timeout: Option<Duration>,
auth: Option<BasicAuth>,
scope: Scope,
}
impl HttpTransport {
pub fn new(
client: Client,
base_url: impl Into<String>,
timeout: Option<Duration>,
auth: Option<BasicAuth>,
scope: Scope,
) -> Self {
let base_url = base_url.into().trim_end_matches('/').to_string();
Self {
client,
base_url,
timeout,
auth,
scope,
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn auth(&self) -> Option<&BasicAuth> {
self.auth.as_ref()
}
pub fn scope(&self) -> &Scope {
&self.scope
}
fn append_scope(&self, url: &mut String, sep: &mut char) {
if let Some(directory) = &self.scope.directory {
push_query(url, sep, "directory", directory);
}
if let Some(workspace) = &self.scope.workspace {
push_query(url, sep, "workspace", workspace);
}
}
pub fn join(&self, path: &str) -> String {
format!("{}/{}", self.base_url, path.trim_start_matches('/'))
}
pub fn session_create_url(&self) -> String {
let mut url = format!("{}/session", self.base_url);
let mut sep = '?';
self.append_scope(&mut url, &mut sep);
url
}
pub fn prompt_async_url(&self, session_id: &str) -> String {
let mut url = format!(
"{}/session/{}/prompt_async",
self.base_url,
encode_segment(session_id)
);
let mut sep = '?';
self.append_scope(&mut url, &mut sep);
url
}
pub fn messages_url(
&self,
session_id: &str,
limit: Option<u64>,
before: Option<&str>,
) -> String {
let mut url = format!(
"{}/session/{}/message",
self.base_url,
encode_segment(session_id)
);
let mut sep = '?';
if let Some(limit) = limit {
push_query(&mut url, &mut sep, "limit", &limit.to_string());
}
if let Some(before) = before {
push_query(&mut url, &mut sep, "before", before);
}
self.append_scope(&mut url, &mut sep);
url
}
pub fn abort_url(&self, session_id: &str) -> String {
let mut url = format!(
"{}/session/{}/abort",
self.base_url,
encode_segment(session_id)
);
let mut sep = '?';
self.append_scope(&mut url, &mut sep);
url
}
pub fn permission_url(&self, session_id: &str, permission_id: &str) -> String {
let mut url = format!(
"{}/session/{}/permissions/{}",
self.base_url,
encode_segment(session_id),
encode_segment(permission_id)
);
let mut sep = '?';
self.append_scope(&mut url, &mut sep);
url
}
pub fn event_url(&self) -> String {
let mut url = format!("{}/event", self.base_url);
let mut sep = '?';
self.append_scope(&mut url, &mut sep);
url
}
pub fn event_request(&self) -> RequestBuilder {
let mut builder = self.client.get(self.event_url());
if let Some(auth) = &self.auth {
builder = builder.basic_auth(&auth.username, Some(&auth.password));
}
builder
}
async fn send(&self, method: Method, url: &str, body: Option<Value>) -> Result<String> {
let mut builder = self.client.request(method, url);
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
if let Some(auth) = &self.auth {
builder = builder.basic_auth(&auth.username, Some(&auth.password));
}
if let Some(body) = body {
builder = builder.json(&body);
}
let response = builder.send().await?;
let status = response.status();
let text = response.text().await?;
if status.is_success() {
Ok(text)
} else {
Err(Error::Http {
status: status.as_u16(),
body: text,
})
}
}
pub async fn request_json<R: DeserializeOwned>(
&self,
method: Method,
url: &str,
body: Option<Value>,
) -> Result<R> {
let text = self.send(method, url, body).await?;
Ok(serde_json::from_str(&text)?)
}
pub async fn request_unit(&self, method: Method, url: &str, body: Option<Value>) -> Result<()> {
self.send(method, url, body).await?;
Ok(())
}
}