diff --git a/README.md b/README.md
index e69904e..461695e 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ file system and network operations.
The Filesystem module offers a clean abstraction over `std::fs`, the standard
file system operations. For comprehensive documentation and usage examples,
-please refer to the .
+please refer to .
### Key Filesystem Features:
diff --git a/src/lib.rs b/src/lib.rs
index fd7df0d..a75dcc0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,68 @@
-//
+//! # kxio
+//!
+//! `kxio` is a Rust library that provides injectable `FileSystem` and `Network`
+//! resources to enhance the testability of your code. By abstracting system-level
+//! interactions, `kxio` enables easier mocking and testing of code that relies on
+//! file system and network operations.
+//!
+//! ## Features
+//!
+//! - Filesystem Abstraction
+//! - Network Abstraction
+//! - Enhanced Testability
+//!
+//! ## Filesystem
+//!
+//! The Filesystem module offers a clean abstraction over `std::fs`, the standard
+//! file system operations. For comprehensive documentation and usage examples,
+//! please refer to .
+//!
+//! ### Key Filesystem Features:
+//!
+//! - File reading and writing
+//! - Directory operations
+//! - File metadata access
+//! - Fluent API for operations like `.reader().bytes()`
+//!
+//! ## Network
+//!
+//! The Network module offers a testable interface over the `reqwest` crate. For
+//! comprehensive documentation and usage examples, please refer to
+//!
+//!
+//! ## Getting Started
+//!
+//! Add `kxio` to your `Cargo.toml`:
+//!
+//! ```toml
+//! [dependencies]
+//! kxio = "x.y.z"
+//! ```
+//!
+//! ## Usage
+//!
+//! See the example [get.rs](https://git.kemitix.net/kemitix/kxio/src/branch/main/examples/get.rs) for an annotated example on how to use the `kxio` library.
+//! It covers both the `net` and `fs` modules.
+//!
+//! ## Development
+//!
+//! - The project uses [Cargo Mutants](https://crates.io/crates/cargo-mutants) for mutation testing.
+//! - [ForgeJo Actions](https://forgejo.org/docs/next/user/actions/) are used for continuous testing and linting.
+//!
+//! ## Contributing
+//!
+//! Contributions are welcome! Please check our [issue tracker](https://git.kemitix.net/kemitix/kxio/issues) for open tasks or
+//! submit your own ideas.
+//!
+//! ## License
+//!
+//! This project is licensed under the terms specified in the `LICENSE` file in the
+//! repository root.
+//!
+//! ---
+//!
+//! For more information, bug reports, or feature requests, please visit our [repository](https://git.kemitix.net/kemitix/kxio).
+
pub mod fs;
pub mod net;
mod result;
diff --git a/src/net/mod.rs b/src/net/mod.rs
index 1161184..2970da6 100644
--- a/src/net/mod.rs
+++ b/src/net/mod.rs
@@ -149,6 +149,148 @@
//!
//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum,
//! allowing for specific error handling related to network operations.
+//! Provides a testable interface over the [reqwest] crate.
+//!
+//! ## Overview
+//!
+//! The `net` module provides a testable interface for network operations.
+//! It includes implementations for both real network interactions and mocked network operations for testing purposes.
+//!
+//! ## Key methods and types:
+//!
+//! - [kxio::net::new()][new()]: Creates a new `Net` instance
+//! - [kxio::net::mock()][mock()]: Creates a new `MockNet` instance for use in tests
+//! - [Error]: enum for network-related errors
+//! - [Result]: an alias for `core::result::Result`
+//! - [Net]: struct for real and mocked network operations
+//! - [MockNet]: struct for defining behaviours of mocked network operations
+//!
+//! ## Usage
+//!
+//! Write your program to take a reference to [Net].
+//!
+//! Use the [Net::client] functionto create a [reqwest::RequestBuilder] which you should then pass to the [Net::send] method.
+//! This is rather than building the request and calling its own `send` method, doing so would result in the network request being sent, even under-test.
+//!
+//! ```rust
+//! use kxio::net;
+//! async fn get_example(net: &net::Net) -> net::Result<()> {
+//! let response = net.send(net.client().get("https://example.com")).await?;
+//! ///...
+//! Ok(())
+//! }
+//! ```
+//!
+//! ### Real Network Operations
+//!
+//! In your production code you will want to make real network requests.
+//!
+//! Construct a [Net] using [kxio::net::new()][new()]. Then pass as a reference to your program.
+//!
+//! ```rust
+//! use kxio::net;
+//! # #[tokio::main]
+//! # async fn main() -> net::Result<()> {
+//! let net = net::new();
+//!
+//! get_example(&net).await?;
+//! # Ok(())
+//! # }
+//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
+//! ```
+//!
+//! ### Mocked Network Operations
+//!
+//! In your tests you will want to mock your network requests and responses.
+//!
+//! Construct a [MockNet] using [kxio::net::mock()][mock()].
+//!
+//! ```rust
+//! use kxio::net;
+//! let mock_net = net::mock();
+//! ```
+//!
+//! Create a [reqwest::Client] using [MockNet::client()].
+//!
+//! ```rust
+//! # let mock_net = kxio::net::mock();
+//! let client = mock_net.client();
+//! // this is the same as:
+//! let client = reqwest::Client::new();
+//! ```
+//!
+//! Define the expected responses for each request, using the [MockNet::on],
+//! that you expect you program to make during the test. You can choose what each request should be
+//! matched against. The default is to the match when both the Method and Url are the same.
+//!
+//! ```rust
+//! use kxio::net;
+//! use kxio::net::MatchOn;
+//!
+//! # #[tokio::main]
+//! # async fn main() -> net::Result<()> {
+//! # let mock_net = net::mock();
+//! # let client = mock_net.client();
+//! mock_net.on(client.get("https://example.com"))
+//! .match_on(vec![
+//! MatchOn::Url,
+//! MatchOn::Method
+//! ])
+//! .respond_with_body(mock_net.response().status(200).body("Mocked response"))?;
+//! # mock_net.reset();
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! All [MatchOn] options:
+//! - [MatchOn::Method] (default)
+//! - [MatchOn::Url] (default)
+//! - [MatchOn::Headers]
+//! - [MatchOn::Body].
+//!
+//! Once you have defined all your expected responses, convert the [MockNet] into a [Net].
+//!
+//! ```rust
+//! # use kxio::net;
+//! # let mock_net = net::mock();
+//! let net: net::Net = mock_net.into();
+//! // or
+//! # let mock_net = net::mock();
+//! let net = net::Net::from(mock_net);
+//! ```
+//!
+//! Now you can pass a reference to `net` to your program.
+//!
+//! ```rust
+//! # use kxio::net;
+//! # #[tokio::main]
+//! # async fn main() -> net::Result<()> {
+//! # let mock_net = net::mock();
+//! # let net = net::Net::from(mock_net);
+//! get_example(&net).await?;
+//! # Ok(())
+//! # }
+//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
+//! ```
+//!
+//! When your test is finished, the [MockNet] will check that all the expected requests were
+//! actually made. If there were any missed, then the test will [panic].
+//!
+//! If you don't want this to happen, then call [MockNet::reset] before your test finishes.
+//! You will need to recover the [MockNet] from the [Net].
+//!
+//! ```rust
+//! # use kxio::net;
+//! # let mock_net = net::mock();
+//! # let net = net::Net::from(mock_net);
+//! let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
+//! mock_net.reset();
+//! ````
+//!
+//! ## Error Handling
+//!
+//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum,
+//! allowing for specific error handling related to network operations.
mod result;
mod system;
diff --git a/src/net/result.rs b/src/net/result.rs
index 18295e4..0742be6 100644
--- a/src/net/result.rs
+++ b/src/net/result.rs
@@ -1,15 +1,32 @@
//
use derive_more::derive::From;
-/// Represents a error accessing the network.
+/// The Errors that may occur within [kxio::net][crate::net].
#[derive(Debug, From, derive_more::Display)]
pub enum Error {
+ /// The Errors that may occur when processing a `Request`.
+ ///
+ /// Note: Errors may include the full URL used to make the `Request`. If the URL
+ /// contains sensitive information (e.g. an API key as a query parameter), be
+ /// sure to remove it ([`without_url`](reqwest::Error::without_url))
Reqwest(reqwest::Error),
+
+ /// The Errors that may occur when processing a `Request`.
+ ///
+ /// The cause has been converted to a String.
Request(String),
+
+ /// There was network request that doesn't match any that were expected
#[display("Unexpected request: {0}", 0.to_string())]
UnexpectedMockRequest(reqwest::Request),
+
+ /// There was an error accessing the list of expected requests.
RwLockLocked,
+
+ /// There was an error making a network request.
Http(http::Error),
+
+ /// Attempted to extract a [MockNet][super::MockNet] from a [Net][super::Net] that does not contain one.
NetIsNotAMock,
}
impl std::error::Error for Error {}
@@ -22,7 +39,7 @@ impl Clone for Error {
}
}
-/// Represents a success or a failure.
+/// Represents a success or a failure within [kxio::net][crate::net].
///
/// Any failure is related to `std::io`, a Path Traversal
/// (i.e. trying to escape the base of the `FileSystem`),
diff --git a/src/net/system.rs b/src/net/system.rs
index 6374e74..04bfc83 100644
--- a/src/net/system.rs
+++ b/src/net/system.rs
@@ -5,26 +5,41 @@ use reqwest::Client;
use super::{Error, Result};
+/// Marker trait used to identify whether a [Net] is mocked or not.
pub trait NetType {}
+/// Marker struct that indicates that a [Net] is a mock.
#[derive(Debug)]
pub struct Mocked;
impl NetType for Mocked {}
+/// Market struct that indicates that a [Net] is not mocked.
#[derive(Debug)]
pub struct Unmocked;
impl NetType for Unmocked {}
+/// A list of planned requests and responses
type Plans = Vec;
+/// The different ways to match a request.
#[derive(Debug, PartialEq, Eq)]
pub enum MatchOn {
+ /// The request must have a specific HTTP Request Method.
Method,
+
+ /// The request must have a specific URL.
Url,
+
+ /// The request must have a specify HTTP Body.
Body,
+
+ /// The request must have a specific set of HTTP Headers.
Headers,
}
+/// A planned request and the response to return
+///
+/// Contains a list of the criteria that a request must meet before being considered a match.
#[derive(Debug)]
struct Plan {
request: reqwest::Request,
@@ -39,7 +54,7 @@ pub struct Net {
mock: Option>,
}
impl Net {
- // constructors
+ /// Creates a new unmocked [Net] for creating real network requests.
pub(super) const fn new() -> Self {
Self {
inner: InnerNet::::new(),
@@ -47,6 +62,7 @@ impl Net {
}
}
+ /// Creats a new [MockNet] for use in tests.
pub(super) const fn mock() -> MockNet {
MockNet {
inner: InnerNet::::new(),
@@ -54,11 +70,39 @@ impl Net {
}
}
impl Net {
- // public interface
+ /// Helper to create a default [reqwest::Client].
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # use kxio::net::Result;
+ /// let net = kxio::net::new();
+ /// let client = net.client();
+ /// let request = client.get("https://hyper.rs");
+ /// ```
pub fn client(&self) -> reqwest::Client {
Default::default()
}
+ /// Constructs the Request and sends it to the target URL, returning a
+ /// future Response.
+ ///
+ /// # Errors
+ ///
+ /// This method fails if there was an error while sending request,
+ /// redirect loop was detected or redirect limit was exhausted.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use kxio::net::Result;
+ /// # async fn run() -> Result<()> {
+ /// let net = kxio::net::new();
+ /// let request = net.client().get("https://hyper.rs");
+ /// let response = net.send(request).await?;
+ /// # Ok(())
+ /// # }
+ /// ```
pub async fn send(
&self,
request: impl Into,
@@ -88,27 +132,105 @@ impl TryFrom for MockNet {
}
}
+/// A struct for defining the expected requests and their responses that should be made
+/// during a test.
+///
+/// When the [MockNet] goes out of scope it will verify that all expected requests were consumed,
+/// otherwise it will `panic`.
+///
+/// # Example
+///
+/// ```rust
+/// # use kxio::net::Result;
+/// # fn run() -> Result<()> {
+/// let mock_net = kxio::net::mock();
+/// let client = mock_net.client();
+/// // define an expected requet, and the response that should be returned
+/// mock_net.on(client.get("https://hyper.rs"))
+/// .respond_with_body(mock_net.response().status(200).body("Ok"))?;
+/// let net: kxio::net::Net = mock_net.into();
+/// // use 'net' in your program, by passing it as a reference
+///
+/// // In some rare cases you don't want to assert that all expected requests were made.
+/// // You should recover the `MockNet` from the `Net` and `MockNet::reset` it.
+/// let mock_net = kxio::net::MockNet::try_from(net)?;
+/// mock_net.reset(); // only if explicitly needed
+/// # Ok(())
+/// # }
+/// ```
#[derive(Debug)]
pub struct MockNet {
inner: InnerNet,
}
impl MockNet {
+ /// Helper to create a default [reqwest::Client].
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mock_net = kxio::net::mock();
+ /// let client = mock_net.client();
+ /// let request = client.get("https://hyper.rs");
+ /// ```
pub fn client(&self) -> Client {
Default::default()
}
+
+ /// Specify an expected request.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # use kxio::net::Result;
+ /// # fn run() -> Result<()> {
+ /// let mock_net = kxio::net::mock();
+ /// let client = mock_net.client();
+ /// mock_net.on(client.get("https://hyper.rs"))
+ /// .respond_with_body(mock_net.response().status(200).body("Ok"))?;
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn on(&self, request: impl Into) -> OnRequest {
self.inner.on(request)
}
+
/// Creates a [http::response::Builder] to be extended and returned by a mocked network request.
pub fn response(&self) -> http::response::Builder {
Default::default()
}
+
+ /// Constructs the request and matches it against the expected requests.
+ ///
+ /// The first on it finds where it matches against its criteria will be taken ("consumed") and
+ /// its response returned.
+ ///
+ /// The order the expected requests are scanned is the order they were declared. As each
+ /// expected request is successfully matched against, it is removed and can't be matched
+ /// against again.
pub async fn send(
&self,
request: impl Into,
) -> Result {
self.inner.send(request).await
}
+
+ /// Clears all the expected requests and responses from the [MockNet].
+ ///
+ /// When the [MockNet] goes out of scope it will assert that all expected requests and
+ /// responses were consumed. If there are any left unconsumed, then it will `panic`.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # use kxio::net::Result;
+ /// # fn run() -> Result<()> {
+ /// # let mock_net = kxio::net::mock();
+ /// # let net: kxio::net::Net = mock_net.into();
+ /// let mock_net = kxio::net::MockNet::try_from(net)?;
+ /// mock_net.reset(); // only if explicitly needed
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn reset(&self) -> Result<()> {
self.inner.reset()
}
@@ -124,6 +246,9 @@ impl From for Net {
}
}
+/// Part of the inner workings of [Net] and [MockNet].
+///
+/// Holds the list of expected requests for [MockNet].
#[derive(Debug)]
pub struct InnerNet {
_type: PhantomData,
@@ -240,6 +365,7 @@ impl Drop for InnerNet {
}
}
+/// Intermediate struct used while declaring an expected request and its response.
pub enum OnRequest<'net> {
Valid {
net: &'net InnerNet,
@@ -249,6 +375,14 @@ pub enum OnRequest<'net> {
Error(super::Error),
}
impl<'net> OnRequest<'net> {
+ /// Specify the criteria that a request should be compared against.
+ ///
+ /// Given an candidate request, it will be matched against the current request if it has the
+ /// same HTTP Method and Url by default. i.e. if this method is not called.
+ ///
+ /// All criteria must be met for the match to be made.
+ ///
+ /// Calling this method replaces the default criteria with the new criteria.
pub fn match_on(self, match_on: Vec) -> Self {
if let OnRequest::Valid {
net,
@@ -265,6 +399,12 @@ impl<'net> OnRequest<'net> {
self
}
}
+
+ /// Constructs the response to be returned when a request matched the criteria.
+ ///
+ /// The response will have an empty HTTP Body.
+ ///
+ /// Each request and response can only be matched once each.
pub fn respond(self, response: impl Into) -> Result<()> {
match self {
OnRequest::Valid {
@@ -275,6 +415,10 @@ impl<'net> OnRequest<'net> {
OnRequest::Error(error) => Err(error),
}
}
+
+ /// Constructs the response to be returned when a request matched the criteria.
+ ///
+ /// Each request and response can only be matched once each.
pub fn respond_with_body(
self,
body_result: std::result::Result, http::Error>,