use reqwest::{Client, Method, RequestBuilder};
use serde_json::Value;
#[derive(Clone)]
pub struct Endpoint {
pub path: String,
pub method: Method,
pub json_body: Option<Value>,
}
pub struct EndpointBuilder {
pub path_template: String,
pub method: Method,
pub params: Vec<(String, String)>,
pub queries: Vec<(String, String)>,
pub json_body: Option<Value>,
}
impl EndpointBuilder {
pub(crate) fn new(path_template: &str, method: Method) -> Self {
Self {
path_template: path_template.to_string(),
method,
params: vec![],
queries: vec![],
json_body: None,
}
}
pub(crate) fn build(self) -> Endpoint {
let mut path = self.path_template.clone();
for (k, v) in &self.params {
path = path.replace(&format!("{{{}}}", k), v);
}
if !self.queries.is_empty() {
path.push('?');
let qs = self
.queries
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
path.push_str(&qs);
}
Endpoint {
method: self.method,
json_body: self.json_body,
path,
}
}
pub(crate) fn json(mut self, body: Value) -> Self {
self.json_body = Some(body);
self
}
pub(crate) fn query(mut self, name: &str, value: &str) -> Self {
self.queries.push((name.to_string(), value.to_string()));
self
}
pub(crate) fn param(mut self, name: &str, value: &str) -> Self {
self.params.push((name.to_string(), value.to_string()));
self
}
}
#[cfg(feature = "test-utils")]
pub struct EndpointSpec {
pub method: &'static str,
pub path: &'static str,
}
#[cfg(feature = "test-utils")]
inventory::collect!(EndpointSpec);
#[cfg(test)]
mod tests {
use reqwest::Method;
use super::Endpoint;
#[test]
fn param_substitutes_placeholder() {
let ep = Endpoint::builder("/apps/{app_id}/logs", Method::GET)
.param("app_id", "abc123")
.build();
assert_eq!(ep.path, "/apps/abc123/logs");
}
#[test]
fn multiple_params_all_substituted() {
let ep =
Endpoint::builder("/apps/{app_id}/files/{file_id}", Method::GET)
.param("app_id", "app-1")
.param("file_id", "index.js")
.build();
assert_eq!(ep.path, "/apps/app-1/files/index.js");
}
#[test]
fn query_appended_after_path() {
let ep = Endpoint::builder("/apps/{app_id}/errors", Method::GET)
.param("app_id", "app-1")
.query("include_4xx", "true")
.build();
assert_eq!(ep.path, "/apps/app-1/errors?include_4xx=true");
}
#[test]
fn multiple_queries_joined_with_ampersand() {
let ep = Endpoint::builder("/resource", Method::GET)
.query("page", "1")
.query("limit", "20")
.build();
assert_eq!(ep.path, "/resource?page=1&limit=20");
}
#[test]
fn no_params_no_queries_path_unchanged() {
let ep = Endpoint::builder("/apps/status", Method::GET).build();
assert_eq!(ep.path, "/apps/status");
}
}
impl Endpoint {
pub fn builder(path_template: &str, method: Method) -> EndpointBuilder {
EndpointBuilder::new(path_template, method)
}
pub fn request_builder(
&self,
http_client: &Client,
base_url: &str,
) -> RequestBuilder {
let url = format!(
"{}/{}",
base_url.trim_end_matches('/'),
self.path.trim_start_matches('/')
);
let mut request = http_client.request(self.method.clone(), url);
if let Some(body) = &self.json_body {
request = request.json(&body);
}
request
}
}