use std::fmt;
use serde_json::Value;
use crate::error::Result;
#[derive(Debug, Default, Clone)]
pub struct WireRequest<'a> {
pub method: &'a str,
pub path: &'a str,
pub path_params: Vec<(String, String)>,
pub query: Vec<(String, String)>,
pub headers: Vec<(String, String)>,
pub body: Option<String>,
}
pub type Call<'a> = WireRequest<'a>;
#[derive(Debug, Clone)]
pub struct WireResponse {
pub status: u16,
pub endpoint: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl WireResponse {
#[must_use]
pub fn new(
status: u16,
endpoint: String,
headers: Vec<(String, String)>,
body: Vec<u8>,
) -> Self {
Self {
status,
endpoint,
headers,
body,
}
}
#[must_use]
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
#[must_use]
pub fn is_redirection(&self) -> bool {
(300..400).contains(&self.status)
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
}
#[derive(Debug)]
pub struct TransportError {
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
impl TransportError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
#[must_use]
pub fn with_source(
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self {
message: message.into(),
source: Some(Box::new(source)),
}
}
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for TransportError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|boxed| boxed.as_ref() as &(dyn std::error::Error + 'static))
}
}
pub trait TapesTransport {
fn send(
&self,
request: &WireRequest<'_>,
) -> impl Future<Output = std::result::Result<WireResponse, TransportError>>;
}
pub trait StreamingTransport: TapesTransport {
type Body;
fn send_stream(&self, request: &WireRequest<'_>) -> impl Future<Output = Result<Self::Body>>;
}
#[derive(Debug, Clone)]
pub enum SpecFetch {
Unchanged,
Fetched {
document: Value,
etag: Option<String>,
},
}
pub trait SpecTransport {
type Error: fmt::Display;
fn fetch_discovery(&self) -> impl Future<Output = std::result::Result<Value, Self::Error>>;
fn fetch_spec(
&self,
path: &str,
etag: Option<&str>,
) -> impl Future<Output = std::result::Result<SpecFetch, Self::Error>>;
fn execute(
&self,
call: &Call<'_>,
) -> impl Future<Output = std::result::Result<Value, Self::Error>>;
}
#[derive(Debug, Clone, Copy)]
pub struct Wire<T>(pub T);
impl<T> Wire<T> {
#[must_use]
pub fn new(transport: T) -> Self {
Self(transport)
}
}
impl<T: TapesTransport> TapesTransport for Wire<T> {
fn send(
&self,
request: &WireRequest<'_>,
) -> impl Future<Output = std::result::Result<WireResponse, TransportError>> {
self.0.send(request)
}
}
impl<T: TapesTransport> SpecTransport for Wire<T> {
type Error = crate::error::Error;
async fn fetch_discovery(&self) -> Result<Value> {
crate::cassettes::fetch_discovery(&self.0).await
}
async fn fetch_spec(&self, path: &str, etag: Option<&str>) -> Result<SpecFetch> {
crate::cassettes::fetch_spec(&self.0, path, etag).await
}
async fn execute(&self, call: &Call<'_>) -> Result<Value> {
crate::cassettes::invoke(&self.0, call).await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
struct Canned;
impl TapesTransport for Canned {
async fn send(
&self,
request: &WireRequest<'_>,
) -> std::result::Result<WireResponse, TransportError> {
Ok(WireResponse::new(
200,
format!("http://tapes.test{}", request.path),
vec![("etag".to_owned(), "\"sha256:abc\"".to_owned())],
br#"{"cassettes":[]}"#.to_vec(),
))
}
}
#[tokio::test]
async fn a_transport_drives_the_cassette_seam_through_the_bridge() {
let bridged = Wire::new(Canned);
let discovery = SpecTransport::fetch_discovery(&bridged).await.unwrap();
assert_eq!(discovery["cassettes"], serde_json::json!([]));
let fetched = SpecTransport::fetch_spec(&bridged, "/v1/cassettes/x/openapi.json", None)
.await
.unwrap();
match fetched {
SpecFetch::Fetched { etag, .. } => {
assert_eq!(etag.as_deref(), Some("\"sha256:abc\""));
}
SpecFetch::Unchanged => panic!("expected a document"),
}
}
#[test]
fn a_header_is_found_however_the_transport_spelled_it() {
let response = WireResponse::new(
200,
"http://tapes.test/v1/cassettes".to_owned(),
vec![("ETag".to_owned(), "\"x\"".to_owned())],
Vec::new(),
);
assert_eq!(response.header("etag"), Some("\"x\""));
assert_eq!(response.header("ETAG"), Some("\"x\""));
assert_eq!(response.header("if-none-match"), None);
}
}