feat: (broken) Add filesystem and network
Some checks failed
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/push/todo-check Pipeline was successful

There are still some broken links around the reqwest types
This commit is contained in:
Paul Campbell 2024-04-08 15:19:06 +01:00
parent a8a861355c
commit 23f51be16b
16 changed files with 2759 additions and 3 deletions

View file

@ -9,9 +9,26 @@ edition = "2021"
# logging # logging
tracing = "0.1" tracing = "0.1"
# network
async-trait = "0.1"
http = "0.2"
reqwest = "0.11"
secrecy = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-xml-rs = "0.6"
thiserror = "1.0"
# fs # fs
tempfile = "3.10" tempfile = "3.10"
[dev-dependencies]
# testing
assert2 = "0.3"
pretty_assertions = "1.4"
test-log = "0.2"
tokio-test = "0.4"
[package.metadata.bin] [package.metadata.bin]
# Conventional commits githook # Conventional commits githook
cc-cli = { version = "0.1" } cc-cli = { version = "0.1" }

151
src/filesystem.rs Normal file
View file

@ -0,0 +1,151 @@
#![allow(unused)]
use std::{
ops::Deref,
path::PathBuf,
sync::{Arc, Mutex},
};
use tempfile::{tempdir, TempDir};
use tracing::info;
#[derive(Clone, Debug)]
pub enum FileSystem {
Real(RealFileSystemEnv),
Temp(TempFileSystemEnv),
}
impl FileSystem {
pub fn new_real(cwd: Option<PathBuf>) -> Self {
let cwd = cwd.unwrap_or_default();
Self::Real(RealFileSystemEnv::new(cwd))
}
pub fn new_temp() -> std::io::Result<Self> {
TempFileSystemEnv::new().map(Self::Temp)
}
}
impl Deref for FileSystem {
type Target = dyn FileSystemEnv;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(env) => env,
Self::Temp(env) => env,
}
}
}
pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
fn cwd(&self) -> &PathBuf;
fn in_cwd(&self, name: &str) -> PathBuf {
self.cwd().join(name)
}
fn write_file(&self, file_name: &str, content: &str) -> std::io::Result<PathBuf> {
use std::fs::File;
use std::io::{LineWriter, Write};
let path = self.in_cwd(file_name);
info!("writing to {:?}", path);
let file = File::create(path.clone())?;
let mut file = LineWriter::new(file);
file.write_all(content.as_bytes())?;
Ok(path)
}
fn file_exists(&self, name: &PathBuf) -> bool {
use std::fs::File;
File::open(name).is_ok()
}
fn read_file(&self, file_name: &str) -> std::io::Result<String> {
use std::fs::File;
use std::io::Read;
let path = self.in_cwd(file_name);
info!("reading from {:?}", path);
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
}
#[derive(Clone, Debug, Default)]
pub struct RealFileSystemEnv {
cwd: PathBuf,
}
#[derive(Clone, Debug)]
pub struct TempFileSystemEnv {
cwd: PathBuf,
temp_dir: Arc<Mutex<TempDir>>,
}
impl FileSystemEnv for TempFileSystemEnv {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl FileSystemEnv for RealFileSystemEnv {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl RealFileSystemEnv {
const fn new(cwd: PathBuf) -> Self {
Self { cwd }
}
}
impl TempFileSystemEnv {
fn new() -> std::io::Result<Self> {
let temp_dir = tempdir()?;
info!("temp dir: {:?}", temp_dir.path());
let cwd = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir));
Ok(Self { cwd, temp_dir })
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test_log::test]
fn test_cwd() {
let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_create_on_temp_fs() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?;
assert!(env.cwd().exists());
Ok(())
}
#[test_log::test]
fn test_create_on_real_fs() {
let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_write_and_read_file() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?;
let file_name = "test.txt";
let content = "Hello, World!";
let path = env.write_file(file_name, content)?;
assert_eq!(env.read_file(file_name)?, content);
assert!(path.exists());
Ok(())
}
}

View file

@ -1,3 +1,2 @@
fn main() { pub mod filesystem;
println!("Hello, world!"); pub mod network;
}

796
src/network/mock.rs Normal file
View file

@ -0,0 +1,796 @@
#![cfg(not(tarpaulin_include))]
use serde::de::DeserializeOwned;
use tracing::{event, Level};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::network::StatusCode;
use super::network_env::NetworkTrait;
use super::{
NetRequest, NetResponse, NetUrl, Network, NetworkError, RequestBody, RequestMethod,
ResponseType, SavedRequest,
};
#[derive(Debug, Clone)]
pub struct MockNetwork {
requests: Arc<Mutex<Vec<SavedRequest>>>,
get_responses: HashMap<NetUrl, (StatusCode, String)>,
get_errors: HashMap<NetUrl, String>,
post_responses: HashMap<NetUrl, (StatusCode, String)>,
post_errors: HashMap<NetUrl, String>,
put_responses: HashMap<NetUrl, (StatusCode, String)>,
put_errors: HashMap<NetUrl, String>,
patch_responses: HashMap<NetUrl, (StatusCode, String)>,
patch_errors: HashMap<NetUrl, String>,
delete_responses: HashMap<NetUrl, (StatusCode, String)>,
delete_errors: HashMap<NetUrl, String>,
propfind_responses: HashMap<NetUrl, (StatusCode, String)>,
propfind_errors: HashMap<NetUrl, String>,
}
impl MockNetwork {
pub fn new() -> Self {
Self {
requests: Arc::new(Mutex::new(Vec::new())),
get_responses: HashMap::new(),
get_errors: HashMap::new(),
post_responses: HashMap::new(),
post_errors: HashMap::new(),
put_responses: HashMap::new(),
put_errors: HashMap::new(),
patch_responses: HashMap::new(),
patch_errors: HashMap::new(),
delete_responses: HashMap::new(),
delete_errors: HashMap::new(),
propfind_responses: HashMap::new(),
propfind_errors: HashMap::new(),
}
}
pub fn requests(&self) -> Vec<SavedRequest> {
unsafe { self.requests.lock().unwrap_unchecked().clone() }
}
pub fn add_get_response(&mut self, url: &str, status: StatusCode, body: &str) {
let insert = self
.get_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_get_error(&mut self, url: &str, error: &str) {
self.get_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
pub fn add_post_response(&mut self, url: &str, status: StatusCode, body: &str) {
self.post_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_post_error(&mut self, url: &str, error: &str) {
self.post_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
pub fn add_put_response(&mut self, url: &str, status: StatusCode, body: &str) {
self.put_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_put_error(&mut self, url: &str, error: &str) {
self.put_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
pub fn add_patch_response(&mut self, url: &str, status: StatusCode, body: &str) {
self.patch_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_patch_error(&mut self, url: &str, error: &str) {
self.patch_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
pub fn add_delete_response(&mut self, url: &str, status: StatusCode, body: &str) {
self.delete_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_delete_error(&mut self, url: &str, error: &str) {
self.delete_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
pub fn add_propfind_response(&mut self, url: &str, status: StatusCode, body: &str) {
self.propfind_responses
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
}
pub fn add_propfind_error(&mut self, url: &str, error: &str) {
self.propfind_errors
.insert(NetUrl::new(url.to_string()), error.to_string());
}
fn save_request(&self, method: RequestMethod, url: &str, body: RequestBody) {
unsafe {
self.requests
.lock()
.unwrap_unchecked()
.push(SavedRequest::new(method, url, body));
}
}
#[tracing::instrument(skip_all)]
fn call<Reply: DeserializeOwned>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
tracing::info!("MockNetworkEnv::call({:?})", net_request);
let method = net_request.method();
let url = net_request.url();
let body = net_request.body();
let response_type = net_request.response_type();
self.save_request(method, url, body.clone());
let errors = match method {
RequestMethod::Get => &self.get_errors,
RequestMethod::Post => &self.post_errors,
RequestMethod::Put => &self.put_errors,
RequestMethod::Patch => &self.patch_errors,
RequestMethod::Propfind => &self.propfind_errors,
RequestMethod::Delete => &self.delete_errors,
};
if let Some(error) = errors.get(url) {
event!(
Level::INFO,
"MockNetworkEnv::{}({}) -> error: {}",
method,
**url,
error
);
Err(NetworkError::RequestError(
method,
StatusCode::INTERNAL_SERVER_ERROR,
url.clone(),
))
} else {
let responses = match method {
RequestMethod::Get => &self.get_responses,
RequestMethod::Post => &self.post_responses,
RequestMethod::Put => &self.put_responses,
RequestMethod::Patch => &self.patch_responses,
RequestMethod::Propfind => &self.propfind_responses,
RequestMethod::Delete => &self.delete_responses,
};
let (status, response) = responses.get(url).ok_or_else(|| {
tracing::error!(?method, ?url, "unexpected request");
NetworkError::MockError(method, StatusCode::NOT_IMPLEMENTED, url.clone())
})?;
if status.is_client_error() || status.is_server_error() {
event!(
Level::INFO,
"MockNetworkEnv::{}({}) -> error: {}",
method,
url,
response
);
Err(NetworkError::RequestError(method, *status, url.clone()))
} else {
event!(
Level::INFO,
"MockNetworkEnv::{}({}) -> response: {}",
method,
url,
response
);
let response_body: Option<Reply> = match response_type {
ResponseType::Json => Some(serde_json::from_str(response)?),
ResponseType::Xml => Some(serde_xml_rs::from_str(response)?),
ResponseType::None => None,
ResponseType::Text => {
// use call_string() instead
Err(NetworkError::UnexpectedMockRequest {
method,
request_url: url.clone(),
})?
}
};
Ok(NetResponse::new(method, url, *status, response_body))
}
}
}
fn call_string(&self, net_request: NetRequest) -> Result<NetResponse<String>, NetworkError> {
let method = net_request.method();
let url = net_request.url();
let body = net_request.body();
match body {
RequestBody::String(_) => Ok(()),
RequestBody::None => Ok(()),
_ => Err(NetworkError::InvalidRequestBody),
}?;
self.save_request(method, url, body.clone());
event!(Level::INFO, "MockNetworkEnv::{}({})", method, url);
self.check_for_error(method, url).map_or_else(
|| self.as_response(method, url, &net_request),
|error| as_server_error(method, url, error),
)
}
fn as_response(
&self,
method: RequestMethod,
url: &NetUrl,
net_request: &NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
event!(Level::INFO, "url: {}", url);
let (status, response) = self.check_for_response(method, url).ok_or_else(|| {
NetworkError::MockError(method, StatusCode::NOT_IMPLEMENTED, url.clone())
})?;
event!(Level::INFO, "status: {:?}", status);
let response_body = match net_request.response_type() {
ResponseType::None => None,
_ => {
let response_body: String = response.to_string();
event!(Level::INFO, "response_body: {:?}", response_body);
Some(response_body)
}
};
Ok(NetResponse::new(method, url, *status, response_body))
}
fn check_for_response(
&self,
method: RequestMethod,
url: &NetUrl,
) -> Option<&(StatusCode, String)> {
(match method {
RequestMethod::Get => &self.get_responses,
RequestMethod::Post => &self.post_responses,
RequestMethod::Put => &self.put_responses,
RequestMethod::Patch => &self.patch_responses,
RequestMethod::Propfind => &self.propfind_responses,
RequestMethod::Delete => &self.delete_responses,
})
.get(url)
}
fn check_for_error(&self, method: RequestMethod, url: &NetUrl) -> Option<&String> {
(match method {
RequestMethod::Get => &self.get_errors,
RequestMethod::Post => &self.post_errors,
RequestMethod::Put => &self.put_errors,
RequestMethod::Patch => &self.patch_errors,
RequestMethod::Propfind => &self.propfind_errors,
RequestMethod::Delete => &self.delete_errors,
})
.get(url)
}
}
fn as_server_error(
method: RequestMethod,
url: &NetUrl,
error: &String,
) -> Result<NetResponse<String>, NetworkError> {
event!(
Level::INFO,
"MockNetworkEnv::{}({}) -> error: {}",
method,
url,
error
);
Err(NetworkError::RequestError(
method,
StatusCode::INTERNAL_SERVER_ERROR,
url.clone(),
))
}
impl Default for MockNetwork {
fn default() -> Self {
Self::new()
}
}
impl From<MockNetwork> for Network {
fn from(mock: MockNetwork) -> Self {
Self::Mock(mock)
}
}
#[async_trait::async_trait]
impl NetworkTrait for MockNetwork {
async fn get<Reply: DeserializeOwned>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Get,
"get method must be RequestMethod::Get"
);
self.call(net_request)
}
async fn get_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Get,
"get_string method must be RequestMethod::Get"
);
assert_eq!(
net_request.response_type(),
ResponseType::Text,
"get_string response_type must be ResponseType::Text"
);
self.call_string(net_request)
}
async fn post_json<Reply: DeserializeOwned>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Post,
"post method must be RequestMethod::Post"
);
assert!(
matches!(net_request.body(), RequestBody::Json(_)),
"request body must be RequestBody::Json"
);
self.call(net_request)
}
async fn post_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Post,
"post_string method must be RequestMethod::Post"
);
assert_eq!(
net_request.response_type(),
ResponseType::Text,
"post_string response_type must be ResponseType::Text"
);
self.call_string(net_request)
}
async fn put_json<Reply: DeserializeOwned>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Put,
"put method must be RequestMethod::Put"
);
assert!(
matches!(net_request.body(), RequestBody::Json(_)),
"request body must be RequestBody::Json"
);
self.call(net_request)
}
async fn put_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Put,
"put_string method must be RequestMethod::Put"
);
assert_eq!(
net_request.response_type(),
ResponseType::Text,
"put_string response_type must be ResponseType::Text"
);
self.call_string(net_request)
}
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Patch,
"patch method must be RequestMethod::Patch"
);
assert!(
matches!(net_request.body(), RequestBody::Json(_)),
"request body must be RequestBody::Json"
);
self.call(net_request)
}
#[tracing::instrument(skip(self))]
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Delete,
"delete method must be RequestMethod::Delete"
);
assert_eq!(
net_request.response_type(),
ResponseType::None,
"delete response_type must be ResponseType::None"
);
self.call(net_request)
}
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Propfind,
"propfind method must be RequestMethod::Propfind"
);
assert_eq!(
net_request.response_type(),
ResponseType::Xml,
"propfind response_type must be ResponseType::Xml"
);
assert_eq!(
net_request.body(),
&RequestBody::None,
"delete body must be RequestBody::None"
);
self.call(net_request)
}
async fn propfind_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
assert_eq!(
net_request.method(),
RequestMethod::Propfind,
"propfind_string method must be RequestMethod::Propfind"
);
assert_eq!(
net_request.response_type(),
ResponseType::Text,
"propfind_string response_type must be ResponseType::Text"
);
assert_eq!(
net_request.body(),
&RequestBody::None,
"delete body must be RequestBody::None"
);
self.call_string(net_request)
}
}
#[cfg(test)]
mod tests {
use crate::network::{NetResponse, NetworkError, RequestMethod};
use super::*;
use reqwest::StatusCode;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio_test::block_on;
#[test_log::test]
fn test_mock_network_env_get() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_get_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into())).build();
let response: NetResponse<HashMap<String, String>> = block_on(net.get(net_request))?;
assert_eq!(
response
.response_body()
.and_then(|body| body.get("foo").cloned()),
Some("bar".to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_get_error() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_get_error("https://httpbin.org", "error");
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into())).build();
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
block_on(net.get(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_get_string() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_get_response("https://httpbin.org", StatusCode::OK, r#"{"foo":"bar"}"#);
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into()))
.response_type(ResponseType::Text)
.build();
let response: NetResponse<String> = block_on(net.get_string(net_request))?;
assert_eq!(
response.response_body(),
Some(json!({"foo":"bar"}).to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_get_string_error() {
let mut net = MockNetwork::new();
net.add_get_error("https://httpbin.org", "error");
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into()))
.response_type(ResponseType::Text)
.build();
let result: Result<NetResponse<String>, NetworkError> =
block_on(net.get_string(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::None
)]
);
}
#[test_log::test]
fn test_mock_network_env_post() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_post_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org".into()))
.json_body(json!({}))?
.build();
let response: NetResponse<HashMap<String, String>> = block_on(net.post_json(net_request))?;
assert_eq!(
response
.response_body()
.and_then(|body| body.get("foo").cloned()),
Some("bar".to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Post,
"https://httpbin.org",
RequestBody::Json(json!({}))
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_post_error() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_post_error("https://httpbin.org", "error");
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org".into()))
.json_body(json!({}))?
.build();
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
block_on(net.post_json(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Post,
"https://httpbin.org",
RequestBody::Json(json!({}))
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_put_json() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_put_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
.json_body(json!({}))?
.build();
let response: NetResponse<HashMap<String, String>> = block_on(net.put_json(net_request))?;
assert_eq!(
response
.response_body()
.and_then(|body| body.get("foo").cloned()),
Some("bar".to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Put,
"https://httpbin.org",
RequestBody::Json(json!({}))
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_put_json_error() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_put_error("https://httpbin.org", "error");
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
.json_body(json!({}))?
.build();
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
block_on(net.put_json(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Put,
"https://httpbin.org",
RequestBody::Json(json!({}))
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_put_string() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_put_response("https://httpbin.org", StatusCode::OK, r#"{"foo":"bar"}"#);
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
.string_body("PLAIN-TEXT".to_string())
.build();
let response: NetResponse<String> = block_on(net.put_string(net_request))?;
assert_eq!(
response.response_body(),
Some(json!({"foo":"bar"}).to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Put,
"https://httpbin.org",
RequestBody::String("PLAIN-TEXT".to_string()),
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_put_string_error() {
let mut net = MockNetwork::new();
net.add_put_error("https://httpbin.org", "error");
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
.string_body("PLAIN-TEXT".to_string())
.build();
let result: Result<NetResponse<String>, NetworkError> =
block_on(net.put_string(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Put,
"https://httpbin.org",
RequestBody::String("PLAIN-TEXT".to_string())
)]
);
}
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
struct PropfindTestResponse {
pub foo: String,
}
#[test_log::test]
fn test_mock_network_env_propfind() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_propfind_response(
"https://caldav.org",
StatusCode::OK,
r#"<container><foo>bar</foo></container>"#,
);
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
.response_type(ResponseType::Xml)
.build();
let response: NetResponse<PropfindTestResponse> = block_on(net.propfind(net_request))?;
assert_eq!(
response.response_body().map(|body| body.foo),
Some("bar".to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Propfind,
"https://caldav.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_propfind_string() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_propfind_response(
"https://caldav.org",
StatusCode::OK,
r#"<container><foo>bar</foo></container>"#,
);
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
.response_type(ResponseType::Text)
.build();
let response: NetResponse<String> = block_on(net.propfind_string(net_request))?;
assert_eq!(
response.response_body(),
Some("<container><foo>bar</foo></container>".to_string())
);
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Propfind,
"https://caldav.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_propfind_error() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_propfind_error("https://caldav.org", "error");
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
.response_type(ResponseType::Xml)
.build();
let result: Result<NetResponse<PropfindTestResponse>, NetworkError> =
block_on(net.propfind(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Propfind,
"https://caldav.org",
RequestBody::None
)]
);
Ok(())
}
#[test_log::test]
fn test_mock_network_env_propfind_string_error() -> Result<(), NetworkError> {
let mut net = MockNetwork::new();
net.add_propfind_error("https://caldav.org", "error");
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
.response_type(ResponseType::Text)
.build();
let result: Result<NetResponse<String>, NetworkError> =
block_on(net.propfind_string(net_request));
assert!(result.is_err());
assert_eq!(
net.requests(),
vec![SavedRequest::new(
RequestMethod::Propfind,
"https://caldav.org",
RequestBody::None
)]
);
Ok(())
}
}

36
src/network/mod.rs Normal file
View file

@ -0,0 +1,36 @@
mod mock;
mod net_auth;
mod net_request;
mod net_request_headers;
mod net_response;
mod network_env;
mod network_error;
mod real;
mod request_body;
mod request_method;
mod response_type;
mod saved_request;
pub use mock::MockNetwork;
pub use net_auth::NetAuth;
pub use net_auth::NetAuthPassword;
pub use net_auth::NetAuthUsername;
pub use net_request::NetRequest;
pub use net_request::NetRequestLogging;
pub use net_request::NetUrl;
pub use net_request_headers::NetRequestHeaders;
pub use net_response::NetResponse;
pub use network_env::Network;
pub use network_error::NetworkError;
pub use real::RealNetwork;
pub use request_body::RequestBody;
pub use request_method::RequestMethod;
pub use response_type::ResponseType;
pub use saved_request::SavedRequest;
pub use reqwest::header::HeaderMap;
pub use reqwest::Error as ReqwestError;
pub use reqwest::StatusCode;
pub use serde_json::json;
pub type NetworkResult<T> = Result<T, NetworkError>;

66
src/network/net_auth.rs Normal file
View file

@ -0,0 +1,66 @@
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub struct Password(secrecy::SecretString);
impl Password {
pub fn new(password: String) -> Self {
Self(secrecy::SecretString::new(password))
}
pub fn expose_password(&self) -> &str {
secrecy::ExposeSecret::expose_secret(&self.0)
}
}
#[derive(Debug, Clone)]
pub struct NetAuthUsername(String);
impl NetAuthUsername {
pub fn new(username: String) -> Self {
Self(username)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for NetAuthUsername {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// "Password for HTTP authentication");
#[derive(Debug, Clone)]
pub struct NetAuthPassword(Password);
impl NetAuthPassword {
pub fn new(password: String) -> Self {
Self(Password::new(password))
}
pub fn expose_password(&self) -> &str {
self.0.expose_password()
}
// pub const fn as_str(&self) -> &str {
// "********"
// }
}
impl From<Password> for NetAuthPassword {
fn from(password: Password) -> Self {
Self(password)
}
}
// new_type_display!(NetAuthPassword);
#[derive(Debug, Clone)]
pub struct NetAuth {
username: NetAuthUsername,
password: NetAuthPassword,
}
impl NetAuth {
pub const fn new(username: NetAuthUsername, password: NetAuthPassword) -> Self {
Self { username, password }
}
pub const fn username(&self) -> &NetAuthUsername {
&self.username
}
pub const fn password(&self) -> &NetAuthPassword {
&self.password
}
}

462
src/network/net_request.rs Normal file
View file

@ -0,0 +1,462 @@
use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Deref;
use crate::network::HeaderMap;
use crate::network::NetAuth;
use crate::network::RequestBody;
use crate::network::RequestMethod;
use crate::network::ResponseType;
use super::NetRequestHeaders;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct NetUrl(String);
impl NetUrl {
pub const fn new(url: String) -> Self {
Self(url)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for NetUrl {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for NetUrl {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum NetRequestLogging {
None,
Request,
Response,
Both,
}
impl Default for NetRequestLogging {
fn default() -> Self {
Self::None
}
}
#[derive(Debug)]
pub struct NetRequest {
method: RequestMethod,
url: NetUrl,
body: RequestBody,
response_type: ResponseType,
auth: Option<NetAuth>,
headers: NetRequestHeaders,
log: NetRequestLogging,
}
impl NetRequest {
pub fn get(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.url(net_url)
.header("Accept", "application/json")
}
pub fn post(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.method(RequestMethod::Post)
.url(net_url)
}
pub fn put(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.method(RequestMethod::Put)
.url(net_url)
}
pub fn patch(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.method(RequestMethod::Patch)
.url(net_url)
}
pub fn delete(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.method(RequestMethod::Delete)
.response_type(ResponseType::None)
.url(net_url)
}
pub fn propfind(net_url: NetUrl) -> NetRequestBuilder {
NetRequestBuilder::default()
.method(RequestMethod::Propfind)
.response_type(ResponseType::None)
.url(net_url)
}
const fn new(
method: RequestMethod,
url: NetUrl,
headers: NetRequestHeaders,
body: RequestBody,
response_type: ResponseType,
auth: Option<NetAuth>,
log: NetRequestLogging,
) -> Self {
Self {
method,
url,
headers,
body,
response_type,
auth,
log,
}
}
pub fn as_trace(&self) -> String {
format!(
"{} {}",
self.method,
self.url.as_str().chars().take(90).collect::<String>()
)
}
pub const fn method(&self) -> RequestMethod {
self.method
}
pub const fn url(&self) -> &NetUrl {
&self.url
}
pub const fn body(&self) -> &RequestBody {
&self.body
}
pub const fn response_type(&self) -> ResponseType {
self.response_type
}
pub const fn log(&self) -> NetRequestLogging {
self.log
}
pub fn auth(&self) -> Option<NetAuth> {
self.auth.clone()
}
pub fn headers(&self) -> HeaderMap {
self.headers
.clone()
.try_into()
.unwrap_or_else(|_| HeaderMap::new())
}
pub fn as_curl(&self) -> Result<String, serde_json::Error> {
let mut curl = format!("curl -X {} {}", self.method, *self.url);
if let Some(accept) = &self.response_type.accept() {
if self.headers.get("Accept").is_none() {
curl.push_str(&format!(" -H 'Accept: {}'", accept));
}
}
let mut headers = vec![];
for (key, value) in self.headers.iter() {
headers.push(format!(" -H '{}: {}'", key, value));
}
headers.sort();
for header in headers {
curl.push_str(&header);
}
if let Some(auth) = &self.auth() {
curl.push_str(&format!(
" -u {}:{}",
auth.username(),
auth.password().expose_password()
));
}
if self.method == RequestMethod::Post || self.method == RequestMethod::Put {
let body = String::try_from(&self.body)?;
curl.push_str(&format!(" -d '{}'", body));
}
Ok(curl)
}
}
#[derive(Default)]
pub struct NetRequestBuilder {
method: RequestMethod,
url: NetUrl,
headers: NetRequestHeaders,
body: RequestBody,
response_type: ResponseType,
auth: Option<NetAuth>,
log: NetRequestLogging,
}
impl NetRequestBuilder {
pub fn build(self) -> NetRequest {
NetRequest::new(
self.method,
self.url,
self.headers,
self.body,
self.response_type,
self.auth,
self.log,
)
}
pub fn method(mut self, method: RequestMethod) -> Self {
assert_ne!(method, RequestMethod::default());
self.method = method;
if self.method == RequestMethod::Get || self.method == RequestMethod::Delete {
self.body = RequestBody::None;
}
self
}
fn url(mut self, url: NetUrl) -> Self {
self.url = url;
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers = self.headers.with(key, value);
self
}
pub fn headers(mut self, new_headers: NetRequestHeaders) -> Self {
assert_ne!(new_headers, NetRequestHeaders::default());
for (key, value) in new_headers.iter() {
self.headers = self.headers.with(key, value);
}
self
}
pub fn body(mut self, body: RequestBody) -> Self {
match body {
RequestBody::Json(_) => {
self.headers = self.headers.with("Content-Type", "application/json")
}
RequestBody::Xml(_) => {
self.headers = self.headers.with("Content-Type", "application/xml")
}
_ => (),
}
self.body = body;
self
}
pub fn string_body(mut self, body: String) -> Self {
self.body = RequestBody::String(body);
self.header("Content-Type", "text/plain")
.response_type(ResponseType::Text)
}
pub fn json_body<T: serde::Serialize>(mut self, body: T) -> Result<Self, serde_json::Error> {
self.body = RequestBody::json(body)?;
Ok(self.header("Content-Type", "application/json"))
}
pub fn xml_body(mut self, body: &str) -> Self {
self.body = RequestBody::Xml(body.to_string());
self.header("Content-Type", "application/xml")
}
pub fn response_type(mut self, response_type: ResponseType) -> Self {
assert_ne!(response_type, ResponseType::default());
self.response_type = response_type;
match response_type.accept() {
Some(accept) => self.header("Accept", accept.as_str()),
None => {
self.headers.remove("Accept");
self
}
}
}
pub fn auth(mut self, auth: NetAuth) -> Self {
self.auth = Some(auth);
self
}
pub const fn log(mut self, log: NetRequestLogging) -> Self {
self.log = log;
self
}
}
#[cfg(test)]
mod tests {
use assert2::let_assert;
use serde_json::json;
use crate::network::net_auth::{NetAuthPassword, NetAuthUsername};
use super::*;
#[test_log::test]
fn test_as_curl_no_auth() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X GET https://httpbin.org/get -H 'Accept: application/json'".to_string()
);
}
#[test_log::test]
fn test_as_curl_auth() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.auth(NetAuth::new(
NetAuthUsername::new("user".into()),
NetAuthPassword::new("pass".into()),
))
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X GET https://httpbin.org/get -H 'Accept: application/json' -u user:pass"
.to_string()
);
}
#[test_log::test]
fn test_as_curl_no_auth_post_json() -> Result<(), serde_json::Error> {
let request = NetRequest::post(NetUrl("https://httpbin.org/post".to_string()))
.json_body(json!({
"args": {},
"url": "https://httpbin.org/post",
}))?
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X POST https://httpbin.org/post -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{\"args\":{},\"url\":\"https://httpbin.org/post\"}'".to_string()
);
Ok(())
}
#[test_log::test]
fn test_as_curl_no_auth_post_text() {
let request = NetRequest::post(NetUrl("https://httpbin.org/post".to_string()))
.string_body("body".to_string())
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X POST https://httpbin.org/post -H 'Accept: text/plain' -H 'Content-Type: text/plain' -d 'body'".to_string()
);
}
#[test_log::test]
fn test_as_curl_no_auth_put() -> Result<(), serde_json::Error> {
let request = NetRequest::put(NetUrl("https://httpbin.org/put".to_string()))
.json_body(json!({
"args": {},
"url": "https://httpbin.org/put",
}))?
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X PUT https://httpbin.org/put -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{\"args\":{},\"url\":\"https://httpbin.org/put\"}'".to_string()
);
Ok(())
}
#[test_log::test]
fn test_as_curl_no_auth_delete() {
let request = NetRequest::delete(NetUrl("https://httpbin.org/delete".to_string())).build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X DELETE https://httpbin.org/delete".to_string()
);
}
#[test_log::test]
fn test_as_curl_no_auth_put_xml() {
let request = NetRequest::put(NetUrl("https://httpbin.org/put".to_string()))
.xml_body("<xml/>")
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X PUT https://httpbin.org/put -H 'Accept: application/json' -H 'Content-Type: application/xml' -d '<xml/>'"
.to_string()
);
}
#[test_log::test]
fn test_as_curl_accept_json() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X GET https://httpbin.org/get -H 'Accept: application/json'".to_string()
);
}
#[test_log::test]
fn test_as_curl_accept_xml() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.response_type(ResponseType::Xml)
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X GET https://httpbin.org/get -H 'Accept: application/xml'".to_string()
);
}
#[test_log::test]
fn test_as_curl_accept_text() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.response_type(ResponseType::Text)
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X GET https://httpbin.org/get -H 'Accept: text/plain'".to_string()
);
}
#[test_log::test]
fn text_as_curl_accept_none() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.response_type(ResponseType::None)
.build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(curl, "curl -X GET https://httpbin.org/get".to_string());
}
#[test_log::test]
fn test_as_curl_with_headers() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.header("header1", "value1")
.header("header2", "value2")
.build();
let_assert!(Ok(curl) = request.as_curl());
assert!(curl.contains("-H 'header1: value1'"));
assert!(curl.contains("-H 'header2: value2'"));
assert!(curl.starts_with("curl -X GET https://httpbin.org/get"));
}
#[test_log::test]
fn test_net_request_headers() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
.header("header1", "value1")
.header("header2", "value2")
.build();
let headers = request.headers();
assert!(headers.len() >= 2);
let_assert!(Some(value1) = headers.get("header1"));
assert_eq!(value1, "value1");
let_assert!(Some(value2) = headers.get("header2"));
assert_eq!(value2, "value2");
}
#[test_log::test]
fn test_as_curl_propfind() {
let request =
NetRequest::propfind(NetUrl("https://httpbin.org/propfind".to_string())).build();
let_assert!(Ok(curl) = request.as_curl());
assert_eq!(
curl,
"curl -X PROPFIND https://httpbin.org/propfind".to_string()
);
}
#[test_log::test]
fn test_as_trace() {
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
assert_eq!(request.as_trace(), "GET https://httpbin.org/get");
}
}

View file

@ -0,0 +1,102 @@
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
};
use crate::network::HeaderMap;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct NetRequestHeaders(HashMap<String, String>);
impl NetRequestHeaders {
pub fn new() -> Self {
Self::default() //(HashMap::new())
}
pub fn with(mut self, key: &str, value: &str) -> Self {
self.0.insert(key.into(), value.into());
self
}
}
impl<T> From<T> for NetRequestHeaders
where
T: Into<HashMap<String, String>>,
{
fn from(x: T) -> Self {
Self(x.into())
}
}
impl TryFrom<NetRequestHeaders> for HeaderMap {
type Error = http::Error;
fn try_from(x: NetRequestHeaders) -> Result<Self, Self::Error> {
Self::try_from(&x.0)
}
}
impl Deref for NetRequestHeaders {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for NetRequestHeaders {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use assert2::let_assert;
use pretty_assertions::assert_eq;
use super::*;
#[test_log::test]
fn test_net_request_headers_from_hash_map() {
let nrh = NetRequestHeaders::from(
[("a", "b"), ("c", "d")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<_, _>>(),
);
assert_eq!(nrh.len(), 2);
let_assert!(Some(a) = nrh.get("a"));
assert_eq!(a, "b");
let_assert!(Some(c) = nrh.get("c"));
assert_eq!(c, "d");
}
#[test_log::test]
fn test_net_request_headers_with_new_entry() {
let nrh = NetRequestHeaders::from(
[("a", "b"), ("c", "d")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<_, _>>(),
)
.with("e", "f");
assert_eq!(nrh.len(), 3);
let_assert!(Some(a) = nrh.get("a"));
assert_eq!(a, "b");
let_assert!(Some(c) = nrh.get("c"));
assert_eq!(c, "d");
let_assert!(Some(e) = nrh.get("e"));
assert_eq!(e, "f");
}
#[test_log::test]
fn test_net_request_headers_try_into_header_map() {
let result: Result<HeaderMap, ReqwestError> = NetRequestHeaders::from(
[("a", "b"), ("c", "d")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<_, _>>(),
)
.try_into();
let_assert!(Ok(hm) = result);
assert_eq!(hm.len(), 2);
let_assert!(Some(a) = hm.get("a"));
assert_eq!(a, "b");
let_assert!(Some(c) = hm.get("c"));
assert_eq!(c, "d");
}
}

View file

@ -0,0 +1,38 @@
use crate::network::StatusCode;
use super::{net_request::NetUrl, RequestMethod};
#[derive(Debug)]
pub struct NetResponse<T> {
method: RequestMethod,
request_url: NetUrl,
status_code: StatusCode,
response_body: Option<T>,
}
impl<T> NetResponse<T> {
pub fn new(
method: RequestMethod,
request_url: &NetUrl,
status_code: StatusCode,
response_body: Option<T>,
) -> Self {
Self {
method,
request_url: request_url.clone(),
status_code,
response_body,
}
}
pub const fn method(&self) -> &RequestMethod {
&self.method
}
pub const fn request_url(&self) -> &NetUrl {
&self.request_url
}
pub const fn status_code(&self) -> StatusCode {
self.status_code
}
pub fn response_body(self) -> Option<T> {
self.response_body
}
}

174
src/network/network_env.rs Normal file
View file

@ -0,0 +1,174 @@
use async_trait::async_trait;
use serde::de::DeserializeOwned;
use crate::network::{MockNetwork, NetRequest, NetworkError, RealNetwork, SavedRequest};
use super::NetResponse;
#[derive(Debug, Clone)]
pub enum Network {
Mock(MockNetwork),
Real(RealNetwork),
}
impl Network {
pub fn new_mock() -> Self {
Self::Mock(MockNetwork::default())
}
pub fn new_real() -> Self {
Self::Real(RealNetwork::default())
}
pub async fn get<T: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<T>, NetworkError> {
match self {
Self::Mock(mock) => mock.get(net_request).await,
Self::Real(real) => real.get(net_request).await,
}
}
pub async fn get_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
match self {
Self::Mock(mock) => mock.get_string(net_request).await,
Self::Real(real) => real.get_string(net_request).await,
}
}
pub async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
match self {
Self::Mock(mock) => mock.post_json(net_request).await,
Self::Real(real) => real.post_json(net_request).await,
}
}
pub async fn post_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
match self {
Self::Mock(mock) => mock.post_string(net_request).await,
Self::Real(real) => real.post_string(net_request).await,
}
}
pub async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
match self {
Self::Mock(mock) => mock.put_json(net_request).await,
Self::Real(real) => real.put_json(net_request).await,
}
}
pub async fn put_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
match self {
Self::Mock(mock) => mock.put_string(net_request).await,
Self::Real(real) => real.put_string(net_request).await,
}
}
pub async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
match self {
Self::Mock(mock) => mock.patch_json(net_request).await,
Self::Real(real) => real.patch_json(net_request).await,
}
}
pub async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
match self {
Self::Mock(mock) => mock.delete(net_request).await,
Self::Real(real) => real.delete(net_request).await,
}
}
pub async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
match self {
Self::Mock(mock) => mock.propfind(net_request).await,
Self::Real(real) => real.propfind(net_request).await,
}
}
pub async fn propfind_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
match self {
Self::Mock(mock) => mock.propfind_string(net_request).await,
Self::Real(real) => real.propfind_string(net_request).await,
}
}
pub fn mocked_requests(&self) -> Option<Vec<SavedRequest>> {
match self {
Self::Mock(mock) => Some(mock.requests()),
Self::Real(_) => None,
}
}
}
#[async_trait]
pub(super) trait NetworkTrait: Sync + Send + Clone + std::fmt::Debug {
async fn get<T: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<T>, NetworkError>;
async fn get_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError>;
async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError>;
async fn post_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError>;
async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError>;
async fn put_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError>;
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError>;
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError>;
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError>;
async fn propfind_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError>;
}

View file

@ -0,0 +1,56 @@
#[derive(thiserror::Error, Debug)]
pub enum NetworkError {
#[error(transparent)]
Reqwest(#[from] crate::network::ReqwestError),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
SerdeXml(#[from] serde_xml_rs::Error),
#[error(transparent)]
HttpMethod(#[from] http::method::InvalidMethod),
#[error(transparent)]
Http(#[from] http::Error),
#[error("{0} failed: {1} ({2})")]
RequestFailed(super::RequestMethod, super::StatusCode, super::NetUrl),
#[error("{0} failed: {1} ({2})")]
MockError(super::RequestMethod, super::StatusCode, super::NetUrl),
#[error("missing response body")]
MissingResponseBody,
#[error("unexpected mock request: {method:?} {request_url}")]
UnexpectedMockRequest {
method: super::RequestMethod,
request_url: super::NetUrl,
},
#[error("{0} failed: {1} ({2})")]
RequestError(super::RequestMethod, super::StatusCode, super::NetUrl),
#[error("invalid response type")]
InvalidResponseType,
#[error("invalid request body")]
InvalidRequestBody,
#[error("response body is empty")]
EmptyResponseBody,
}
#[cfg(test)]
mod tests {
use super::NetworkError;
#[test]
const fn test_network_error_is_send() {
const fn assert_send<T: Send>() {}
assert_send::<NetworkError>();
assert_send::<Result<(), NetworkError>>();
}
}

612
src/network/real.rs Normal file
View file

@ -0,0 +1,612 @@
use reqwest::RequestBuilder;
use serde::de::DeserializeOwned;
use tracing::{event, Level};
use super::{
network_env::NetworkTrait, NetAuth, NetRequest, NetRequestLogging, NetResponse, Network,
NetworkError, RequestMethod, ResponseType, StatusCode,
};
trait WithAuthentiction {
fn auth(self, auth: Option<NetAuth>) -> reqwest::RequestBuilder;
}
impl WithAuthentiction for reqwest::RequestBuilder {
fn auth(self, auth: Option<NetAuth>) -> reqwest::RequestBuilder {
if let Some(auth) = auth {
self.basic_auth(
auth.username().to_string(),
Some(auth.password().expose_password()),
)
} else {
self
}
}
}
trait WithResponseType {
fn response_type(self, response_type: ResponseType) -> Self;
}
impl WithResponseType for reqwest::RequestBuilder {
fn response_type(self, response_type: ResponseType) -> Self {
match response_type {
ResponseType::Json => self.header(reqwest::header::ACCEPT, "application/json"),
ResponseType::Xml => self.header(reqwest::header::ACCEPT, "application/xml"),
ResponseType::Text => self.header(reqwest::header::ACCEPT, "text/plain"),
ResponseType::None => self,
}
}
}
#[derive(Debug, Clone)]
pub struct RealNetwork {
client: reqwest::Client,
}
impl RealNetwork {
#[cfg(not(tarpaulin_include))]
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
async fn send<T: DeserializeOwned + std::fmt::Debug>(
&self,
request: RequestBuilder,
net_request: NetRequest,
) -> Result<NetResponse<T>, NetworkError> {
let response = request.send().await?;
let status_code = response.status();
let text = response.text().await?;
if status_code.is_success() {
tracing::trace!(status = ?status_code);
} else {
tracing::error!("Request failed");
self.log_request(&net_request);
self.log_response(&net_request, &status_code, text.as_str());
return Err(NetworkError::RequestFailed(
net_request.method(),
status_code,
net_request.url().clone(),
));
}
match net_request.response_type() {
ResponseType::Json => serde_json::from_str(text.as_str())
.map(Some)
.map_err(NetworkError::from),
ResponseType::Xml => serde_xml_rs::from_str(text.as_str())
.map(Some)
.map_err(NetworkError::from),
ResponseType::Text => {
panic!("text response type not implemented - use send_for_text(...) instead")
}
ResponseType::None => Ok(None),
}
.map(|response_body| {
match net_request.log() {
NetRequestLogging::None => (),
NetRequestLogging::Request => self.log_request(&net_request),
NetRequestLogging::Response => {
self.log_response(&net_request, &status_code, text.as_str());
}
NetRequestLogging::Both => {
self.log_request(&net_request);
self.log_response(&net_request, &status_code, text.as_str());
}
};
NetResponse::new(
net_request.method(),
net_request.url(),
status_code,
response_body,
)
})
}
async fn send_for_text(
&self,
request: RequestBuilder,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
let response = request.send().await?;
let status_code = response.status();
if status_code.is_success() {
tracing::trace!(status = ?status_code);
} else {
tracing::error!(status = ?status_code, request = ?net_request);
}
match net_request.response_type() {
ResponseType::Text => {
let text = response.text().await?;
Ok(Some(text))
}
_ => panic!("text response type not implemented - use send(...) instead"),
}
.map(|response_body| {
NetResponse::new(
net_request.method(),
net_request.url(),
status_code,
response_body,
)
})
}
fn log_request(&self, net_request: &NetRequest) {
tracing::info!(?net_request, "RealNetworkEnv::request");
}
fn log_response(
&self,
net_request: &NetRequest,
status_code: &StatusCode,
response_body: &str,
) {
tracing::info!(
?net_request,
status = ?status_code,
?response_body,
"RealNetworkEnv::response"
);
}
}
impl Default for RealNetwork {
fn default() -> Self {
Self::new()
}
}
impl From<RealNetwork> for Network {
fn from(real: RealNetwork) -> Self {
Self::Real(real)
}
}
#[cfg(not(tarpaulin_include))]
#[async_trait::async_trait]
impl NetworkTrait for RealNetwork {
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn get<T: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<T>, NetworkError> {
tracing::info!("RealNetworkEnv::get({:?})", net_request);
let url = net_request.url();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let request = self
.client
.get(url.to_string())
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::post_json({:?})", net_request);
let url = net_request.url();
let body = net_request.body();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let body = String::try_from(body)?;
let request = self
.client
.post(url.to_string())
.body(body)
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn post_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::post_string({:?})", net_request);
let url = net_request.url();
let body = net_request.body();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let body = String::try_from(body)?;
let request = self
.client
.post(url.to_string())
.body(body)
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send_for_text(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::put_json({:?})", net_request);
let url = net_request.url();
let body = net_request.body();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let body = String::try_from(body)?;
let request = self
.client
.put(url.to_string())
.body(body)
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn put_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::put_string({:?})", net_request);
let url = net_request.url();
let body = net_request.body();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let body = String::try_from(body)?;
let request = self
.client
.put(url.to_string())
.body(body)
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send_for_text(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::put_json({:?})", net_request);
let url = net_request.url();
let body = net_request.body();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let body = String::try_from(body)?;
let request = self
.client
.patch(url.to_string())
.body(body)
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::delete({:?})", net_request);
let url = net_request.url();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let request = self
.client
.delete(url.to_string())
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
&self,
net_request: NetRequest,
) -> Result<NetResponse<Reply>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::propfind({:?})", net_request);
let url = net_request.url();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let request = self
.client
.request(reqwest::Method::from_bytes(b"PROPFIND")?, url.to_string())
.auth(auth)
.response_type(response_type)
.headers(headers);
let response = request.send().await?;
let status_code = response.status();
event!(Level::TRACE, status = %status_code);
let body = match response_type {
ResponseType::Xml => serde_xml_rs::from_str(response.text().await?.as_str()),
_ => Err(NetworkError::InvalidResponseType)?,
};
body.map_err(Into::into).map(|response_body| {
NetResponse::new(RequestMethod::Propfind, url, status_code, response_body)
})
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn get_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::get_string({:?})", net_request);
let url = net_request.url();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let request = self
.client
.get(url.to_string())
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send_for_text(request, net_request).await
}
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
async fn propfind_string(
&self,
net_request: NetRequest,
) -> Result<NetResponse<String>, NetworkError> {
// event!(Level::INFO, "RealNetworkEnv::propfind_string({:?})", net_request);
let url = net_request.url();
let auth = net_request.auth();
let response_type = net_request.response_type();
let headers = net_request.headers();
let request = self
.client
.request(reqwest::Method::from_bytes(b"PROPFIND")?, url.to_string())
.auth(auth)
.response_type(response_type)
.headers(headers);
self.send_for_text(request, net_request).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::{
net_auth::{NetAuthPassword, NetAuthUsername},
NetUrl, NetworkError, StatusCode,
};
use assert2::let_assert;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use tokio_test::block_on;
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
pub struct GetResponse {
pub args: HashMap<String, String>,
pub url: String,
}
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
pub struct PostResponse {
pub args: HashMap<String, String>,
pub url: String,
pub data: String,
}
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
pub struct PutResponse {
pub args: HashMap<String, String>,
pub url: String,
pub data: String,
}
#[test_log::test]
fn test_with_authentication_none() {
let client = reqwest::Client::new();
let request = client.get("https://httpbin.org/get").auth(None);
let_assert!(Ok(build) = request.build());
assert!(build.headers().is_empty());
}
#[test_log::test]
fn test_with_authentication_some() {
let client = reqwest::Client::new();
let request = client
.get("https://httpbin.org/get")
.auth(Some(NetAuth::new(
NetAuthUsername::new("user".into()),
NetAuthPassword::new("pass".into()),
)));
let_assert!(Ok(build) = request.build());
let headers = build.headers();
let_assert!(Some(authorization) = headers.get(reqwest::header::AUTHORIZATION));
assert_eq!(authorization, "Basic dXNlcjpwYXNz");
}
#[test_log::test]
fn test_with_response_type_json() {
let client = reqwest::Client::new();
let request = client
.get("https://httpbin.org/get")
.response_type(ResponseType::Json);
let_assert!(Ok(request) = request.build());
let headers = request.headers();
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
assert_eq!(accept, "application/json");
}
#[test_log::test]
fn test_with_response_type_xml() {
let client = reqwest::Client::new();
let request = client
.get("https://httpbin.org/get")
.response_type(ResponseType::Xml);
let_assert!(Ok(request) = request.build());
let headers = request.headers();
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
assert_eq!(accept, "application/xml");
}
#[test_log::test]
fn test_with_response_type_text() {
let client = reqwest::Client::new();
let request = client
.get("https://httpbin.org/get")
.response_type(ResponseType::Text);
let_assert!(Ok(request) = request.build());
let headers = request.headers();
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
assert_eq!(accept, "text/plain");
}
#[test_log::test]
fn test_with_response_type_none() {
let client = reqwest::Client::new();
let request = client
.get("https://httpbin.org/get")
.response_type(ResponseType::None);
let_assert!(Ok(request) = request.build());
let headers = request.headers();
assert!(headers.get(reqwest::header::ACCEPT).is_none());
}
#[test_log::test]
#[ignore]
fn test_real_network_env_get() -> Result<(), NetworkError> {
let env = RealNetwork::new();
let net_request =
NetRequest::get(NetUrl::new("https://httpbin.org/get?arg=baz".into())).build();
let response: NetResponse<GetResponse> = block_on(env.get(net_request))?;
let_assert!(Some(body) = response.response_body());
assert_eq!(
body.args.get("arg"),
Some(&"baz".to_string()),
"args from body"
);
Ok(())
}
#[test_log::test]
#[ignore]
fn test_real_network_env_get_error() {
let env = RealNetwork::new();
let net_request =
NetRequest::get(NetUrl::new("https://httpbin.org/status/400".into())).build();
let result: Result<NetResponse<String>, NetworkError> = block_on(env.get(net_request));
assert!(result.is_err(), "response is not a String");
}
#[test_log::test]
#[ignore]
fn test_real_network_env_post_json() -> Result<(), NetworkError> {
let env = RealNetwork::new();
let body = serde_json::json!({"foo":"bar"});
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org/post?arg=baz".into()))
.json_body(body)?
.build();
let response: NetResponse<PostResponse> = block_on(env.post_json(net_request))?;
let_assert!(Some(body) = response.response_body());
assert_eq!(
body.args.get("arg"),
Some(&"baz".to_string()),
"args from body"
);
assert_eq!(body.data, "{\"foo\":\"bar\"}".to_string(), "data from body");
Ok(())
}
#[test_log::test]
#[ignore]
fn test_real_network_env_post_json_error() -> Result<(), NetworkError> {
let env = RealNetwork::new();
let net_request =
NetRequest::post(NetUrl::new("https://httpbin.org/status/400".into())).build();
let response: Result<NetResponse<PostResponse>, NetworkError> =
block_on(env.post_json(net_request));
match response {
Ok(_) => panic!("expected error"),
Err(e) => match e {
NetworkError::MockError(method, status, _url) => {
assert_eq!(method, RequestMethod::Post);
assert_eq!(status, StatusCode::BAD_REQUEST)
}
_ => panic!("unexpected error type"),
},
}
Ok(())
}
#[test_log::test]
#[ignore]
fn test_real_network_env_put() -> Result<(), NetworkError> {
let env = RealNetwork::new();
let body = serde_json::json!({"foo":"bar"});
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org/put?arg=baz".into()))
.json_body(body)?
.build();
let response: NetResponse<PutResponse> = block_on(env.put_json(net_request))?;
let_assert!(Some(body) = response.response_body());
assert_eq!(
body.args.get("arg"),
Some(&"baz".to_string()),
"args from body"
);
assert_eq!(body.data, "{\"foo\":\"bar\"}".to_string(), "data from body");
Ok(())
}
#[test_log::test]
#[ignore]
fn test_real_network_env_put_error() {
let env = RealNetwork::new();
let net_request =
NetRequest::put(NetUrl::new("https://httpbin.org/status/400".into())).build();
let result: Result<NetResponse<String>, NetworkError> = block_on(env.put_json(net_request));
assert!(result.is_err(), "response is not a String");
}
#[test_log::test]
#[ignore]
fn test_real_network_env_delete() {
let env = RealNetwork::new();
let net_request =
NetRequest::delete(NetUrl::new("https://httpbin.org/delete?arg=baz".into())).build();
let result = block_on(env.delete(net_request));
assert!(result.is_ok());
}
}

View file

@ -0,0 +1,86 @@
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RequestBody {
None,
String(String),
Json(serde_json::Value),
Xml(String), // no separate type for XML, so store as string
}
impl Default for RequestBody {
fn default() -> Self {
Self::None
}
}
impl TryFrom<&RequestBody> for String {
type Error = serde_json::Error;
fn try_from(value: &RequestBody) -> Result<Self, Self::Error> {
match value {
RequestBody::None => Ok("".to_string()),
RequestBody::String(s) => Ok(s.to_string()),
RequestBody::Json(json) => serde_json::to_string(&json),
RequestBody::Xml(s) => Ok(s.to_string()),
}
}
}
impl RequestBody {
pub fn json<T: Serialize>(source: T) -> Result<Self, serde_json::Error> {
serde_json::to_value(source).map(Self::Json)
}
pub fn content_type(&self) -> Option<String> {
match self {
Self::None => None,
Self::String(_) => Some("text/plain".to_string()),
Self::Json(_) => Some("application/json".to_string()),
Self::Xml(_) => Some("application/xml".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use assert2::let_assert;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
#[test_log::test]
fn test_request_body_json() {
let_assert!(Ok(json) = RequestBody::json("hello"));
assert_eq!(
json,
RequestBody::Json(serde_json::Value::String("hello".to_string()))
);
}
#[test_log::test]
fn test_request_body_content_type() {
assert_eq!(RequestBody::None.content_type(), None);
assert_eq!(
RequestBody::String("".to_string()).content_type(),
Some("text/plain".to_string())
);
assert_eq!(
RequestBody::Json(json!("")).content_type(),
Some("application/json".to_string())
);
assert_eq!(
RequestBody::Xml("".to_string()).content_type(),
Some("application/xml".to_string())
);
}
#[test_log::test]
fn test_request_body_try_from() {
let_assert!(Ok(value) = String::try_from(&RequestBody::None));
assert_eq!(value, "");
let_assert!(Ok(value) = String::try_from(&RequestBody::String("hello".to_string())));
assert_eq!(value, "hello");
let_assert!(Ok(value) = String::try_from(&RequestBody::Json(json!("hello"))));
assert_eq!(value, "\"hello\"");
let_assert!(Ok(value) = String::try_from(&RequestBody::Xml("hello".to_string())));
assert_eq!(value, "hello");
}
}

View file

@ -0,0 +1,26 @@
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum RequestMethod {
Get,
Post,
Put,
Patch,
Delete,
Propfind,
}
impl Default for RequestMethod {
fn default() -> Self {
Self::Get
}
}
impl std::fmt::Display for RequestMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Patch => write!(f, "PATCH"),
Self::Delete => write!(f, "DELETE"),
Self::Propfind => write!(f, "PROPFIND"),
}
}
}

View file

@ -0,0 +1,22 @@
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ResponseType {
Json,
Xml,
None,
Text,
}
impl ResponseType {
pub fn accept(&self) -> Option<String> {
match self {
Self::Json => Some("application/json".to_string()),
Self::Text => Some("text/plain".to_string()),
Self::Xml => Some("application/xml".to_string()),
Self::None => None,
}
}
}
impl Default for ResponseType {
fn default() -> Self {
Self::Json
}
}

View file

@ -0,0 +1,113 @@
use super::{RequestBody, RequestMethod};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SavedRequest {
method: RequestMethod,
url: String,
body: RequestBody,
}
impl SavedRequest {
pub fn new(method: RequestMethod, url: &str, body: RequestBody) -> Self {
Self {
method,
url: url.to_string(),
body,
}
}
pub const fn method(&self) -> RequestMethod {
self.method
}
pub fn url(&self) -> &str {
&self.url
}
pub const fn body(&self) -> &RequestBody {
&self.body
}
}
#[cfg(test)]
mod tests {
use assert2::let_assert;
use super::*;
#[test_log::test]
fn test_saved_request() {
let request =
SavedRequest::new(RequestMethod::Get, "https://httpbin.org", RequestBody::None);
assert_eq!(request.method, RequestMethod::Get);
assert_eq!(request.url, "https://httpbin.org");
assert_eq!(request.body, RequestBody::None);
}
#[test_log::test]
fn test_saved_request_body() {
let request = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
let_assert!(RequestBody::String(body) = request.body());
assert_eq!(body, "body");
}
#[test_log::test]
fn test_saved_request_eq() {
let request1 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
let request2 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
assert_eq!(request1, request2);
}
#[test_log::test]
fn test_saved_request_ne_method() {
let request1 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
let request2 = SavedRequest::new(
RequestMethod::Post,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
assert_ne!(request1, request2);
}
#[test_log::test]
fn test_saved_request_ne_url() {
let request1 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
let request2 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org2",
RequestBody::String("body".to_string()),
);
assert_ne!(request1, request2);
}
#[test_log::test]
fn test_saved_request_ne_body() {
let request1 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body".to_string()),
);
let request2 = SavedRequest::new(
RequestMethod::Get,
"https://httpbin.org",
RequestBody::String("body2".to_string()),
);
assert_ne!(request1, request2);
}
}