Compare commits

...

4 commits

Author SHA1 Message Date
50b7b0e69e docs(examples): add annotations to the get example
Some checks failed
Rust / build (map[name:nightly]) (push) Failing after 38s
Rust / build (map[name:stable]) (push) Failing after 1m15s
2024-11-09 18:28:29 +00:00
9c0cf07bcc docs(readme): write a proper readme
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m30s
Rust / build (map[name:nightly]) (push) Successful in 1m57s
Release Please / Release-plz (push) Successful in 31s
2024-11-09 18:27:39 +00:00
d5340bec78 build: add cargo mutants to local dev test build step
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m48s
Rust / build (map[name:nightly]) (push) Successful in 1m59s
Release Please / Release-plz (push) Successful in 1m28s
2024-11-09 17:30:18 +00:00
4594a792e0 fix: use Default to create reqwest client
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m52s
Rust / build (map[name:stable]) (push) Successful in 4m19s
Release Please / Release-plz (push) Successful in 1m37s
2024-11-09 17:30:18 +00:00
4 changed files with 155 additions and 33 deletions

View file

@ -1,13 +1,69 @@
## kxio # kxio
Provides injectable Filesystem and Network resources to make code more testable. `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.
### Filesystem ## Features
Documentation is [here](https://docs.rs/kxio/latest/kxio/fs/). - **Filesystem Abstraction**
- **Network Abstraction**
- **Enhanced Testability**
### Network ## 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 the <https://docs.rs/kxio/latest/kxio/fs/>.
### 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
<https://docs.rs/kxio/latest/kxio/net/>
## Getting Started
Add `kxio` to your `Cargo.toml`:
```toml
[dependencies]
kxio = "x.y.z"
```
## Usage
See the example [get.rs](./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.
## Acknowledgements
- Built with Rust
---
For more information, bug reports, or feature requests, please visit our [repository](https://git.kemitix.net/kemitix/kxio).
The entire [network] module needs to be completly rewritten
It's use is strongly discouraged.
A new [net] module will likely be its replacement.

View file

@ -1,46 +1,98 @@
// example to show fetching a URL and saving to a file /// This is an example to show fetching a file from a webiste and saving to a file
///
use std::path::Path; /// The example consts of:
///
/// - The main program, in `main()` - demonstrates how to setup `kxio` for use in prod
/// - A test module - demonstrates how to use `kxio` in tests
/// - sample functions - showing how to use `kxio` the body of your program, and be testable
///
/// NOTE: running this program with `cargo run --example get` will create and delete the file
/// `example-readme.md` in the current directory.
use std::path::{Path, PathBuf};
#[tokio::main] #[tokio::main]
async fn main() -> kxio::Result<()> { async fn main() -> kxio::Result<()> {
let net = kxio::net::new(); // Create a `Net` object for making real network requests.
let fs = kxio::fs::temp()?; let net: kxio::net::Net = kxio::net::new();
// Create a `FileSystem` object for accessing files within the current directory.
// The object created will return a `PathTraveral` error result if there is an attempt to\
// access a file outside of this directory.
let fs: kxio::fs::FileSystem = kxio::fs::new(PathBuf::from("./"));
// The URL we will fetch - the readme for this library.
let url = "https://git.kemitix.net/kemitix/kxio/raw/branch/main/README.md"; let url = "https://git.kemitix.net/kemitix/kxio/raw/branch/main/README.md";
let file_path = fs.base().join("README.md");
download_and_save(url, &file_path, &fs, &net).await?; // Create a PathBuf to a file within the directory that the `fs` object has access to.
let file_path = fs.base().join("example-readme.md");
print_file(&file_path, &fs)?; // Create a generic handle for the file. This doesn't open the file, and always succeeds.
let path: kxio::fs::PathReal<kxio::fs::PathMarker> = fs.path(&file_path);
// Other options are;
// `fs.file(&file_path)` - for a file
// `fs.dir(&dir_path)` - for a directory
// Checks if the path exists (whether a file, directory, etc)
if path.exists()? {
// extracts the path from the handle
let pathbuf = path.as_pathbuf();
eprintln!("The file {} already exists. Aborting!", pathbuf.display());
return Ok(());
}
// Passes a reference to the `fs` and `net` objects for use by your program.
// Your programs should not know whether they are handling a mock or the real thing.
// Any file or network access should be made using these handlers to be properly testable.
download_and_save_to_file(url, &file_path, &fs, &net).await?;
delete_file(&file_path, &fs)?;
Ok(()) Ok(())
} }
async fn download_and_save( /// An function that uses a `FileSystem` and a `Net` object to interact with the outside world.
async fn download_and_save_to_file(
url: &str, url: &str,
file_path: &Path, file_path: &Path,
// The file system abstraction
fs: &kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
// The network abstraction
net: &kxio::net::Net, net: &kxio::net::Net,
) -> kxio::Result<()> { ) -> kxio::Result<()> {
println!("fetching: {url}"); println!("fetching: {url}");
let request = net.client().get(url);
let response = net.send(request).await?; // Uses the network abstraction to create a perfectly normal `reqwest::ResponseBuilder`.
let request: reqwest::RequestBuilder = net.client().get(url);
// Rather than calling `.build().send()?` on the request, pass it to the `net`
// This allows the `net` to either make the network request as normal, or, if we are
// under test, to handle the request as the test dictates.
// NOTE: if the `.build().send()` is called on the `request` then that WILL result in
// a real network request being made, even under test conditions. Only ever use the
// `net.send(...)` function to keep your code testable.
let response: reqwest::Response = net.send(request).await?;
let body = response.text().await?; let body = response.text().await?;
println!("fetched {} bytes", body.bytes().len()); println!("fetched {} bytes", body.bytes().len());
println!("writing file: {}", file_path.display()); println!("writing file: {}", file_path.display());
let file = fs.file(file_path); // Uses the file system abstraction to create a handle for a file.
let file: kxio::fs::PathReal<kxio::fs::FileMarker> = fs.file(file_path);
// Writes the body to the file.
file.write(body)?; file.write(body)?;
Ok(()) Ok(())
} }
fn print_file(file_path: &Path, fs: &kxio::fs::FileSystem) -> kxio::Result<()> { /// An function that uses a `FileSystem` object to interact with the outside world.
fn delete_file(file_path: &Path, fs: &kxio::fs::FileSystem) -> kxio::Result<()> {
println!("reading file: {}", file_path.display()); println!("reading file: {}", file_path.display());
let file = fs.file(file_path);
let reader = file.reader()?; // Uses the file system abstraction to create a handle for a file.
let contents = reader.as_str(); let file: kxio::fs::PathReal<kxio::fs::FileMarker> = fs.file(file_path);
// Creates a `Reader` which loaded the file into memory.
let reader: kxio::fs::Reader = file.reader()?;
let contents: &str = reader.as_str();
println!("{contents}"); println!("{contents}");
Ok(()) Ok(())
@ -48,31 +100,44 @@ fn print_file(file_path: &Path, fs: &kxio::fs::FileSystem) -> kxio::Result<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use kxio::net::MatchOn;
use super::*; use super::*;
// This test demonstrates how to use the `kxio` to test your program.
#[tokio::test] #[tokio::test]
async fn should_save_remote_body() { async fn should_save_remote_body() {
//given //given
let net = kxio::net::mock(); // Create a fake/mock network abstraction
// When `net` goes out of scope it will check that all the expected network requests (see
// `net.on(...)` below) were all made. If there are any that were not made, the test will
// be failed. If you want to avoid this, then call `net.reset()` before your test ends.
let net: kxio::net::MockNet = kxio::net::mock();
let url = "http://localhost:8080"; let url = "http://localhost:8080";
net.on(net.client().get(url).build().expect("request"))
.respond( // declare what response should be made for a given request
net.response() let response: http::Response<&str> = net.response()
.body("contents") .body("contents")
.expect("response body") .expect("response body");
.into(), let request = net.client().get(url).build().expect("request");
) net.on(request)
// By default, the METHOD and URL must match, equivalent to:
//.match_on(vec![MatchOn::Method, MatchOn::Url])
.respond(response.into())
.expect("mock"); .expect("mock");
// Create a temporary directory that will be deleted with `fs` goes out of scope
let fs = kxio::fs::temp().expect("temp fs"); let fs = kxio::fs::temp().expect("temp fs");
let file_path = fs.base().join("foo"); let file_path = fs.base().join("foo");
//when //when
download_and_save(url, &file_path, &fs, &net.into()) // Pass the file sytsem and network abstractions to the code to be tested
download_and_save_to_file(url, &file_path, &fs, &net.into())
.await .await
.expect("system under test"); .expect("system under test");
//then //then
// Open a file and read it
let file = fs.file(&file_path); let file = fs.file(&file_path);
let reader = file.reader().expect("reader"); let reader = file.reader().expect("reader");
let contents = reader.as_str(); let contents = reader.as_str();

View file

@ -8,6 +8,7 @@ build:
cargo hack test cargo hack test
cargo doc cargo doc
cargo test --example get cargo test --example get
cargo mutants --jobs 4
install-hooks: install-hooks:
@echo "Installing git hooks" @echo "Installing git hooks"

View file

@ -53,7 +53,7 @@ impl Net {
impl Net { impl Net {
// public interface // public interface
pub fn client(&self) -> reqwest::Client { pub fn client(&self) -> reqwest::Client {
self.inner.client() Default::default()
} }
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> { pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {