1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
use crate::{
client::{Client, Request, Response},
enums::{RequestMethod, RequestType, ResponseType},
errors::ClientError,
};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use url::Url;
/// Represents a remote HTTP endpoint which can be executed using a
/// [crate::client::Client].
///
/// This trait can be implemented directly, however, users should prefer using
/// the provided `rustify_derive` macro for generating implementations. An
/// Endpoint consists of:
/// * An `action` which is combined with the base URL of a Client to form a
/// fully qualified URL.
/// * A `method` of type [RequestType] which determines the HTTP method used
/// when a Client executes this endpoint.
/// * A `ResponseType` type which determines the type of response this
/// Endpoint will return when executed.
///
/// Presently, this trait only supports sending and receiving data using JSON.
/// The struct implementing this trait must also implement [serde::Serialize].
/// The fields of the struct act as a representation of data that will be
/// serialized and sent to the remote server. Fields that should be excluded
/// from this behavior can be tagged with the `#[serde(skip)]` attribute. The
/// Endpoint will take the raw response body from the remote server and attempt
/// to deserialize it into the given `ResponseType` which must implement
/// [serde::Deserialize]. This deserialized value is then returned after
/// execution completes.
///
/// Implementations can override the default [transform][Endpoint::transform] in
/// order to modify the raw response content from the remote server before
/// returning it. This is often useful when the remote API wraps all responses
/// in a common format and the desire is to remove the wrapper before returning
/// the deserialized response. It can also be used to check for any errors
/// generated by the API and escalate them accordingly.
///
/// # Example
/// ```
/// use rustify::clients::reqwest::ReqwestClient;
/// use rustify::endpoint::Endpoint;
/// use rustify_derive::Endpoint;
/// use serde::Serialize;
///
/// #[derive(Debug, Endpoint, Serialize)]
/// #[endpoint(path = "my/endpoint")]
/// struct MyEndpoint {}
///
/// // Configure a client with a base URL of http://myapi.com
/// let client = ReqwestClient::default("http://myapi.com");
///
/// // Construct a new instance of our Endpoint
/// let endpoint = MyEndpoint {};
///
/// // Execute our Endpoint using the client
/// // This sends a GET request to http://myapi.com/my/endpoint
/// // It assumes an empty response
/// let result = endpoint.exec(&client);
/// ```
pub trait Endpoint: Debug + Serialize + Sized {
/// The type that the raw response from executing this endpoint will
/// automatically be deserialized to. This type must implement
/// [serde::Deserialize].
type Result: DeserializeOwned;
/// The content type of the request body
const REQUEST_BODY_TYPE: RequestType;
/// The content type of the response body
const RESPONSE_BODY_TYPE: ResponseType;
/// The relative URL path that represents the location of this Endpoint.
/// This is combined with the base URL from a
/// [Client][crate::client::Client] instance to create the fully qualified
/// URL.
fn path(&self) -> String;
/// The HTTP method to be used when executing this Endpoint.
fn method(&self) -> RequestMethod;
/// Optional query parameters to add to the request
fn query(&self) -> Vec<(String, Value)> {
Vec::new()
}
/// Optional raw request data that will be sent instead of serializing the
/// struct.
fn data(&self) -> Option<&[u8]> {
None
}
/// Executes the Endpoint using the given [Client] and returns the
/// deserialized response as defined by [Endpoint::Result].
fn exec<C: Client>(&self, client: &C) -> Result<Option<Self::Result>, ClientError> {
log::info!("Executing endpoint");
log::debug! {"Endpoint: {:#?}", self};
let req = build_request(self, client.base(), self.data())?;
let resp = client.execute(req)?;
parse(self, &resp.body)
}
/// Executes the Endpoint using the given [Client] and [MiddleWare],
/// returning the deserialized response as defined by [Endpoint::Result].
fn exec_mut<C: Client, M: MiddleWare>(
&self,
client: &C,
middle: &M,
) -> Result<Option<Self::Result>, ClientError> {
log::info!("Executing endpoint");
log::debug! {"Endpoint: {:#?}", self};
let mut req = build_request(self, client.base(), self.data())?;
middle.request(self, &mut req)?;
let mut resp = client.execute(req)?;
middle.response(self, &mut resp)?;
parse(self, &resp.body)
}
/// Executes the Endpoint using the given [Client], returning the raw
/// response as a byte array.
fn exec_raw<C: Client>(&self, client: &C) -> Result<Vec<u8>, ClientError> {
log::info!("Executing endpoint");
log::debug! {"Endpoint: {:#?}", self};
let req = build_request(self, client.base(), self.data())?;
let resp = client.execute(req)?;
Ok(resp.body)
}
/// Executes the Endpoint using the given [Client] and [MiddleWare],
/// returning the raw response as a byte array.
fn exec_raw_mut<C: Client, M: MiddleWare>(
&self,
client: &C,
middle: &M,
) -> Result<Vec<u8>, ClientError> {
log::info!("Executing endpoint");
log::debug! {"Endpoint: {:#?}", self};
let mut req = build_request(self, client.base(), self.data())?;
middle.request(self, &mut req)?;
let mut resp = client.execute(req)?;
middle.response(self, &mut resp)?;
Ok(resp.body)
}
}
pub trait MiddleWare {
fn request<E: Endpoint>(&self, endpoint: &E, req: &mut Request) -> Result<(), ClientError>;
fn response<E: Endpoint>(&self, endpoint: &E, resp: &mut Response) -> Result<(), ClientError>;
}
/// Builds a [Request] using the given [Endpoint] and base URL
fn build_request<E: Endpoint>(
endpoint: &E,
base: &str,
data: Option<&[u8]>,
) -> Result<Request, ClientError> {
let url = build_url(endpoint, base)?;
let method = endpoint.method();
let query = endpoint.query();
let headers = Vec::new();
let body = match data {
Some(d) => d.to_vec(),
None => match E::REQUEST_BODY_TYPE {
RequestType::JSON => {
let parse_data =
serde_json::to_string(endpoint).map_err(|e| ClientError::DataParseError {
source: Box::new(e),
})?;
match parse_data.as_str() {
"null" => "".to_string(),
"{}" => "".to_string(),
_ => parse_data,
}
.into_bytes()
}
},
};
Ok(Request {
url,
method,
query,
headers,
body,
})
}
/// Combines the given base URL with the relative URL path from this
/// Endpoint to create a fully qualified URL.
fn build_url<E: Endpoint>(endpoint: &E, base: &str) -> Result<url::Url, ClientError> {
log::info!(
"Building endpoint url from {} base URL and {} action",
base,
endpoint.path()
);
let mut url = Url::parse(base).map_err(|e| ClientError::UrlParseError {
url: base.to_string(),
source: e,
})?;
url.path_segments_mut()
.unwrap()
.extend(endpoint.path().split('/'));
Ok(url)
}
/// Parses a response body into the [Endpoint::Result], choosing a deserializer
/// based on [Endpoint::RESPONSE_BODY_TYPE].
fn parse<E: Endpoint>(_: &E, body: &[u8]) -> Result<Option<E::Result>, ClientError> {
if body.is_empty() {
return Ok(None);
}
match E::RESPONSE_BODY_TYPE {
ResponseType::JSON => {
serde_json::from_slice(body).map_err(|e| ClientError::ResponseParseError {
source: Box::new(e),
content: String::from_utf8(body.to_vec()).ok(),
})
}
}
}