diff --git a/Cargo.toml b/Cargo.toml index 8800973..183f02a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,35 +14,24 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# logging -tracing = "0.1" - -# network async-trait = "0.1" +derive_more = { version = "1.0", features = [ "from", "display", "constructor" ] } http = "1.1" +path-clean = "1.0" reqwest = "0.12" secrecy = "0.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde-xml-rs = "0.6" -thiserror = "2.0" - -# fs tempfile = "3.10" -path-clean = "1.0" - -# boilerplate -derive_more = { version = "1.0.0-beta", features = [ - "from", - "display", - "constructor", -] } +thiserror = "2.0" +tracing = "0.1" [dev-dependencies] -# testing assert2 = "0.3" pretty_assertions = "1.4" test-log = "0.2" +tokio = { version = "1.41", features = ["macros"] } tokio-test = "0.4" [package.metadata.bin] diff --git a/src/lib.rs b/src/lib.rs index ac1f34b..82273b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ // pub mod fs; +pub mod net; + +#[deprecated] pub mod network; diff --git a/src/net/mock.rs b/src/net/mock.rs new file mode 100644 index 0000000..d9e9861 --- /dev/null +++ b/src/net/mock.rs @@ -0,0 +1,77 @@ +// +use super::{Error, Net, Result}; + +#[derive(Debug)] +struct Plan { + request: reqwest::Request, + response: reqwest::Response, +} + +#[derive(Default, Debug)] +pub struct MockNet { + plans: Vec, +} +impl MockNet { + pub(crate) fn new() -> Self { + Self::default() + } + + pub fn into_net(self) -> Net { + Net::mock(self) + } + + pub fn client(&self) -> reqwest::Client { + reqwest::Client::new() + } + + pub fn response(&self) -> http::response::Builder { + http::Response::builder() + } + + pub fn on(&mut self, request: reqwest::Request) -> OnRequest { + OnRequest { + mock: self, + request, + } + } + + fn _on(&mut self, request: reqwest::Request, response: reqwest::Response) { + self.plans.push(Plan { request, response }) + } + + pub(crate) async fn send( + &mut self, + request: reqwest::RequestBuilder, + ) -> Result { + let request = request.build()?; + let index = self.plans.iter().position(|plan| { + // TODO: add support or only matching on selected criteria + plan.request.method() == request.method() + && plan.request.url() == request.url() + && match (plan.request.body(), request.body()) { + (None, None) => true, + (Some(plan), Some(request)) => plan.as_bytes() == request.as_bytes(), + _ => false, + } + && plan.request.headers() == request.headers() + }); + match index { + Some(i) => Ok(self.plans.remove(i).response), + None => Err(Error::UnexpectedMockRequest(request)), + } + } + + pub fn assert(&self) -> Result<()> { + todo!() + } +} + +pub struct OnRequest<'mock> { + mock: &'mock mut MockNet, + request: reqwest::Request, +} +impl<'mock> OnRequest<'mock> { + pub fn response(self, response: reqwest::Response) { + self.mock._on(self.request, response) + } +} diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..5fd2654 --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,22 @@ +//! Provides a generic interface for network operations. +//! +//! + +mod mock; +mod system; +mod result; + +pub use result::{Error, Result}; + +pub use system::Net; + + +/// Creates a new `Net`. +pub const fn new() -> Net { + Net::new() +} + +/// Creates a new `MockNet` for use in tests. +pub fn mock() -> mock::MockNet { + mock::MockNet::new() +} diff --git a/src/net/result.rs b/src/net/result.rs new file mode 100644 index 0000000..205f3b6 --- /dev/null +++ b/src/net/result.rs @@ -0,0 +1,29 @@ +// + +use derive_more::derive::From; + +/// Represents a error accessing the network. +#[derive(Debug, From, derive_more::Display)] +pub enum Error { + Reqwest(reqwest::Error), + Request(String), + #[display("Unexpected request: {0}", 0.to_string())] + UnexpectedMockRequest(reqwest::Request), +} +impl std::error::Error for Error {} +impl Clone for Error { + fn clone(&self) -> Self { + match self { + Self::Reqwest(req) => Self::Request(req.to_string()), + Self::Request(req) => Self::Request(req.clone()), + Self::UnexpectedMockRequest(_) => todo!(), + } + } +} + +/// Represents a success or a failure. +/// +/// Any failure is related to `std::io`, a Path Traversal +/// (i.e. trying to escape the base of the `FileSystem`), +/// or attempting to use a file as a directory or /vise versa/. +pub type Result = core::result::Result; diff --git a/src/net/system.rs b/src/net/system.rs new file mode 100644 index 0000000..47add97 --- /dev/null +++ b/src/net/system.rs @@ -0,0 +1,31 @@ +// +use super::{mock::MockNet, Error}; + +pub struct Net { + mock: Option, +} +impl Net { + pub(crate) const fn mock(mock: MockNet) -> Self { + Self { mock: Some(mock) } + } + +} + +impl Net { + + pub(crate) const fn new() -> Self { + Self { mock: None } + } + + pub fn client(&self) -> reqwest::Client { + reqwest::Client::new() + } + + pub async fn send(&mut self, request: reqwest::RequestBuilder) -> Result { + if let Some(mock) = &mut self.mock { + mock.send(request).await + } else { + request.send().await.map_err(Error::from) + } + } +} diff --git a/tests/net.rs b/tests/net.rs new file mode 100644 index 0000000..da8d677 --- /dev/null +++ b/tests/net.rs @@ -0,0 +1,33 @@ +// +use kxio::net::Error; + +type TestResult = Result<(), Error>; + +mod get { + use super::*; + + #[tokio::test] + async fn test() -> TestResult { + let mut net_mock = kxio::net::mock(); + + let url = "https://www.example.com"; + let request = net_mock.client().get(url).build()?; + let my_response = net_mock.response().status(200).body("OK").unwrap(); + + net_mock.on(request).response(my_response.clone().into()); + + let mut net = net_mock.into_net(); + let client = net.client(); + + let response = net.send(client.get(url)).await?; + + assert_eq!(response.status(), my_response.status()); + + // net_mock.assert()?; + + // let my_net = net::new(); + // my_net.send(request).await?; + + Ok(()) + } +}