Compare commits
No commits in common. "50b7b0e69e777e6252ddfa3b3863df4ca6868759" and "02adc7dcd216eefba9245d69d187de2d4d153630" have entirely different histories.
50b7b0e69e
...
02adc7dcd2
4 changed files with 33 additions and 155 deletions
72
README.md
72
README.md
|
@ -1,69 +1,13 @@
|
||||||
# kxio
|
## kxio
|
||||||
|
|
||||||
`kxio` is a Rust library that provides injectable `FileSystem` and `Network`
|
Provides injectable Filesystem and Network resources to make code more testable.
|
||||||
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
|
||||||
|
|
||||||
- **Filesystem Abstraction**
|
Documentation is [here](https://docs.rs/kxio/latest/kxio/fs/).
|
||||||
- **Network Abstraction**
|
|
||||||
- **Enhanced Testability**
|
|
||||||
|
|
||||||
## Filesystem
|
### Network
|
||||||
|
|
||||||
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.
|
||||||
|
|
113
examples/get.rs
113
examples/get.rs
|
@ -1,98 +1,46 @@
|
||||||
/// This is an example to show fetching a file from a webiste and saving to a file
|
// example to show fetching a URL and saving to a file
|
||||||
///
|
|
||||||
/// The example consts of:
|
use std::path::Path;
|
||||||
///
|
|
||||||
/// - 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<()> {
|
||||||
// Create a `Net` object for making real network requests.
|
let net = kxio::net::new();
|
||||||
let net: kxio::net::Net = kxio::net::new();
|
let fs = kxio::fs::temp()?;
|
||||||
|
|
||||||
// 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");
|
||||||
|
|
||||||
// Create a PathBuf to a file within the directory that the `fs` object has access to.
|
download_and_save(url, &file_path, &fs, &net).await?;
|
||||||
let file_path = fs.base().join("example-readme.md");
|
|
||||||
|
|
||||||
// Create a generic handle for the file. This doesn't open the file, and always succeeds.
|
print_file(&file_path, &fs)?;
|
||||||
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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An function that uses a `FileSystem` and a `Net` object to interact with the outside world.
|
async fn download_and_save(
|
||||||
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);
|
||||||
// Uses the network abstraction to create a perfectly normal `reqwest::ResponseBuilder`.
|
let response = net.send(request).await?;
|
||||||
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());
|
||||||
// Uses the file system abstraction to create a handle for a file.
|
let file = fs.file(file_path);
|
||||||
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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An function that uses a `FileSystem` object to interact with the outside world.
|
fn print_file(file_path: &Path, fs: &kxio::fs::FileSystem) -> kxio::Result<()> {
|
||||||
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);
|
||||||
// Uses the file system abstraction to create a handle for a file.
|
let reader = file.reader()?;
|
||||||
let file: kxio::fs::PathReal<kxio::fs::FileMarker> = fs.file(file_path);
|
let contents = reader.as_str();
|
||||||
// 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(())
|
||||||
|
@ -100,44 +48,31 @@ fn delete_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
|
||||||
// Create a fake/mock network abstraction
|
let net = kxio::net::mock();
|
||||||
// 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"))
|
||||||
// declare what response should be made for a given request
|
.respond(
|
||||||
let response: http::Response<&str> = net.response()
|
net.response()
|
||||||
.body("contents")
|
.body("contents")
|
||||||
.expect("response body");
|
.expect("response body")
|
||||||
let request = net.client().get(url).build().expect("request");
|
.into(),
|
||||||
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
|
||||||
// Pass the file sytsem and network abstractions to the code to be tested
|
download_and_save(url, &file_path, &fs, &net.into())
|
||||||
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();
|
||||||
|
|
1
justfile
1
justfile
|
@ -8,7 +8,6 @@ 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"
|
||||||
|
|
|
@ -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 {
|
||||||
Default::default()
|
self.inner.client()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
||||||
|
|
Loading…
Reference in a new issue