From f9d8696e00704a65e0e59fa8c4a5a5672f9d0646 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Mon, 4 Nov 2024 10:22:31 +0000 Subject: [PATCH] WIP: feat!: fluent api for net --- Cargo.toml | 21 ++++--------- src/lib.rs | 3 ++ src/net/mock.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++ src/net/mod.rs | 22 ++++++++++++++ src/net/result.rs | 27 +++++++++++++++++ src/net/system.rs | 30 +++++++++++++++++++ tests/net.rs | 36 ++++++++++++++++++++++ 7 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/net/mock.rs create mode 100644 src/net/mod.rs create mode 100644 src/net/result.rs create mode 100644 src/net/system.rs create mode 100644 tests/net.rs 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..883de93 --- /dev/null +++ b/src/net/mock.rs @@ -0,0 +1,76 @@ +// +use super::{Error, Net, Result}; + +struct Plan { + request: reqwest::Request, + response: reqwest::Response, +} + +#[derive(Default)] +pub struct MockNet { + plans: Vec, +} +impl MockNet { + pub(crate) fn new() -> Self { + Self::default() + } + + pub fn as_net(&self) -> Net<'_> { + Net::mock(self) + } + + pub fn client(&self) -> reqwest::Client { + Net::client() + } + + pub fn response(&self) -> http::response::Builder { + http::Response::builder() + } + + pub fn on(&mut self, request: reqwest::RequestBuilder) -> Result> { + Ok(OnRequest { + mock: self, + request: request.build()?, + }) + } + + 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 ony 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..93229b7 --- /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<'static> { + 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..fe664ec --- /dev/null +++ b/src/net/result.rs @@ -0,0 +1,27 @@ +// + +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), + 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()), + } + } +} + +/// 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..5759e2f --- /dev/null +++ b/src/net/system.rs @@ -0,0 +1,30 @@ +// +use super::{mock::MockNet, Error}; + +pub struct Net<'mock> { + mock: Option<&'mock MockNet>, +} +impl<'mock> Net<'mock> { + pub(crate) const fn mock(mock: &'mock MockNet) -> Self { + Self { mock: Some(mock) } + } + +} + +impl Net<'_> { + + pub(crate) const fn new() -> Self { + Self { mock: None } + } + + pub fn client() -> reqwest::Client { + reqwest::Client::new() + } + + pub async fn send(&self, request: reqwest::RequestBuilder) -> Result { + match self.mock { + Some(mock) => mock.send(request).await, + None => request.send().await.map_err(Error::from), + } + } +} diff --git a/tests/net.rs b/tests/net.rs new file mode 100644 index 0000000..e662dd6 --- /dev/null +++ b/tests/net.rs @@ -0,0 +1,36 @@ +// +use kxio::net::Error; + +type TestResult = Result<(), Error>; + +mod get { + + use kxio::net::Net; + + 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); + let my_response = net_mock.response().status(200).body("OK").unwrap(); + + net_mock.on(request).response(my_response.clone().into()); + + let net = net_mock.as_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(()) + } +}