Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
41 changed files with 2869 additions and 2866 deletions
|
@ -1,4 +1,7 @@
|
||||||
# ./cargo/config
|
# ./cargo/config
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "/usr/bin/clang-15"
|
||||||
|
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold", "--cfg", "tokio_unstable"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
name: Release Please
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-plz:
|
|
||||||
name: Release-plz
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Run release-plz release-pr
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: release-plz release-pr --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
||||||
- name: Run release-plz release
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["next"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: docker
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
toolchain:
|
|
||||||
- name: stable
|
|
||||||
- name: nightly
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check TODOs
|
|
||||||
uses: kemitix/todo-checker@v1.1.0
|
|
||||||
|
|
||||||
- name: Machete
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo machete
|
|
||||||
|
|
||||||
- name: Format
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
|
|
||||||
|
|
||||||
- name: Clippy
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test
|
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -16,6 +16,3 @@ Cargo.lock
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
mutants.out/
|
|
||||||
mutants.out.old/
|
|
||||||
|
|
77
.woodpecker.yml
Normal file
77
.woodpecker.yml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
steps:
|
||||||
|
|
||||||
|
update-builder-image:
|
||||||
|
when:
|
||||||
|
- event: cron
|
||||||
|
image: docker.io/woodpeckerci/plugin-docker-buildx:3.2
|
||||||
|
settings:
|
||||||
|
username: kemitix
|
||||||
|
repo: git.kemitix.net/kemitix/kxio-builder
|
||||||
|
auto_tag: true
|
||||||
|
dockerfile: Dockerfile.builder
|
||||||
|
dry-run: false # push to remote repo
|
||||||
|
registry: git.kemitix.net
|
||||||
|
password:
|
||||||
|
from_secret: woodpecker-docker-push
|
||||||
|
|
||||||
|
todo_check:
|
||||||
|
# INFO: https://woodpecker-ci.org/plugins/TODO-Checker
|
||||||
|
image: codeberg.org/epsilon_02/todo-checker:1.1
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: [main, next]
|
||||||
|
settings:
|
||||||
|
# kxio-woodpecker-todo-checker - read:issue
|
||||||
|
repository_token: '4acf14f93747e044aa2d1397367741b53f3d4f8f'
|
||||||
|
prefix_regex: "(#|//) (TODO|FIXME): "
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
lint_and_build:
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: [main, next]
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
image: git.kemitix.net/kemitix/kxio-builder:latest
|
||||||
|
environment:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
commands:
|
||||||
|
- ls -l /usr/local/cargo/bin/
|
||||||
|
- cargo fmt --all -- --check
|
||||||
|
- cargo clippy -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
|
||||||
|
- cargo build
|
||||||
|
|
||||||
|
test:
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: [main, next]
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
image: git.kemitix.net/kemitix/kxio-builder:latest
|
||||||
|
environment:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
commands:
|
||||||
|
- cargo test
|
||||||
|
|
||||||
|
publish_to_crates_io:
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
image: docker.io/rust:1.77
|
||||||
|
commands:
|
||||||
|
- cargo login "$CARGO_REGISTRY_TOKEN"
|
||||||
|
- cargo publish --registry crates-io --no-verify
|
||||||
|
secrets: [cargo_registry_token]
|
||||||
|
|
||||||
|
publish_to_forgejo:
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
# INFO: https://woodpecker-ci.org/plugins/Gitea%20Release
|
||||||
|
image: docker.io/woodpeckerci/plugin-gitea-release:0.3
|
||||||
|
settings:
|
||||||
|
base_url: https://git.kemitix.net
|
||||||
|
api_key:
|
||||||
|
from_secret: FORGEJO_RELEASE_PLUGIN
|
||||||
|
target: main
|
||||||
|
prerelease: true
|
30
Cargo.toml
30
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "kxio"
|
name = "kxio"
|
||||||
version = "1.2.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
||||||
description = "Provides injectable Filesystem and Network resources to make code more testable"
|
description = "Provides injectable Filesystem and Network resources to make code more testable"
|
||||||
|
@ -8,30 +8,34 @@ license = "MIT"
|
||||||
repository = "https://git.kemitix.net/kemitix/kxio"
|
repository = "https://git.kemitix.net/kemitix/kxio"
|
||||||
exclude = [".cargo_home"]
|
exclude = [".cargo_home"]
|
||||||
|
|
||||||
[lints.rust]
|
[features]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
fs = []
|
||||||
|
network = []
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
derive_more = { version = "1.0", features = [
|
# logging
|
||||||
"constructor",
|
tracing = "0.1"
|
||||||
"display",
|
|
||||||
"from"
|
# network
|
||||||
] }
|
async-trait = "0.1"
|
||||||
http = "1.1"
|
http = "1.1"
|
||||||
path-clean = "1.0"
|
|
||||||
reqwest = "0.12"
|
reqwest = "0.12"
|
||||||
|
secrecy = "0.8"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde-xml-rs = "0.6"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# fs
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
# testing
|
||||||
assert2 = "0.3"
|
assert2 = "0.3"
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
test-log = "0.2"
|
test-log = "0.2"
|
||||||
tokio = { version = "1.41", features = [
|
|
||||||
"macros",
|
|
||||||
"rt-multi-thread"
|
|
||||||
] }
|
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|
||||||
[package.metadata.bin]
|
[package.metadata.bin]
|
||||||
|
|
23
Dockerfile.builder
Normal file
23
Dockerfile.builder
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
FROM docker.io/rust:latest
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y clang-15 mold && \
|
||||||
|
curl -L https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz -o cargo-binstall.tgz && \
|
||||||
|
tar -xzf cargo-binstall.tgz && \
|
||||||
|
rm cargo-binstall.tgz && \
|
||||||
|
mv cargo-binstall /usr/local/bin/ && \
|
||||||
|
cargo binstall -y cargo-chef && \
|
||||||
|
rustup component add rustfmt clippy
|
||||||
|
|
||||||
|
# verify that the binaries are installed
|
||||||
|
RUN ls -l /usr/local/cargo/bin/
|
||||||
|
RUN cargo chef --version
|
||||||
|
RUN rustfmt --version
|
||||||
|
RUN cargo fmt --version
|
||||||
|
RUN cargo clippy --version
|
||||||
|
RUN mold --version
|
||||||
|
RUN clang-15 --version
|
||||||
|
RUN cargo --version
|
||||||
|
RUN rustc --version
|
||||||
|
RUN rustup --version
|
||||||
|
RUN rustup show
|
70
README.md
70
README.md
|
@ -1,69 +1,5 @@
|
||||||
# kxio
|
## kxio
|
||||||
|
|
||||||
`kxio` is a Rust library that provides injectable `FileSystem` and `Network`
|
[![status-badge](https://ci.kemitix.net/api/badges/53/status.svg)](https://ci.kemitix.net/repos/53)
|
||||||
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 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).
|
|
||||||
|
|
||||||
|
Provides injectable Filesystem and Network resources to make code more testable.
|
||||||
|
|
144
examples/get.rs
144
examples/get.rs
|
@ -1,144 +0,0 @@
|
||||||
/// This is an example to show fetching a file from a webiste and saving to a file
|
|
||||||
///
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> kxio::Result<()> {
|
|
||||||
// Create a `Net` object for making real network requests.
|
|
||||||
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("./");
|
|
||||||
|
|
||||||
// The URL we will fetch - the readme for this library.
|
|
||||||
let url = "https://git.kemitix.net/kemitix/kxio/raw/branch/main/README.md";
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
// 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
file_path: &Path,
|
|
||||||
// The file system abstraction
|
|
||||||
fs: &kxio::fs::FileSystem,
|
|
||||||
// The network abstraction
|
|
||||||
net: &kxio::net::Net,
|
|
||||||
) -> kxio::Result<()> {
|
|
||||||
println!("fetching: {url}");
|
|
||||||
|
|
||||||
// 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?;
|
|
||||||
println!("fetched {} bytes", body.bytes().len());
|
|
||||||
|
|
||||||
println!("writing file: {}", file_path.display());
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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());
|
|
||||||
|
|
||||||
// Uses the file system abstraction to create a handle for a file.
|
|
||||||
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}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// This test demonstrates how to use the `kxio` to test your program.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn should_save_remote_body() {
|
|
||||||
//given
|
|
||||||
// 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";
|
|
||||||
|
|
||||||
// declare what response should be made for a given request
|
|
||||||
let response: http::Response<&str> =
|
|
||||||
net.response().body("contents").expect("response body");
|
|
||||||
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");
|
|
||||||
|
|
||||||
// Create a temporary directory that will be deleted with `fs` goes out of scope
|
|
||||||
let fs = kxio::fs::temp().expect("temp fs");
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// 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
|
|
||||||
.expect("system under test");
|
|
||||||
|
|
||||||
//then
|
|
||||||
// Open a file and read it
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
let reader = file.reader().expect("reader");
|
|
||||||
let contents = reader.as_str();
|
|
||||||
|
|
||||||
assert_eq!(contents, "contents");
|
|
||||||
}
|
|
||||||
}
|
|
17
justfile
17
justfile
|
@ -1,20 +1,3 @@
|
||||||
build:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
cargo fmt
|
|
||||||
cargo fmt --check
|
|
||||||
cargo hack clippy
|
|
||||||
cargo hack build
|
|
||||||
cargo hack test
|
|
||||||
cargo doc
|
|
||||||
cargo test --example get
|
|
||||||
cargo mutants --jobs 4
|
|
||||||
|
|
||||||
install-hooks:
|
install-hooks:
|
||||||
@echo "Installing git hooks"
|
@echo "Installing git hooks"
|
||||||
git config core.hooksPath .git-hooks
|
git config core.hooksPath .git-hooks
|
||||||
|
|
||||||
validate-dev-branch:
|
|
||||||
git rebase -i origin/main -x 'cargo build --features "fs,network"'
|
|
||||||
git rebase -i origin/main -x 'cargo test --features "fs,network"'
|
|
||||||
git rebase -i origin/main -x 'cargo clippy --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used'
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended"],
|
"extends": [
|
||||||
"packageRules": [
|
"config:recommended"
|
||||||
{
|
|
||||||
"matchManagers": ["cargo"],
|
|
||||||
"rangeStrategy": "replace"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
151
src/filesystem.rs
Normal file
151
src/filesystem.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ops::Deref,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::{tempdir, TempDir};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum FileSystem {
|
||||||
|
Real(RealFileSystemEnv),
|
||||||
|
Temp(TempFileSystemEnv),
|
||||||
|
}
|
||||||
|
impl FileSystem {
|
||||||
|
pub fn new_real(cwd: Option<PathBuf>) -> Self {
|
||||||
|
let cwd = cwd.unwrap_or_default();
|
||||||
|
Self::Real(RealFileSystemEnv::new(cwd))
|
||||||
|
}
|
||||||
|
pub fn new_temp() -> std::io::Result<Self> {
|
||||||
|
TempFileSystemEnv::new().map(Self::Temp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for FileSystem {
|
||||||
|
type Target = dyn FileSystemEnv;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Real(env) => env,
|
||||||
|
Self::Temp(env) => env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
|
||||||
|
fn cwd(&self) -> &PathBuf;
|
||||||
|
|
||||||
|
fn in_cwd(&self, name: &str) -> PathBuf {
|
||||||
|
self.cwd().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(&self, file_name: &str, content: &str) -> std::io::Result<PathBuf> {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{LineWriter, Write};
|
||||||
|
|
||||||
|
let path = self.in_cwd(file_name);
|
||||||
|
debug!("writing to {:?}", path);
|
||||||
|
let file = File::create(path.clone())?;
|
||||||
|
let mut file = LineWriter::new(file);
|
||||||
|
file.write_all(content.as_bytes())?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_exists(&self, name: &PathBuf) -> bool {
|
||||||
|
use std::fs::File;
|
||||||
|
File::open(name).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file(&self, file_name: &str) -> std::io::Result<String> {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
let path = self.in_cwd(file_name);
|
||||||
|
debug!("reading from {:?}", path);
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let mut content = String::new();
|
||||||
|
file.read_to_string(&mut content)?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct RealFileSystemEnv {
|
||||||
|
cwd: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TempFileSystemEnv {
|
||||||
|
cwd: PathBuf,
|
||||||
|
temp_dir: Arc<Mutex<TempDir>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSystemEnv for TempFileSystemEnv {
|
||||||
|
fn cwd(&self) -> &PathBuf {
|
||||||
|
&self.cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSystemEnv for RealFileSystemEnv {
|
||||||
|
fn cwd(&self) -> &PathBuf {
|
||||||
|
&self.cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealFileSystemEnv {
|
||||||
|
const fn new(cwd: PathBuf) -> Self {
|
||||||
|
Self { cwd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempFileSystemEnv {
|
||||||
|
fn new() -> std::io::Result<Self> {
|
||||||
|
let temp_dir = tempdir()?;
|
||||||
|
info!("temp dir: {:?}", temp_dir.path());
|
||||||
|
let cwd = temp_dir.path().to_path_buf();
|
||||||
|
let temp_dir = Arc::new(Mutex::new(temp_dir));
|
||||||
|
Ok(Self { cwd, temp_dir })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_cwd() {
|
||||||
|
let cwd = PathBuf::from("/tmp");
|
||||||
|
let env = RealFileSystemEnv::new(cwd.clone());
|
||||||
|
assert_eq!(env.cwd(), &cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_create_on_temp_fs() -> std::io::Result<()> {
|
||||||
|
let env = TempFileSystemEnv::new()?;
|
||||||
|
assert!(env.cwd().exists());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_create_on_real_fs() {
|
||||||
|
let cwd = PathBuf::from("/tmp");
|
||||||
|
let env = RealFileSystemEnv::new(cwd.clone());
|
||||||
|
assert_eq!(env.cwd(), &cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_write_and_read_file() -> std::io::Result<()> {
|
||||||
|
let env = TempFileSystemEnv::new()?;
|
||||||
|
let file_name = "test.txt";
|
||||||
|
let content = "Hello, World!";
|
||||||
|
let path = env.write_file(file_name, content)?;
|
||||||
|
assert_eq!(env.read_file(file_name)?, content);
|
||||||
|
assert!(path.exists());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
121
src/fs/dir.rs
121
src/fs/dir.rs
|
@ -1,121 +0,0 @@
|
||||||
//
|
|
||||||
use crate::fs::{DirItem, DirItemIterator, Result};
|
|
||||||
|
|
||||||
use super::{DirHandle, Error, FileHandle, PathHandle, PathMarker};
|
|
||||||
|
|
||||||
impl DirHandle {
|
|
||||||
/// Creates a new, empty directory at the path
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::create_dir]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// dir.create()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn create(&self) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::create_dir(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively create a directory and all of its parent components if they are missing.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::create_dir_all]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// dir.create_all()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn create_all(&self) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::create_dir_all(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator over the entries within a directory.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::read_dir]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// for entry in dir.read()? { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn read(&self) -> Result<Box<dyn Iterator<Item = Result<DirItem>>>> {
|
|
||||||
self.check_error()?;
|
|
||||||
let read_dir = std::fs::read_dir(self.as_pathbuf()).map_err(Error::Io)?;
|
|
||||||
Ok(Box::new(DirItemIterator::new(read_dir)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes an empty directory.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::remove_dir]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// dir.remove()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// }
|
|
||||||
pub fn remove(&self) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::remove_dir(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively remove a directory and all of its contents.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::remove_dir_all]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// dir.remove_all()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// }
|
|
||||||
pub fn remove_all(&self) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::remove_dir_all(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TryFrom<PathHandle<PathMarker>> for FileHandle {
|
|
||||||
type Error = crate::fs::Error;
|
|
||||||
|
|
||||||
fn try_from(path: PathHandle<PathMarker>) -> std::result::Result<Self, Self::Error> {
|
|
||||||
match path.as_file() {
|
|
||||||
Ok(Some(dir)) => Ok(dir.clone()),
|
|
||||||
Ok(None) => Err(crate::fs::Error::NotADirectory {
|
|
||||||
path: path.as_pathbuf(),
|
|
||||||
}),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TryFrom<PathHandle<PathMarker>> for DirHandle {
|
|
||||||
type Error = crate::fs::Error;
|
|
||||||
|
|
||||||
fn try_from(path: PathHandle<PathMarker>) -> std::result::Result<Self, Self::Error> {
|
|
||||||
match path.as_dir() {
|
|
||||||
Ok(Some(dir)) => Ok(dir.clone()),
|
|
||||||
Ok(None) => Err(crate::fs::Error::NotADirectory {
|
|
||||||
path: path.as_pathbuf(),
|
|
||||||
}),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
//
|
|
||||||
use std::{
|
|
||||||
fs::{DirEntry, ReadDir},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Error;
|
|
||||||
|
|
||||||
/// Represents an item in a directory
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum DirItem {
|
|
||||||
File(PathBuf),
|
|
||||||
Dir(PathBuf),
|
|
||||||
SymLink(PathBuf),
|
|
||||||
Fifo(PathBuf),
|
|
||||||
Unsupported(PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An iterator for items in a directory.
|
|
||||||
#[derive(Debug, derive_more::Constructor)]
|
|
||||||
pub struct DirItemIterator(ReadDir);
|
|
||||||
impl Iterator for DirItemIterator {
|
|
||||||
type Item = super::Result<DirItem>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.0.next().map(map_dir_item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_dir_item(item: std::io::Result<DirEntry>) -> super::Result<DirItem> {
|
|
||||||
let item = item.map_err(Error::Io)?;
|
|
||||||
let file_type = item.file_type().map_err(Error::Io)?;
|
|
||||||
if file_type.is_dir() {
|
|
||||||
Ok(DirItem::Dir(item.path()))
|
|
||||||
} else if file_type.is_file() {
|
|
||||||
Ok(DirItem::File(item.path()))
|
|
||||||
} else if file_type.is_symlink() {
|
|
||||||
Ok(DirItem::SymLink(item.path()))
|
|
||||||
} else {
|
|
||||||
Ok(DirItem::Unsupported(item.path()))
|
|
||||||
}
|
|
||||||
}
|
|
101
src/fs/file.rs
101
src/fs/file.rs
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
use crate::fs::Result;
|
|
||||||
|
|
||||||
use super::{reader::Reader, Error, FileHandle};
|
|
||||||
|
|
||||||
impl FileHandle {
|
|
||||||
/// Returns a [Reader] for the file.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// let reader = file.reader()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn reader(&self) -> Result<Reader> {
|
|
||||||
self.check_error()?;
|
|
||||||
Reader::new(&self.as_pathbuf())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a slice as the entire contents of a file.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::write]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo").join("bar");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// file.write("new file contents")?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::write(self.as_pathbuf(), contents).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copies the contents of a file to another file.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::copy]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let src_path = fs.base().join("foo");
|
|
||||||
/// let dest_path = fs.base().join("bar");
|
|
||||||
/// let src = fs.file(&src_path);
|
|
||||||
/// # fs.file(&dest_path).write("new file contents")?;
|
|
||||||
/// let dest = fs.file(&dest_path);
|
|
||||||
/// src.copy(&dest)?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn copy(&self, dest: &FileHandle) -> Result<u64> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::copy(self.as_pathbuf(), dest.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes a file.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::remove_file]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// file.remove()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn remove(&self) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::remove_file(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a hard link on the filesystem.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::hard_link]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let src_path = fs.base().join("foo");
|
|
||||||
/// let src = fs.file(&src_path);
|
|
||||||
/// # src.write("bar")?;
|
|
||||||
/// let dst_path = fs.base().join("bar");
|
|
||||||
/// let dst = fs.file(&dst_path);
|
|
||||||
/// src.hard_link(&dst)?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn hard_link(&self, dest: &FileHandle) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::hard_link(self.as_pathbuf(), dest.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
//! Provides an injectable reference to part of the filesystem.
|
|
||||||
//!
|
|
||||||
//! Create a new `FileSystem` to access a directory using `kxio::fs::new(path)`.
|
|
||||||
//! Create a new `TempFileSystem` to access a temporary directory using `kxio::fs::temp()?`;
|
|
||||||
//!
|
|
||||||
//! `TempFileSystem` derefs automatically to `FileSystem` so can be used anywhere
|
|
||||||
//! you would use `FileSystem`.
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! # use std::path::PathBuf;
|
|
||||||
//! # use kxio::fs::FileSystem;
|
|
||||||
//! # use kxio::fs::PathReal;
|
|
||||||
//! # use kxio::fs::DirHandle;
|
|
||||||
//! # use kxio::fs::FileHandle;
|
|
||||||
//! # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
//! let fs = kxio::fs::temp()?;
|
|
||||||
//! let fs: FileSystem = kxio::fs::new(fs.base().to_path_buf());
|
|
||||||
//! let dir_path: PathBuf = fs.base().join("foo");
|
|
||||||
//! let dir: DirHandle = fs.dir(&dir_path);
|
|
||||||
//! dir.create()?;
|
|
||||||
//! let file_path = dir_path.join("bar.txt");
|
|
||||||
//! let file: FileHandle = fs.file(&file_path);
|
|
||||||
//! file.write("new file contents")?;
|
|
||||||
//! let reader = file.reader()?;
|
|
||||||
//! assert_eq!(reader.to_string(), "new file contents");
|
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! # Standard library equivalents
|
|
||||||
//!
|
|
||||||
//! Given a `FileSystem` `fs`:
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! let fs = kxio::fs::temp().expect("temp fs"); // for testing
|
|
||||||
//! // or
|
|
||||||
//! # let pathbuf = fs.base().join("foo").to_path_buf();
|
|
||||||
//! let fs = kxio::fs::new(pathbuf);
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! - [x] `std::fs::canonicalize` - `fs.path(path).canonicalize()` - Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved.
|
|
||||||
//! - [x] `std::fs::copy` - `fs.file(path).copy(target)` - Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file.
|
|
||||||
//! - [x] `std::fs::create_dir` - `fs.dir(path).create()` - Creates a new, empty directory at the provided path
|
|
||||||
//! - [x] `std::fs::create_dir_all` - `fs.dir(path).create_all()` - Recursively create a directory and all of its parent components if they are missing.
|
|
||||||
//! - [x] `std::fs::hard_link` - `fs.file(path).hard_link(other)` - Creates a new hard link on the filesystem.
|
|
||||||
//! - [x] `std::fs::metadata` - `fs.path(path).metadata()` - Given a path, query the file system to get information about a file, directory, etc.
|
|
||||||
//! - [x] `std::fs::read` - `fs.file(path).reader().bytes()` - Read the entire contents of a file into a bytes vector.
|
|
||||||
//! - [x] `std::fs::read_dir` - `fs.dir(path).read()` - Returns an iterator over the entries within a directory.
|
|
||||||
//! - [x] `std::fs::read_link` - `fs.path(path).read_link()` - Reads a symbolic link, returning the file that the link points to.
|
|
||||||
//! - [x] `std::fs::read_to_string` - `fs.file(path).reader().to_string()` - Read the entire contents of a file into a string.
|
|
||||||
//! - [x] `std::fs::remove_dir` - `fs.dir(path).remove()` - Removes an empty directory.
|
|
||||||
//! - [x] `std::fs::remove_dir_all` - `fs.dir(path).remove_all()` - Removes a directory at this path, after removing all its contents. Use carefully!
|
|
||||||
//! - [x] `std::fs::remove_file` - `fs.file(path).remove()` - Removes a file from the filesystem.
|
|
||||||
//! - [x] `std::fs::rename` - `fs.path(path).rename()` - Rename a file or directory to a new name, replacing the original file if to already exists.
|
|
||||||
//! - [x] `std::fs::set_permissions` - `fs.path(path).set_permissions(perms)` - Changes the permissions found on a file or a directory.
|
|
||||||
//! - [x] `std::fs::symlink_metadata` - `fs.path(path).symlink_metadata()` - Query the metadata about a file without following symlinks.
|
|
||||||
//! - [x] `std::fs::write` - `fs.file(path).write()` - Write a slice as the entire contents of a file.
|
|
||||||
//!
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
mod dir;
|
|
||||||
mod dir_item;
|
|
||||||
mod file;
|
|
||||||
mod path;
|
|
||||||
mod reader;
|
|
||||||
mod result;
|
|
||||||
mod system;
|
|
||||||
mod temp;
|
|
||||||
|
|
||||||
pub use dir_item::{DirItem, DirItemIterator};
|
|
||||||
pub use path::*;
|
|
||||||
pub use reader::Reader;
|
|
||||||
pub use result::{Error, Result};
|
|
||||||
pub use system::{DirHandle, FileHandle, FileSystem, PathHandle};
|
|
||||||
|
|
||||||
/// Creates a new `FileSystem` for the path.
|
|
||||||
///
|
|
||||||
/// This will create a `FileSystem` that provides access to the
|
|
||||||
/// filesystem under the given path.
|
|
||||||
///
|
|
||||||
/// Any attempt to access outside this base will result in a
|
|
||||||
/// `error::Error::PathTraversal` error when attempting the
|
|
||||||
/// opertation.
|
|
||||||
pub fn new(base: impl Into<PathBuf>) -> FileSystem {
|
|
||||||
FileSystem::new(base.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new `TempFileSystem` for a temporary directory.
|
|
||||||
///
|
|
||||||
/// The `TempFileSystem` provides a `Deref` to a `FileSystem` for
|
|
||||||
/// the temporary directory.
|
|
||||||
///
|
|
||||||
/// When the `TempFileSystem` is dropped, the temporary directory
|
|
||||||
/// is deleted.
|
|
||||||
///
|
|
||||||
/// Returns an error if the temporary directory cannot be created.
|
|
||||||
pub fn temp() -> Result<temp::TempFileSystem> {
|
|
||||||
temp::TempFileSystem::new()
|
|
||||||
}
|
|
369
src/fs/path.rs
369
src/fs/path.rs
|
@ -1,369 +0,0 @@
|
||||||
//
|
|
||||||
use std::{
|
|
||||||
marker::PhantomData,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::fs::{Error, Result};
|
|
||||||
|
|
||||||
use super::{DirHandle, FileHandle, PathHandle};
|
|
||||||
|
|
||||||
/// Marker trait for the type of [PathReal].
|
|
||||||
pub trait PathType {}
|
|
||||||
|
|
||||||
/// Path marker for the type of [PathReal].
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct PathMarker;
|
|
||||||
impl PathType for PathMarker {}
|
|
||||||
|
|
||||||
/// File marker for the type of [PathReal].
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FileMarker;
|
|
||||||
impl PathType for FileMarker {}
|
|
||||||
|
|
||||||
/// Dir marker for the type of [PathReal].
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DirMarker;
|
|
||||||
impl PathType for DirMarker {}
|
|
||||||
|
|
||||||
/// Represents a path in the filesystem.
|
|
||||||
///
|
|
||||||
/// It can be a simple path, or it can be a file or a directory.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct PathReal<T: PathType> {
|
|
||||||
base: PathBuf,
|
|
||||||
path: PathBuf,
|
|
||||||
_phanton: PhantomData<T>,
|
|
||||||
pub(super) error: Option<Error>,
|
|
||||||
}
|
|
||||||
impl<T: PathType> PathReal<T> {
|
|
||||||
pub(super) fn new(base: impl Into<PathBuf>, path: impl Into<PathBuf>) -> Self {
|
|
||||||
let base: PathBuf = base.into();
|
|
||||||
let path: PathBuf = path.into();
|
|
||||||
let error = PathReal::<T>::validate(&base, &path);
|
|
||||||
Self {
|
|
||||||
base,
|
|
||||||
path,
|
|
||||||
_phanton: PhantomData::<T>,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [PathBuf] for the path.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let path = fs.path(&path);
|
|
||||||
/// let pathbuf = path.as_pathbuf();
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn as_pathbuf(&self) -> PathBuf {
|
|
||||||
self.base.join(&self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn put(&mut self, error: Error) {
|
|
||||||
if self.error.is_none() {
|
|
||||||
self.error.replace(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate(base: &Path, path: &Path) -> Option<Error> {
|
|
||||||
match PathReal::<PathMarker>::clean_path(path) {
|
|
||||||
Err(error) => Some(error),
|
|
||||||
Ok(path) => {
|
|
||||||
if !path.starts_with(base) {
|
|
||||||
return Some(Error::PathTraversal {
|
|
||||||
base: base.to_path_buf(),
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean_path(path: &Path) -> Result<PathBuf> {
|
|
||||||
// let path = path.as_ref();
|
|
||||||
use path_clean::PathClean;
|
|
||||||
let abs_path = if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::current_dir().expect("current_dir").join(path)
|
|
||||||
}
|
|
||||||
.clean();
|
|
||||||
Ok(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn check_error(&self) -> Result<()> {
|
|
||||||
if let Some(error) = &self.error {
|
|
||||||
Err(error.clone())
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the path exists.
|
|
||||||
///
|
|
||||||
/// N.B. If you have the path used to create the file or directory, you
|
|
||||||
/// should use [std::path::Path::exists] instead.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// # dir.create()?;
|
|
||||||
/// if dir.exists()? { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn exists(&self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.as_pathbuf().exists())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the path is a directory.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// # fs.dir(&path).create()?;
|
|
||||||
/// if path.is_dir() { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn is_dir(&self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.as_pathbuf().is_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the path is a file.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// # fs.dir(&path).create()?;
|
|
||||||
/// if path.is_file() { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn is_file(&self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.as_pathbuf().is_file())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path as a directory if it exists and is a directory, otherwise
|
|
||||||
/// it will return None.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// # fs.dir(&path).create()?;
|
|
||||||
/// let file = fs.path(&path);
|
|
||||||
/// if let Ok(Some(dir)) = file.as_dir() { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn as_dir(&self) -> Result<Option<DirHandle>> {
|
|
||||||
self.check_error()?;
|
|
||||||
if self.as_pathbuf().is_dir() {
|
|
||||||
Ok(Some(PathReal::new(&self.base, &self.path)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path as a file if it exists and is a file, otherwise
|
|
||||||
/// it will return None.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// # fs.dir(&path).create()?;
|
|
||||||
/// let file = fs.path(&path);
|
|
||||||
/// if let Ok(Some(file)) = file.as_file() { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn as_file(&self) -> Result<Option<FileHandle>> {
|
|
||||||
self.check_error()?;
|
|
||||||
if self.as_pathbuf().is_file() {
|
|
||||||
Ok(Some(PathReal::new(&self.base, &self.path)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renames a path.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::rename]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let src_path = fs.base().join("foo");
|
|
||||||
/// let src = fs.file(&src_path);
|
|
||||||
/// # src.write("bar")?;
|
|
||||||
/// let dst_path = fs.base().join("bar");
|
|
||||||
/// let dst = fs.file(&dst_path);
|
|
||||||
/// src.rename(&dst)?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn rename(&self, dest: &PathHandle<T>) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::rename(self.as_pathbuf(), dest.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the metadata for a path.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::metadata]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// let metadata = file.metadata()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn metadata(&self) -> Result<std::fs::Metadata> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::metadata(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a symbolic link to a path.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::os::unix::fs::symlink]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let src_path = fs.base().join("foo");
|
|
||||||
/// let src = fs.file(&src_path);
|
|
||||||
/// # src.write("bar")?;
|
|
||||||
/// let link_path = fs.base().join("bar");
|
|
||||||
/// let link = fs.path(&link_path);
|
|
||||||
/// src.soft_link(&link)?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn soft_link(&self, link: &PathReal<PathMarker>) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::os::unix::fs::symlink(self.as_pathbuf(), link.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the path is a symbolic link.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// # dir.create()?;
|
|
||||||
/// if dir.is_link()? { /* ... */ }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn is_link(&self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.as_pathbuf().is_symlink())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the canonical, absolute form of the path with all intermediate
|
|
||||||
/// components normalized and symbolic links resolved.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// # fs.dir(&path).create()?;
|
|
||||||
/// let dir = fs.path(&path);
|
|
||||||
/// let canonical = dir.canonicalize()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn canonicalize(&self) -> Result<PathBuf> {
|
|
||||||
self.check_error()?;
|
|
||||||
self.as_pathbuf().canonicalize().map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the metadata for a path without following symlinks.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::symlink_metadata]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// let metadata = file.symlink_metadata()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn symlink_metadata(&self) -> Result<std::fs::Metadata> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::symlink_metadata(self.as_pathbuf()).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the permissions of a file or directory.
|
|
||||||
///
|
|
||||||
/// Wrapper for [std::fs::set_permissions]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # use std::os::unix::fs::PermissionsExt;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// # file.write("bar")?;
|
|
||||||
/// file.set_permissions(std::fs::Permissions::from_mode(0o755))?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn set_permissions(&self, perms: std::fs::Permissions) -> Result<()> {
|
|
||||||
self.check_error()?;
|
|
||||||
std::fs::set_permissions(self.as_pathbuf(), perms).map_err(Error::Io)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_link(&self) -> Result<PathReal<PathMarker>> {
|
|
||||||
self.check_error()?;
|
|
||||||
let read_path = std::fs::read_link(self.as_pathbuf()).map_err(Error::Io)?;
|
|
||||||
let path = read_path.strip_prefix(&self.base).unwrap().to_path_buf();
|
|
||||||
Ok(PathReal::new(&self.base, &path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<PathHandle<PathMarker>> for PathBuf {
|
|
||||||
fn from(path: PathReal<PathMarker>) -> Self {
|
|
||||||
path.base.join(path.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<DirHandle> for PathHandle<PathMarker> {
|
|
||||||
fn from(dir: DirHandle) -> Self {
|
|
||||||
PathReal::new(dir.base, dir.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FileHandle> for PathHandle<PathMarker> {
|
|
||||||
fn from(file: FileHandle) -> Self {
|
|
||||||
PathReal::new(file.base, file.path)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
//
|
|
||||||
use std::{fmt::Display, path::Path, str::Lines};
|
|
||||||
|
|
||||||
use crate::fs::Result;
|
|
||||||
|
|
||||||
use super::Error;
|
|
||||||
|
|
||||||
/// A reader for a file.
|
|
||||||
pub struct Reader {
|
|
||||||
contents: String,
|
|
||||||
}
|
|
||||||
impl Reader {
|
|
||||||
pub(super) fn new(path: &Path) -> Result<Self> {
|
|
||||||
let contents = std::fs::read_to_string(path).map_err(Error::Io)?;
|
|
||||||
Ok(Self { contents })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the contents of the file as a string.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// # file.write("new file contents")?;
|
|
||||||
/// let contents = file.reader()?.as_str();
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.contents
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator over the lines in the file.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// # file.write("new file contents")?;
|
|
||||||
/// let lines = file.reader()?.lines();
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn lines(&self) -> Lines<'_> {
|
|
||||||
self.contents.lines()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the contents of the file as bytes.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use kxio::fs::Result;
|
|
||||||
/// # fn main() -> Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// # file.write("new file contents")?;
|
|
||||||
/// let bytes = file.reader()?.bytes();
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn bytes(&self) -> &[u8] {
|
|
||||||
self.contents.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Display for Reader {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.contents)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
//
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Represents a error accessing the file system.
|
|
||||||
///
|
|
||||||
/// 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/.
|
|
||||||
#[derive(Debug, derive_more::Display)]
|
|
||||||
pub enum Error {
|
|
||||||
Io(std::io::Error),
|
|
||||||
|
|
||||||
IoString(String),
|
|
||||||
|
|
||||||
#[display("Path access attempted outside of base ({base:?}): {path:?}")]
|
|
||||||
PathTraversal {
|
|
||||||
base: PathBuf,
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[display("Path must be a directory: {path:?}")]
|
|
||||||
NotADirectory {
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[display("Path must be a file: {path:?}")]
|
|
||||||
NotAFile {
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
impl Clone for Error {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Error::Io(err) => Error::IoString(err.to_string()),
|
|
||||||
Error::IoString(err) => Error::IoString(err.clone()),
|
|
||||||
Error::PathTraversal { base, path } => Error::PathTraversal {
|
|
||||||
base: base.clone(),
|
|
||||||
path: path.clone(),
|
|
||||||
},
|
|
||||||
Error::NotADirectory { path } => Error::NotADirectory { path: path.clone() },
|
|
||||||
Error::NotAFile { path } => Error::NotAFile { path: path.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<T> = core::result::Result<T, Error>;
|
|
153
src/fs/system.rs
153
src/fs/system.rs
|
@ -1,153 +0,0 @@
|
||||||
//
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::fs::{Error, Result};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
path::{DirMarker, FileMarker, PathReal},
|
|
||||||
PathMarker,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents to base of a section of a file system.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FileSystem {
|
|
||||||
base: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a directory path in the filesystem.
|
|
||||||
pub type DirHandle = PathReal<DirMarker>;
|
|
||||||
|
|
||||||
/// Represents a file path in the filesystem.
|
|
||||||
pub type FileHandle = PathReal<FileMarker>;
|
|
||||||
|
|
||||||
/// Represents a path in the filesystem.
|
|
||||||
pub type PathHandle<T> = PathReal<T>;
|
|
||||||
|
|
||||||
impl FileSystem {
|
|
||||||
pub const fn new(base: PathBuf) -> Self {
|
|
||||||
Self { base }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the base of the [FileSystem].
|
|
||||||
pub fn base(&self) -> &Path {
|
|
||||||
&self.base
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [PathBuf] for the path.
|
|
||||||
pub fn path_of(&self, path: PathBuf) -> Result<PathBuf> {
|
|
||||||
let path_of = self.base.as_path().join(path);
|
|
||||||
self.validate(&path_of)?;
|
|
||||||
Ok(path_of)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Access the path as a directory.
|
|
||||||
///
|
|
||||||
/// The path must exist and be a directory.
|
|
||||||
///
|
|
||||||
/// If the path does not exist, or is not a directory, an error is returned.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let dir = fs.dir(&path);
|
|
||||||
/// dir.create()?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn dir(&self, path: &Path) -> DirHandle {
|
|
||||||
let mut dir = PathReal::new(&self.base, path);
|
|
||||||
|
|
||||||
if dir.error.is_none() {
|
|
||||||
if let Ok(exists) = dir.exists() {
|
|
||||||
if exists {
|
|
||||||
if let Ok(is_dir) = dir.is_dir() {
|
|
||||||
if !is_dir {
|
|
||||||
dir.put(Error::NotADirectory {
|
|
||||||
path: dir.as_pathbuf(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Access the path as a file.
|
|
||||||
///
|
|
||||||
/// The path must exist and be a file.
|
|
||||||
///
|
|
||||||
/// If the path does not exist, or is not a file, an error is returned.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let file = fs.file(&path);
|
|
||||||
/// file.write("new file contents")?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn file(&self, path: &Path) -> FileHandle {
|
|
||||||
let mut file = PathReal::new(&self.base, path);
|
|
||||||
|
|
||||||
if file.error.is_none() {
|
|
||||||
if let Ok(exists) = file.exists() {
|
|
||||||
if exists {
|
|
||||||
if let Ok(is_file) = file.is_file() {
|
|
||||||
if !is_file {
|
|
||||||
file.put(Error::NotAFile {
|
|
||||||
path: file.as_pathbuf(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Access the path as a path.
|
|
||||||
///
|
|
||||||
/// The path must exist.
|
|
||||||
///
|
|
||||||
/// If the path does not exist, an error is returned.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn try_main() -> kxio::fs::Result<()> {
|
|
||||||
/// let fs = kxio::fs::temp()?;
|
|
||||||
/// let path = fs.base().join("foo");
|
|
||||||
/// let path_handle = fs.path(&path);
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn path(&self, path: &Path) -> PathHandle<PathMarker> {
|
|
||||||
PathReal::new(&self.base, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate(&self, path: &Path) -> Result<()> {
|
|
||||||
let path = self.clean_path(path)?;
|
|
||||||
if !path.starts_with(&self.base) {
|
|
||||||
return Err(Error::PathTraversal {
|
|
||||||
base: self.base.clone(),
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean_path(&self, path: &Path) -> Result<PathBuf> {
|
|
||||||
// let path = path.as_ref();
|
|
||||||
use path_clean::PathClean;
|
|
||||||
let abs_path = if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::current_dir().map_err(Error::Io)?.join(path)
|
|
||||||
}
|
|
||||||
.clean();
|
|
||||||
Ok(abs_path)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
use super::{Error, FileSystem};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TempFileSystem {
|
|
||||||
real: FileSystem,
|
|
||||||
_temp_dir: Arc<Mutex<TempDir>>,
|
|
||||||
}
|
|
||||||
impl TempFileSystem {
|
|
||||||
pub fn new() -> super::Result<Self> {
|
|
||||||
let temp_dir = tempfile::tempdir().map_err(Error::Io)?;
|
|
||||||
let base = temp_dir.path().to_path_buf();
|
|
||||||
let temp_dir = Arc::new(Mutex::new(temp_dir));
|
|
||||||
let real = super::new(base);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
real,
|
|
||||||
_temp_dir: temp_dir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::ops::Deref for TempFileSystem {
|
|
||||||
type Target = FileSystem;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.real
|
|
||||||
}
|
|
||||||
}
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -1,6 +1,4 @@
|
||||||
//
|
#[cfg(feature = "fs")]
|
||||||
pub mod fs;
|
pub mod filesystem;
|
||||||
pub mod net;
|
#[cfg(feature = "network")]
|
||||||
mod result;
|
pub mod network;
|
||||||
|
|
||||||
pub use result::{Error, Result};
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
//! Provides a generic interface for network operations.
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
|
|
||||||
mod result;
|
|
||||||
mod system;
|
|
||||||
|
|
||||||
pub use result::{Error, Result};
|
|
||||||
|
|
||||||
pub use system::{MatchOn, MockNet, Net};
|
|
||||||
|
|
||||||
/// Creates a new `Net`.
|
|
||||||
pub const fn new() -> Net {
|
|
||||||
Net::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new `MockNet` for use in tests.
|
|
||||||
pub fn mock() -> MockNet {
|
|
||||||
Net::mock()
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
//
|
|
||||||
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),
|
|
||||||
RwLockLocked,
|
|
||||||
}
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
impl Clone for Error {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Reqwest(req) => Self::Request(req.to_string()),
|
|
||||||
err => err.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<T> = core::result::Result<T, Error>;
|
|
|
@ -1,220 +0,0 @@
|
||||||
//
|
|
||||||
use std::{marker::PhantomData, ops::Deref, sync::RwLock};
|
|
||||||
|
|
||||||
use super::{Error, Result};
|
|
||||||
|
|
||||||
pub trait NetType {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Mocked;
|
|
||||||
impl NetType for Mocked {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Unmocked;
|
|
||||||
impl NetType for Unmocked {}
|
|
||||||
|
|
||||||
type Plans = Vec<Plan>;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum MatchOn {
|
|
||||||
Method,
|
|
||||||
Url,
|
|
||||||
Body,
|
|
||||||
Headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Plan {
|
|
||||||
request: reqwest::Request,
|
|
||||||
response: reqwest::Response,
|
|
||||||
match_on: Vec<MatchOn>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Net {
|
|
||||||
inner: InnerNet<Unmocked>,
|
|
||||||
mock: Option<InnerNet<Mocked>>,
|
|
||||||
}
|
|
||||||
impl Net {
|
|
||||||
// constructors
|
|
||||||
pub(super) const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: InnerNet::<Unmocked>::new(),
|
|
||||||
mock: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) const fn mock() -> MockNet {
|
|
||||||
MockNet {
|
|
||||||
inner: InnerNet::<Mocked>::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Net {
|
|
||||||
// public interface
|
|
||||||
pub fn client(&self) -> reqwest::Client {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
|
||||||
match &self.mock {
|
|
||||||
Some(mock) => mock.send(request).await,
|
|
||||||
None => self.inner.send(request).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MockNet {
|
|
||||||
inner: InnerNet<Mocked>,
|
|
||||||
}
|
|
||||||
impl Deref for MockNet {
|
|
||||||
type Target = InnerNet<Mocked>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<MockNet> for Net {
|
|
||||||
fn from(mock_net: MockNet) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: InnerNet::<Unmocked>::new(),
|
|
||||||
mock: Some(mock_net.inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct InnerNet<T: NetType> {
|
|
||||||
_type: PhantomData<T>,
|
|
||||||
plans: RwLock<Plans>,
|
|
||||||
}
|
|
||||||
impl InnerNet<Unmocked> {
|
|
||||||
const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
_type: PhantomData,
|
|
||||||
plans: RwLock::new(vec![]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
|
||||||
request.send().await.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: NetType> InnerNet<T> {
|
|
||||||
pub fn client(&self) -> reqwest::Client {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl InnerNet<Mocked> {
|
|
||||||
const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
_type: PhantomData,
|
|
||||||
plans: RwLock::new(vec![]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
|
||||||
let request = request.build()?;
|
|
||||||
let read_plans = self.plans.read().map_err(|_| Error::RwLockLocked)?;
|
|
||||||
let index = read_plans.iter().position(|plan| {
|
|
||||||
// METHOD
|
|
||||||
(if plan.match_on.contains(&MatchOn::Method) {
|
|
||||||
plan.request.method() == request.method()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
})
|
|
||||||
// URL
|
|
||||||
&& (if plan.match_on.contains(&MatchOn::Url) {
|
|
||||||
plan.request.url() == request.url()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
})
|
|
||||||
// BODY
|
|
||||||
&& (if plan.match_on.contains(&MatchOn::Body) {
|
|
||||||
match (plan.request.body(), request.body()) {
|
|
||||||
(None, None) => true,
|
|
||||||
(Some(plan), Some(req)) => plan.as_bytes().eq(&req.as_bytes()),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
})
|
|
||||||
// HEADERS
|
|
||||||
&& (if plan.match_on.contains(&MatchOn::Headers) {
|
|
||||||
plan.request.headers() == request.headers()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
drop(read_plans);
|
|
||||||
match index {
|
|
||||||
Some(i) => {
|
|
||||||
let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
|
|
||||||
Ok(write_plans.remove(i).response)
|
|
||||||
}
|
|
||||||
None => Err(Error::UnexpectedMockRequest(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on(&self, request: reqwest::Request) -> OnRequest {
|
|
||||||
OnRequest {
|
|
||||||
net: self,
|
|
||||||
request,
|
|
||||||
match_on: vec![MatchOn::Method, MatchOn::Url],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _on(
|
|
||||||
&self,
|
|
||||||
request: reqwest::Request,
|
|
||||||
response: reqwest::Response,
|
|
||||||
match_on: Vec<MatchOn>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
|
|
||||||
write_plans.push(Plan {
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
match_on,
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&self) -> Result<()> {
|
|
||||||
let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
|
|
||||||
write_plans.clear();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: NetType> Drop for InnerNet<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let Ok(read_plans) = self.plans.read() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
assert!(read_plans.is_empty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OnRequest<'net> {
|
|
||||||
net: &'net InnerNet<Mocked>,
|
|
||||||
request: reqwest::Request,
|
|
||||||
match_on: Vec<MatchOn>,
|
|
||||||
}
|
|
||||||
impl<'net> OnRequest<'net> {
|
|
||||||
pub fn match_on(self, match_on: Vec<MatchOn>) -> Self {
|
|
||||||
Self {
|
|
||||||
net: self.net,
|
|
||||||
request: self.request,
|
|
||||||
match_on,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn respond(self, response: reqwest::Response) -> Result<()> {
|
|
||||||
self.net._on(self.request, response, self.match_on)
|
|
||||||
}
|
|
||||||
}
|
|
795
src/network/mock.rs
Normal file
795
src/network/mock.rs
Normal file
|
@ -0,0 +1,795 @@
|
||||||
|
#![cfg(not(tarpaulin_include))]
|
||||||
|
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crate::network::StatusCode;
|
||||||
|
|
||||||
|
use super::network_env::NetworkTrait;
|
||||||
|
use super::{
|
||||||
|
NetRequest, NetResponse, NetUrl, Network, NetworkError, RequestBody, RequestMethod,
|
||||||
|
ResponseType, SavedRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MockNetwork {
|
||||||
|
requests: Arc<Mutex<Vec<SavedRequest>>>,
|
||||||
|
get_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
get_errors: HashMap<NetUrl, String>,
|
||||||
|
post_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
post_errors: HashMap<NetUrl, String>,
|
||||||
|
put_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
put_errors: HashMap<NetUrl, String>,
|
||||||
|
patch_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
patch_errors: HashMap<NetUrl, String>,
|
||||||
|
delete_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
delete_errors: HashMap<NetUrl, String>,
|
||||||
|
propfind_responses: HashMap<NetUrl, (StatusCode, String)>,
|
||||||
|
propfind_errors: HashMap<NetUrl, String>,
|
||||||
|
}
|
||||||
|
impl MockNetwork {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
requests: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
get_responses: HashMap::new(),
|
||||||
|
get_errors: HashMap::new(),
|
||||||
|
post_responses: HashMap::new(),
|
||||||
|
post_errors: HashMap::new(),
|
||||||
|
put_responses: HashMap::new(),
|
||||||
|
put_errors: HashMap::new(),
|
||||||
|
patch_responses: HashMap::new(),
|
||||||
|
patch_errors: HashMap::new(),
|
||||||
|
delete_responses: HashMap::new(),
|
||||||
|
delete_errors: HashMap::new(),
|
||||||
|
propfind_responses: HashMap::new(),
|
||||||
|
propfind_errors: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn requests(&self) -> Vec<SavedRequest> {
|
||||||
|
unsafe { self.requests.lock().unwrap_unchecked().clone() }
|
||||||
|
}
|
||||||
|
pub fn add_get_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.get_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_get_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.get_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
pub fn add_post_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.post_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_post_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.post_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
pub fn add_put_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.put_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_put_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.put_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
pub fn add_patch_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.patch_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_patch_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.patch_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
pub fn add_delete_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.delete_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_delete_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.delete_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
pub fn add_propfind_response(&mut self, url: &str, status: StatusCode, body: &str) {
|
||||||
|
self.propfind_responses
|
||||||
|
.insert(NetUrl::new(url.to_string()), (status, body.to_string()));
|
||||||
|
}
|
||||||
|
pub fn add_propfind_error(&mut self, url: &str, error: &str) {
|
||||||
|
self.propfind_errors
|
||||||
|
.insert(NetUrl::new(url.to_string()), error.to_string());
|
||||||
|
}
|
||||||
|
fn save_request(&self, method: RequestMethod, url: &str, body: RequestBody) {
|
||||||
|
unsafe {
|
||||||
|
self.requests
|
||||||
|
.lock()
|
||||||
|
.unwrap_unchecked()
|
||||||
|
.push(SavedRequest::new(method, url, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn call<Reply: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
tracing::info!("MockNetworkEnv::call({:?})", net_request);
|
||||||
|
let method = net_request.method();
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
self.save_request(method, url, body.clone());
|
||||||
|
let errors = match method {
|
||||||
|
RequestMethod::Get => &self.get_errors,
|
||||||
|
RequestMethod::Post => &self.post_errors,
|
||||||
|
RequestMethod::Put => &self.put_errors,
|
||||||
|
RequestMethod::Patch => &self.patch_errors,
|
||||||
|
RequestMethod::Propfind => &self.propfind_errors,
|
||||||
|
RequestMethod::Delete => &self.delete_errors,
|
||||||
|
};
|
||||||
|
if let Some(error) = errors.get(url) {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"MockNetworkEnv::{}({}) -> error: {}",
|
||||||
|
method,
|
||||||
|
**url,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
Err(NetworkError::RequestError(
|
||||||
|
method,
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
url.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let responses = match method {
|
||||||
|
RequestMethod::Get => &self.get_responses,
|
||||||
|
RequestMethod::Post => &self.post_responses,
|
||||||
|
RequestMethod::Put => &self.put_responses,
|
||||||
|
RequestMethod::Patch => &self.patch_responses,
|
||||||
|
RequestMethod::Propfind => &self.propfind_responses,
|
||||||
|
RequestMethod::Delete => &self.delete_responses,
|
||||||
|
};
|
||||||
|
let (status, response) = responses.get(url).ok_or_else(|| {
|
||||||
|
tracing::error!(?method, ?url, "unexpected request");
|
||||||
|
NetworkError::MockError(method, StatusCode::NOT_IMPLEMENTED, url.clone())
|
||||||
|
})?;
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"MockNetworkEnv::{}({}) -> error: {}",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
Err(NetworkError::RequestError(method, *status, url.clone()))
|
||||||
|
} else {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"MockNetworkEnv::{}({}) -> response: {}",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
let response_body: Option<Reply> = match response_type {
|
||||||
|
ResponseType::Json => Some(serde_json::from_str(response)?),
|
||||||
|
ResponseType::Xml => Some(serde_xml_rs::from_str(response)?),
|
||||||
|
ResponseType::None => None,
|
||||||
|
ResponseType::Text => {
|
||||||
|
// use call_string() instead
|
||||||
|
Err(NetworkError::UnexpectedMockRequest {
|
||||||
|
method,
|
||||||
|
request_url: url.clone(),
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(NetResponse::new(method, url, *status, response_body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_string(&self, net_request: NetRequest) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
let method = net_request.method();
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
|
||||||
|
match body {
|
||||||
|
RequestBody::String(_) => Ok(()),
|
||||||
|
RequestBody::None => Ok(()),
|
||||||
|
_ => Err(NetworkError::InvalidRequestBody),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
self.save_request(method, url, body.clone());
|
||||||
|
event!(Level::INFO, "MockNetworkEnv::{}({})", method, url);
|
||||||
|
self.check_for_error(method, url).map_or_else(
|
||||||
|
|| self.as_response(method, url, &net_request),
|
||||||
|
|error| as_server_error(method, url, error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_response(
|
||||||
|
&self,
|
||||||
|
method: RequestMethod,
|
||||||
|
url: &NetUrl,
|
||||||
|
net_request: &NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
event!(Level::INFO, "url: {}", url);
|
||||||
|
let (status, response) = self.check_for_response(method, url).ok_or_else(|| {
|
||||||
|
NetworkError::MockError(method, StatusCode::NOT_IMPLEMENTED, url.clone())
|
||||||
|
})?;
|
||||||
|
event!(Level::INFO, "status: {:?}", status);
|
||||||
|
let response_body = match net_request.response_type() {
|
||||||
|
ResponseType::None => None,
|
||||||
|
_ => {
|
||||||
|
let response_body: String = response.to_string();
|
||||||
|
event!(Level::INFO, "response_body: {:?}", response_body);
|
||||||
|
Some(response_body)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(NetResponse::new(method, url, *status, response_body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_for_response(
|
||||||
|
&self,
|
||||||
|
method: RequestMethod,
|
||||||
|
url: &NetUrl,
|
||||||
|
) -> Option<&(StatusCode, String)> {
|
||||||
|
(match method {
|
||||||
|
RequestMethod::Get => &self.get_responses,
|
||||||
|
RequestMethod::Post => &self.post_responses,
|
||||||
|
RequestMethod::Put => &self.put_responses,
|
||||||
|
RequestMethod::Patch => &self.patch_responses,
|
||||||
|
RequestMethod::Propfind => &self.propfind_responses,
|
||||||
|
RequestMethod::Delete => &self.delete_responses,
|
||||||
|
})
|
||||||
|
.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_for_error(&self, method: RequestMethod, url: &NetUrl) -> Option<&String> {
|
||||||
|
(match method {
|
||||||
|
RequestMethod::Get => &self.get_errors,
|
||||||
|
RequestMethod::Post => &self.post_errors,
|
||||||
|
RequestMethod::Put => &self.put_errors,
|
||||||
|
RequestMethod::Patch => &self.patch_errors,
|
||||||
|
RequestMethod::Propfind => &self.propfind_errors,
|
||||||
|
RequestMethod::Delete => &self.delete_errors,
|
||||||
|
})
|
||||||
|
.get(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_server_error(
|
||||||
|
method: RequestMethod,
|
||||||
|
url: &NetUrl,
|
||||||
|
error: &String,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"MockNetworkEnv::{}({}) -> error: {}",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
Err(NetworkError::RequestError(
|
||||||
|
method,
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
url.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
impl Default for MockNetwork {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<MockNetwork> for Network {
|
||||||
|
fn from(mock: MockNetwork) -> Self {
|
||||||
|
Self::Mock(mock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl NetworkTrait for MockNetwork {
|
||||||
|
async fn get<Reply: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Get,
|
||||||
|
"get method must be RequestMethod::Get"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Get,
|
||||||
|
"get_string method must be RequestMethod::Get"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::Text,
|
||||||
|
"get_string response_type must be ResponseType::Text"
|
||||||
|
);
|
||||||
|
self.call_string(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_json<Reply: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Post,
|
||||||
|
"post method must be RequestMethod::Post"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(net_request.body(), RequestBody::Json(_)),
|
||||||
|
"request body must be RequestBody::Json"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Post,
|
||||||
|
"post_string method must be RequestMethod::Post"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::Text,
|
||||||
|
"post_string response_type must be ResponseType::Text"
|
||||||
|
);
|
||||||
|
self.call_string(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_json<Reply: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Put,
|
||||||
|
"put method must be RequestMethod::Put"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(net_request.body(), RequestBody::Json(_)),
|
||||||
|
"request body must be RequestBody::Json"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Put,
|
||||||
|
"put_string method must be RequestMethod::Put"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::Text,
|
||||||
|
"put_string response_type must be ResponseType::Text"
|
||||||
|
);
|
||||||
|
self.call_string(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Patch,
|
||||||
|
"patch method must be RequestMethod::Patch"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(net_request.body(), RequestBody::Json(_)),
|
||||||
|
"request body must be RequestBody::Json"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Delete,
|
||||||
|
"delete method must be RequestMethod::Delete"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::None,
|
||||||
|
"delete response_type must be ResponseType::None"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"propfind method must be RequestMethod::Propfind"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::Xml,
|
||||||
|
"propfind response_type must be ResponseType::Xml"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.body(),
|
||||||
|
&RequestBody::None,
|
||||||
|
"delete body must be RequestBody::None"
|
||||||
|
);
|
||||||
|
self.call(net_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn propfind_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
assert_eq!(
|
||||||
|
net_request.method(),
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"propfind_string method must be RequestMethod::Propfind"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.response_type(),
|
||||||
|
ResponseType::Text,
|
||||||
|
"propfind_string response_type must be ResponseType::Text"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net_request.body(),
|
||||||
|
&RequestBody::None,
|
||||||
|
"delete body must be RequestBody::None"
|
||||||
|
);
|
||||||
|
self.call_string(net_request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::network::{NetResponse, NetworkError, RequestMethod};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio_test::block_on;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_get() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_get_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
|
||||||
|
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into())).build();
|
||||||
|
let response: NetResponse<HashMap<String, String>> = block_on(net.get(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response
|
||||||
|
.response_body()
|
||||||
|
.and_then(|body| body.get("foo").cloned()),
|
||||||
|
Some("bar".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_get_error() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_get_error("https://httpbin.org", "error");
|
||||||
|
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into())).build();
|
||||||
|
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
|
||||||
|
block_on(net.get(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_get_string() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_get_response("https://httpbin.org", StatusCode::OK, r#"{"foo":"bar"}"#);
|
||||||
|
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<String> = block_on(net.get_string(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response.response_body(),
|
||||||
|
Some(json!({"foo":"bar"}).to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_get_string_error() {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_get_error("https://httpbin.org", "error");
|
||||||
|
let net_request = NetRequest::get(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<String>, NetworkError> =
|
||||||
|
block_on(net.get_string(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_post() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_post_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
|
||||||
|
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.json_body(json!({}))?
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<HashMap<String, String>> = block_on(net.post_json(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response
|
||||||
|
.response_body()
|
||||||
|
.and_then(|body| body.get("foo").cloned()),
|
||||||
|
Some("bar".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Post,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::Json(json!({}))
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_post_error() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_post_error("https://httpbin.org", "error");
|
||||||
|
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.json_body(json!({}))?
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
|
||||||
|
block_on(net.post_json(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Post,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::Json(json!({}))
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_put_json() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_put_response("https://httpbin.org", StatusCode::OK, r#"{"foo": "bar"}"#);
|
||||||
|
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.json_body(json!({}))?
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<HashMap<String, String>> = block_on(net.put_json(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response
|
||||||
|
.response_body()
|
||||||
|
.and_then(|body| body.get("foo").cloned()),
|
||||||
|
Some("bar".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Put,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::Json(json!({}))
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_put_json_error() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_put_error("https://httpbin.org", "error");
|
||||||
|
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.json_body(json!({}))?
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<HashMap<String, String>>, NetworkError> =
|
||||||
|
block_on(net.put_json(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Put,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::Json(json!({}))
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_put_string() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_put_response("https://httpbin.org", StatusCode::OK, r#"{"foo":"bar"}"#);
|
||||||
|
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.string_body("PLAIN-TEXT".to_string())
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<String> = block_on(net.put_string(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response.response_body(),
|
||||||
|
Some(json!({"foo":"bar"}).to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Put,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("PLAIN-TEXT".to_string()),
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_put_string_error() {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_put_error("https://httpbin.org", "error");
|
||||||
|
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org".into()))
|
||||||
|
.string_body("PLAIN-TEXT".to_string())
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<String>, NetworkError> =
|
||||||
|
block_on(net.put_string(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Put,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("PLAIN-TEXT".to_string())
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
struct PropfindTestResponse {
|
||||||
|
pub foo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_propfind() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_propfind_response(
|
||||||
|
"https://caldav.org",
|
||||||
|
StatusCode::OK,
|
||||||
|
r#"<container><foo>bar</foo></container>"#,
|
||||||
|
);
|
||||||
|
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
|
||||||
|
.response_type(ResponseType::Xml)
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<PropfindTestResponse> = block_on(net.propfind(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response.response_body().map(|body| body.foo),
|
||||||
|
Some("bar".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"https://caldav.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_propfind_string() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_propfind_response(
|
||||||
|
"https://caldav.org",
|
||||||
|
StatusCode::OK,
|
||||||
|
r#"<container><foo>bar</foo></container>"#,
|
||||||
|
);
|
||||||
|
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<String> = block_on(net.propfind_string(net_request))?;
|
||||||
|
assert_eq!(
|
||||||
|
response.response_body(),
|
||||||
|
Some("<container><foo>bar</foo></container>".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"https://caldav.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_propfind_error() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_propfind_error("https://caldav.org", "error");
|
||||||
|
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
|
||||||
|
.response_type(ResponseType::Xml)
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<PropfindTestResponse>, NetworkError> =
|
||||||
|
block_on(net.propfind(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"https://caldav.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_mock_network_env_propfind_string_error() -> Result<(), NetworkError> {
|
||||||
|
let mut net = MockNetwork::new();
|
||||||
|
net.add_propfind_error("https://caldav.org", "error");
|
||||||
|
let net_request = NetRequest::propfind(NetUrl::new("https://caldav.org".into()))
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
.build();
|
||||||
|
let result: Result<NetResponse<String>, NetworkError> =
|
||||||
|
block_on(net.propfind_string(net_request));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
net.requests(),
|
||||||
|
vec![SavedRequest::new(
|
||||||
|
RequestMethod::Propfind,
|
||||||
|
"https://caldav.org",
|
||||||
|
RequestBody::None
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
36
src/network/mod.rs
Normal file
36
src/network/mod.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
mod mock;
|
||||||
|
mod net_auth;
|
||||||
|
mod net_request;
|
||||||
|
mod net_request_headers;
|
||||||
|
mod net_response;
|
||||||
|
mod network_env;
|
||||||
|
mod network_error;
|
||||||
|
mod real;
|
||||||
|
mod request_body;
|
||||||
|
mod request_method;
|
||||||
|
mod response_type;
|
||||||
|
mod saved_request;
|
||||||
|
|
||||||
|
pub use mock::MockNetwork;
|
||||||
|
pub use net_auth::NetAuth;
|
||||||
|
pub use net_auth::NetAuthPassword;
|
||||||
|
pub use net_auth::NetAuthUsername;
|
||||||
|
pub use net_request::NetRequest;
|
||||||
|
pub use net_request::NetRequestLogging;
|
||||||
|
pub use net_request::NetUrl;
|
||||||
|
pub use net_request_headers::NetRequestHeaders;
|
||||||
|
pub use net_response::NetResponse;
|
||||||
|
pub use network_env::Network;
|
||||||
|
pub use network_error::NetworkError;
|
||||||
|
pub use real::RealNetwork;
|
||||||
|
pub use request_body::RequestBody;
|
||||||
|
pub use request_method::RequestMethod;
|
||||||
|
pub use response_type::ResponseType;
|
||||||
|
pub use saved_request::SavedRequest;
|
||||||
|
|
||||||
|
pub use reqwest::header::HeaderMap;
|
||||||
|
pub use reqwest::Error as ReqwestError;
|
||||||
|
pub use reqwest::StatusCode;
|
||||||
|
pub use serde_json::json;
|
||||||
|
|
||||||
|
pub type NetworkResult<T> = Result<T, NetworkError>;
|
66
src/network/net_auth.rs
Normal file
66
src/network/net_auth.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Password(secrecy::SecretString);
|
||||||
|
impl Password {
|
||||||
|
pub fn new(password: String) -> Self {
|
||||||
|
Self(secrecy::SecretString::new(password))
|
||||||
|
}
|
||||||
|
pub fn expose_password(&self) -> &str {
|
||||||
|
secrecy::ExposeSecret::expose_secret(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NetAuthUsername(String);
|
||||||
|
impl NetAuthUsername {
|
||||||
|
pub const fn new(username: String) -> Self {
|
||||||
|
Self(username)
|
||||||
|
}
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for NetAuthUsername {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Password for HTTP authentication");
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NetAuthPassword(Password);
|
||||||
|
impl NetAuthPassword {
|
||||||
|
pub fn new(password: String) -> Self {
|
||||||
|
Self(Password::new(password))
|
||||||
|
}
|
||||||
|
pub fn expose_password(&self) -> &str {
|
||||||
|
self.0.expose_password()
|
||||||
|
}
|
||||||
|
// pub const fn as_str(&self) -> &str {
|
||||||
|
// "********"
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
impl From<Password> for NetAuthPassword {
|
||||||
|
fn from(password: Password) -> Self {
|
||||||
|
Self(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// new_type_display!(NetAuthPassword);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NetAuth {
|
||||||
|
username: NetAuthUsername,
|
||||||
|
password: NetAuthPassword,
|
||||||
|
}
|
||||||
|
impl NetAuth {
|
||||||
|
pub const fn new(username: NetAuthUsername, password: NetAuthPassword) -> Self {
|
||||||
|
Self { username, password }
|
||||||
|
}
|
||||||
|
pub const fn username(&self) -> &NetAuthUsername {
|
||||||
|
&self.username
|
||||||
|
}
|
||||||
|
pub const fn password(&self) -> &NetAuthPassword {
|
||||||
|
&self.password
|
||||||
|
}
|
||||||
|
}
|
462
src/network/net_request.rs
Normal file
462
src/network/net_request.rs
Normal file
|
@ -0,0 +1,462 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use crate::network::HeaderMap;
|
||||||
|
use crate::network::NetAuth;
|
||||||
|
use crate::network::RequestBody;
|
||||||
|
use crate::network::RequestMethod;
|
||||||
|
use crate::network::ResponseType;
|
||||||
|
|
||||||
|
use super::NetRequestHeaders;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct NetUrl(String);
|
||||||
|
impl NetUrl {
|
||||||
|
pub const fn new(url: String) -> Self {
|
||||||
|
Self(url)
|
||||||
|
}
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for NetUrl {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for NetUrl {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NetRequestLogging {
|
||||||
|
None,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
impl Default for NetRequestLogging {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NetRequest {
|
||||||
|
method: RequestMethod,
|
||||||
|
url: NetUrl,
|
||||||
|
body: RequestBody,
|
||||||
|
response_type: ResponseType,
|
||||||
|
auth: Option<NetAuth>,
|
||||||
|
headers: NetRequestHeaders,
|
||||||
|
log: NetRequestLogging,
|
||||||
|
}
|
||||||
|
impl NetRequest {
|
||||||
|
pub fn get(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.url(net_url)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
}
|
||||||
|
pub fn post(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.method(RequestMethod::Post)
|
||||||
|
.url(net_url)
|
||||||
|
}
|
||||||
|
pub fn put(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.method(RequestMethod::Put)
|
||||||
|
.url(net_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.method(RequestMethod::Patch)
|
||||||
|
.url(net_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.method(RequestMethod::Delete)
|
||||||
|
.response_type(ResponseType::None)
|
||||||
|
.url(net_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn propfind(net_url: NetUrl) -> NetRequestBuilder {
|
||||||
|
NetRequestBuilder::default()
|
||||||
|
.method(RequestMethod::Propfind)
|
||||||
|
.response_type(ResponseType::None)
|
||||||
|
.url(net_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new(
|
||||||
|
method: RequestMethod,
|
||||||
|
url: NetUrl,
|
||||||
|
headers: NetRequestHeaders,
|
||||||
|
body: RequestBody,
|
||||||
|
response_type: ResponseType,
|
||||||
|
auth: Option<NetAuth>,
|
||||||
|
log: NetRequestLogging,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
response_type,
|
||||||
|
auth,
|
||||||
|
log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn as_trace(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
self.method,
|
||||||
|
self.url.as_str().chars().take(90).collect::<String>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub const fn method(&self) -> RequestMethod {
|
||||||
|
self.method
|
||||||
|
}
|
||||||
|
pub const fn url(&self) -> &NetUrl {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
pub const fn body(&self) -> &RequestBody {
|
||||||
|
&self.body
|
||||||
|
}
|
||||||
|
pub const fn response_type(&self) -> ResponseType {
|
||||||
|
self.response_type
|
||||||
|
}
|
||||||
|
pub const fn log(&self) -> NetRequestLogging {
|
||||||
|
self.log
|
||||||
|
}
|
||||||
|
pub fn auth(&self) -> Option<NetAuth> {
|
||||||
|
self.auth.clone()
|
||||||
|
}
|
||||||
|
pub fn headers(&self) -> HeaderMap {
|
||||||
|
self.headers
|
||||||
|
.clone()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|_| HeaderMap::new())
|
||||||
|
}
|
||||||
|
pub fn as_curl(&self) -> Result<String, serde_json::Error> {
|
||||||
|
let mut curl = format!("curl -X {} {}", self.method, *self.url);
|
||||||
|
if let Some(accept) = &self.response_type.accept() {
|
||||||
|
if self.headers.get("Accept").is_none() {
|
||||||
|
curl.push_str(&format!(" -H 'Accept: {}'", accept));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut headers = vec![];
|
||||||
|
for (key, value) in self.headers.iter() {
|
||||||
|
headers.push(format!(" -H '{}: {}'", key, value));
|
||||||
|
}
|
||||||
|
headers.sort();
|
||||||
|
for header in headers {
|
||||||
|
curl.push_str(&header);
|
||||||
|
}
|
||||||
|
if let Some(auth) = &self.auth() {
|
||||||
|
curl.push_str(&format!(
|
||||||
|
" -u {}:{}",
|
||||||
|
auth.username(),
|
||||||
|
auth.password().expose_password()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.method == RequestMethod::Post || self.method == RequestMethod::Put {
|
||||||
|
let body = String::try_from(&self.body)?;
|
||||||
|
curl.push_str(&format!(" -d '{}'", body));
|
||||||
|
}
|
||||||
|
Ok(curl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NetRequestBuilder {
|
||||||
|
method: RequestMethod,
|
||||||
|
url: NetUrl,
|
||||||
|
headers: NetRequestHeaders,
|
||||||
|
body: RequestBody,
|
||||||
|
response_type: ResponseType,
|
||||||
|
auth: Option<NetAuth>,
|
||||||
|
log: NetRequestLogging,
|
||||||
|
}
|
||||||
|
impl NetRequestBuilder {
|
||||||
|
pub fn build(self) -> NetRequest {
|
||||||
|
NetRequest::new(
|
||||||
|
self.method,
|
||||||
|
self.url,
|
||||||
|
self.headers,
|
||||||
|
self.body,
|
||||||
|
self.response_type,
|
||||||
|
self.auth,
|
||||||
|
self.log,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn method(mut self, method: RequestMethod) -> Self {
|
||||||
|
assert_ne!(method, RequestMethod::default());
|
||||||
|
self.method = method;
|
||||||
|
if self.method == RequestMethod::Get || self.method == RequestMethod::Delete {
|
||||||
|
self.body = RequestBody::None;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(mut self, url: NetUrl) -> Self {
|
||||||
|
self.url = url;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(mut self, key: &str, value: &str) -> Self {
|
||||||
|
self.headers = self.headers.with(key, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn headers(mut self, new_headers: NetRequestHeaders) -> Self {
|
||||||
|
assert_ne!(new_headers, NetRequestHeaders::default());
|
||||||
|
for (key, value) in new_headers.iter() {
|
||||||
|
self.headers = self.headers.with(key, value);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body(mut self, body: RequestBody) -> Self {
|
||||||
|
match body {
|
||||||
|
RequestBody::Json(_) => {
|
||||||
|
self.headers = self.headers.with("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
RequestBody::Xml(_) => {
|
||||||
|
self.headers = self.headers.with("Content-Type", "application/xml")
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
self.body = body;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_body(mut self, body: String) -> Self {
|
||||||
|
self.body = RequestBody::String(body);
|
||||||
|
self.header("Content-Type", "text/plain")
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_body<T: serde::Serialize>(mut self, body: T) -> Result<Self, serde_json::Error> {
|
||||||
|
self.body = RequestBody::json(body)?;
|
||||||
|
Ok(self.header("Content-Type", "application/json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xml_body(mut self, body: &str) -> Self {
|
||||||
|
self.body = RequestBody::Xml(body.to_string());
|
||||||
|
self.header("Content-Type", "application/xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response_type(mut self, response_type: ResponseType) -> Self {
|
||||||
|
assert_ne!(response_type, ResponseType::default());
|
||||||
|
self.response_type = response_type;
|
||||||
|
match response_type.accept() {
|
||||||
|
Some(accept) => self.header("Accept", accept.as_str()),
|
||||||
|
None => {
|
||||||
|
self.headers.remove("Accept");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth(mut self, auth: NetAuth) -> Self {
|
||||||
|
self.auth = Some(auth);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn log(mut self, log: NetRequestLogging) -> Self {
|
||||||
|
self.log = log;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert2::let_assert;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::network::net_auth::{NetAuthPassword, NetAuthUsername};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X GET https://httpbin.org/get -H 'Accept: application/json'".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_auth() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.auth(NetAuth::new(
|
||||||
|
NetAuthUsername::new("user".into()),
|
||||||
|
NetAuthPassword::new("pass".into()),
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X GET https://httpbin.org/get -H 'Accept: application/json' -u user:pass"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth_post_json() -> Result<(), serde_json::Error> {
|
||||||
|
let request = NetRequest::post(NetUrl("https://httpbin.org/post".to_string()))
|
||||||
|
.json_body(json!({
|
||||||
|
"args": {},
|
||||||
|
"url": "https://httpbin.org/post",
|
||||||
|
}))?
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X POST https://httpbin.org/post -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{\"args\":{},\"url\":\"https://httpbin.org/post\"}'".to_string()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth_post_text() {
|
||||||
|
let request = NetRequest::post(NetUrl("https://httpbin.org/post".to_string()))
|
||||||
|
.string_body("body".to_string())
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X POST https://httpbin.org/post -H 'Accept: text/plain' -H 'Content-Type: text/plain' -d 'body'".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth_put() -> Result<(), serde_json::Error> {
|
||||||
|
let request = NetRequest::put(NetUrl("https://httpbin.org/put".to_string()))
|
||||||
|
.json_body(json!({
|
||||||
|
"args": {},
|
||||||
|
"url": "https://httpbin.org/put",
|
||||||
|
}))?
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X PUT https://httpbin.org/put -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{\"args\":{},\"url\":\"https://httpbin.org/put\"}'".to_string()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth_delete() {
|
||||||
|
let request = NetRequest::delete(NetUrl("https://httpbin.org/delete".to_string())).build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X DELETE https://httpbin.org/delete".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_no_auth_put_xml() {
|
||||||
|
let request = NetRequest::put(NetUrl("https://httpbin.org/put".to_string()))
|
||||||
|
.xml_body("<xml/>")
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X PUT https://httpbin.org/put -H 'Accept: application/json' -H 'Content-Type: application/xml' -d '<xml/>'"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_accept_json() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X GET https://httpbin.org/get -H 'Accept: application/json'".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_accept_xml() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.response_type(ResponseType::Xml)
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X GET https://httpbin.org/get -H 'Accept: application/xml'".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_accept_text() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.response_type(ResponseType::Text)
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X GET https://httpbin.org/get -H 'Accept: text/plain'".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn text_as_curl_accept_none() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.response_type(ResponseType::None)
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(curl, "curl -X GET https://httpbin.org/get".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_with_headers() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.header("header1", "value1")
|
||||||
|
.header("header2", "value2")
|
||||||
|
.build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert!(curl.contains("-H 'header1: value1'"));
|
||||||
|
assert!(curl.contains("-H 'header2: value2'"));
|
||||||
|
assert!(curl.starts_with("curl -X GET https://httpbin.org/get"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_net_request_headers() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string()))
|
||||||
|
.header("header1", "value1")
|
||||||
|
.header("header2", "value2")
|
||||||
|
.build();
|
||||||
|
let headers = request.headers();
|
||||||
|
assert!(headers.len() >= 2);
|
||||||
|
let_assert!(Some(value1) = headers.get("header1"));
|
||||||
|
assert_eq!(value1, "value1");
|
||||||
|
let_assert!(Some(value2) = headers.get("header2"));
|
||||||
|
assert_eq!(value2, "value2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_curl_propfind() {
|
||||||
|
let request =
|
||||||
|
NetRequest::propfind(NetUrl("https://httpbin.org/propfind".to_string())).build();
|
||||||
|
let_assert!(Ok(curl) = request.as_curl());
|
||||||
|
assert_eq!(
|
||||||
|
curl,
|
||||||
|
"curl -X PROPFIND https://httpbin.org/propfind".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_as_trace() {
|
||||||
|
let request = NetRequest::get(NetUrl("https://httpbin.org/get".to_string())).build();
|
||||||
|
assert_eq!(request.as_trace(), "GET https://httpbin.org/get");
|
||||||
|
}
|
||||||
|
}
|
102
src/network/net_request_headers.rs
Normal file
102
src/network/net_request_headers.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::network::HeaderMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct NetRequestHeaders(HashMap<String, String>);
|
||||||
|
impl NetRequestHeaders {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default() //(HashMap::new())
|
||||||
|
}
|
||||||
|
pub fn with(mut self, key: &str, value: &str) -> Self {
|
||||||
|
self.0.insert(key.into(), value.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<T> for NetRequestHeaders
|
||||||
|
where
|
||||||
|
T: Into<HashMap<String, String>>,
|
||||||
|
{
|
||||||
|
fn from(x: T) -> Self {
|
||||||
|
Self(x.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<NetRequestHeaders> for HeaderMap {
|
||||||
|
type Error = http::Error;
|
||||||
|
fn try_from(x: NetRequestHeaders) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(&x.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for NetRequestHeaders {
|
||||||
|
type Target = HashMap<String, String>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for NetRequestHeaders {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use assert2::let_assert;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_net_request_headers_from_hash_map() {
|
||||||
|
let nrh = NetRequestHeaders::from(
|
||||||
|
[("a", "b"), ("c", "d")]
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
);
|
||||||
|
assert_eq!(nrh.len(), 2);
|
||||||
|
let_assert!(Some(a) = nrh.get("a"));
|
||||||
|
assert_eq!(a, "b");
|
||||||
|
let_assert!(Some(c) = nrh.get("c"));
|
||||||
|
assert_eq!(c, "d");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_net_request_headers_with_new_entry() {
|
||||||
|
let nrh = NetRequestHeaders::from(
|
||||||
|
[("a", "b"), ("c", "d")]
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)
|
||||||
|
.with("e", "f");
|
||||||
|
assert_eq!(nrh.len(), 3);
|
||||||
|
let_assert!(Some(a) = nrh.get("a"));
|
||||||
|
assert_eq!(a, "b");
|
||||||
|
let_assert!(Some(c) = nrh.get("c"));
|
||||||
|
assert_eq!(c, "d");
|
||||||
|
let_assert!(Some(e) = nrh.get("e"));
|
||||||
|
assert_eq!(e, "f");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_net_request_headers_try_into_header_map() {
|
||||||
|
let result: Result<HeaderMap, http::Error> = NetRequestHeaders::from(
|
||||||
|
[("a", "b"), ("c", "d")]
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)
|
||||||
|
.try_into();
|
||||||
|
let_assert!(Ok(hm) = result);
|
||||||
|
assert_eq!(hm.len(), 2);
|
||||||
|
let_assert!(Some(a) = hm.get("a"));
|
||||||
|
assert_eq!(a, "b");
|
||||||
|
let_assert!(Some(c) = hm.get("c"));
|
||||||
|
assert_eq!(c, "d");
|
||||||
|
}
|
||||||
|
}
|
38
src/network/net_response.rs
Normal file
38
src/network/net_response.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::network::StatusCode;
|
||||||
|
|
||||||
|
use super::{net_request::NetUrl, RequestMethod};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NetResponse<T> {
|
||||||
|
method: RequestMethod,
|
||||||
|
request_url: NetUrl,
|
||||||
|
status_code: StatusCode,
|
||||||
|
response_body: Option<T>,
|
||||||
|
}
|
||||||
|
impl<T> NetResponse<T> {
|
||||||
|
pub fn new(
|
||||||
|
method: RequestMethod,
|
||||||
|
request_url: &NetUrl,
|
||||||
|
status_code: StatusCode,
|
||||||
|
response_body: Option<T>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
method,
|
||||||
|
request_url: request_url.clone(),
|
||||||
|
status_code,
|
||||||
|
response_body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn method(&self) -> &RequestMethod {
|
||||||
|
&self.method
|
||||||
|
}
|
||||||
|
pub const fn request_url(&self) -> &NetUrl {
|
||||||
|
&self.request_url
|
||||||
|
}
|
||||||
|
pub const fn status_code(&self) -> StatusCode {
|
||||||
|
self.status_code
|
||||||
|
}
|
||||||
|
pub fn response_body(self) -> Option<T> {
|
||||||
|
self.response_body
|
||||||
|
}
|
||||||
|
}
|
174
src/network/network_env.rs
Normal file
174
src/network/network_env.rs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::network::{MockNetwork, NetRequest, NetworkError, RealNetwork, SavedRequest};
|
||||||
|
|
||||||
|
use super::NetResponse;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Network {
|
||||||
|
Mock(MockNetwork),
|
||||||
|
Real(RealNetwork),
|
||||||
|
}
|
||||||
|
impl Network {
|
||||||
|
pub fn new_mock() -> Self {
|
||||||
|
Self::Mock(MockNetwork::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_real() -> Self {
|
||||||
|
Self::Real(RealNetwork::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get<T: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<T>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.get(net_request).await,
|
||||||
|
Self::Real(real) => real.get(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.get_string(net_request).await,
|
||||||
|
Self::Real(real) => real.get_string(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.post_json(net_request).await,
|
||||||
|
Self::Real(real) => real.post_json(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.post_string(net_request).await,
|
||||||
|
Self::Real(real) => real.post_string(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.put_json(net_request).await,
|
||||||
|
Self::Real(real) => real.put_json(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.put_string(net_request).await,
|
||||||
|
Self::Real(real) => real.put_string(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.patch_json(net_request).await,
|
||||||
|
Self::Real(real) => real.patch_json(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.delete(net_request).await,
|
||||||
|
Self::Real(real) => real.delete(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.propfind(net_request).await,
|
||||||
|
Self::Real(real) => real.propfind(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn propfind_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => mock.propfind_string(net_request).await,
|
||||||
|
Self::Real(real) => real.propfind_string(net_request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mocked_requests(&self) -> Option<Vec<SavedRequest>> {
|
||||||
|
match self {
|
||||||
|
Self::Mock(mock) => Some(mock.requests()),
|
||||||
|
Self::Real(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[async_trait]
|
||||||
|
pub(super) trait NetworkTrait: Sync + Send + Clone + std::fmt::Debug {
|
||||||
|
async fn get<T: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<T>, NetworkError>;
|
||||||
|
|
||||||
|
async fn get_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError>;
|
||||||
|
|
||||||
|
async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError>;
|
||||||
|
|
||||||
|
async fn post_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError>;
|
||||||
|
|
||||||
|
async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError>;
|
||||||
|
|
||||||
|
async fn put_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError>;
|
||||||
|
|
||||||
|
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError>;
|
||||||
|
|
||||||
|
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError>;
|
||||||
|
|
||||||
|
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError>;
|
||||||
|
|
||||||
|
async fn propfind_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError>;
|
||||||
|
}
|
56
src/network/network_error.rs
Normal file
56
src/network/network_error.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum NetworkError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Reqwest(#[from] crate::network::ReqwestError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SerdeJson(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SerdeXml(#[from] serde_xml_rs::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
HttpMethod(#[from] http::method::InvalidMethod),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Http(#[from] http::Error),
|
||||||
|
|
||||||
|
#[error("{0} failed: {1} ({2})")]
|
||||||
|
RequestFailed(super::RequestMethod, super::StatusCode, super::NetUrl),
|
||||||
|
|
||||||
|
#[error("{0} failed: {1} ({2})")]
|
||||||
|
MockError(super::RequestMethod, super::StatusCode, super::NetUrl),
|
||||||
|
|
||||||
|
#[error("missing response body")]
|
||||||
|
MissingResponseBody,
|
||||||
|
|
||||||
|
#[error("unexpected mock request: {method:?} {request_url}")]
|
||||||
|
UnexpectedMockRequest {
|
||||||
|
method: super::RequestMethod,
|
||||||
|
request_url: super::NetUrl,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("{0} failed: {1} ({2})")]
|
||||||
|
RequestError(super::RequestMethod, super::StatusCode, super::NetUrl),
|
||||||
|
|
||||||
|
#[error("invalid response type")]
|
||||||
|
InvalidResponseType,
|
||||||
|
|
||||||
|
#[error("invalid request body")]
|
||||||
|
InvalidRequestBody,
|
||||||
|
|
||||||
|
#[error("response body is empty")]
|
||||||
|
EmptyResponseBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::NetworkError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
const fn test_network_error_is_send() {
|
||||||
|
const fn assert_send<T: Send>() {}
|
||||||
|
assert_send::<NetworkError>();
|
||||||
|
assert_send::<Result<(), NetworkError>>();
|
||||||
|
}
|
||||||
|
}
|
612
src/network/real.rs
Normal file
612
src/network/real.rs
Normal file
|
@ -0,0 +1,612 @@
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
network_env::NetworkTrait, NetAuth, NetRequest, NetRequestLogging, NetResponse, Network,
|
||||||
|
NetworkError, RequestMethod, ResponseType, StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
trait WithAuthentiction {
|
||||||
|
fn auth(self, auth: Option<NetAuth>) -> reqwest::RequestBuilder;
|
||||||
|
}
|
||||||
|
impl WithAuthentiction for reqwest::RequestBuilder {
|
||||||
|
fn auth(self, auth: Option<NetAuth>) -> reqwest::RequestBuilder {
|
||||||
|
if let Some(auth) = auth {
|
||||||
|
self.basic_auth(
|
||||||
|
auth.username().to_string(),
|
||||||
|
Some(auth.password().expose_password()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait WithResponseType {
|
||||||
|
fn response_type(self, response_type: ResponseType) -> Self;
|
||||||
|
}
|
||||||
|
impl WithResponseType for reqwest::RequestBuilder {
|
||||||
|
fn response_type(self, response_type: ResponseType) -> Self {
|
||||||
|
match response_type {
|
||||||
|
ResponseType::Json => self.header(reqwest::header::ACCEPT, "application/json"),
|
||||||
|
ResponseType::Xml => self.header(reqwest::header::ACCEPT, "application/xml"),
|
||||||
|
ResponseType::Text => self.header(reqwest::header::ACCEPT, "text/plain"),
|
||||||
|
ResponseType::None => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RealNetwork {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
impl RealNetwork {
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send<T: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
request: RequestBuilder,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<T>, NetworkError> {
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status_code = response.status();
|
||||||
|
let text = response.text().await?;
|
||||||
|
if status_code.is_success() {
|
||||||
|
tracing::trace!(status = ?status_code);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Request failed");
|
||||||
|
self.log_request(&net_request);
|
||||||
|
self.log_response(&net_request, &status_code, text.as_str());
|
||||||
|
return Err(NetworkError::RequestFailed(
|
||||||
|
net_request.method(),
|
||||||
|
status_code,
|
||||||
|
net_request.url().clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match net_request.response_type() {
|
||||||
|
ResponseType::Json => serde_json::from_str(text.as_str())
|
||||||
|
.map(Some)
|
||||||
|
.map_err(NetworkError::from),
|
||||||
|
ResponseType::Xml => serde_xml_rs::from_str(text.as_str())
|
||||||
|
.map(Some)
|
||||||
|
.map_err(NetworkError::from),
|
||||||
|
ResponseType::Text => {
|
||||||
|
panic!("text response type not implemented - use send_for_text(...) instead")
|
||||||
|
}
|
||||||
|
ResponseType::None => Ok(None),
|
||||||
|
}
|
||||||
|
.map(|response_body| {
|
||||||
|
match net_request.log() {
|
||||||
|
NetRequestLogging::None => (),
|
||||||
|
NetRequestLogging::Request => self.log_request(&net_request),
|
||||||
|
NetRequestLogging::Response => {
|
||||||
|
self.log_response(&net_request, &status_code, text.as_str());
|
||||||
|
}
|
||||||
|
NetRequestLogging::Both => {
|
||||||
|
self.log_request(&net_request);
|
||||||
|
self.log_response(&net_request, &status_code, text.as_str());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
NetResponse::new(
|
||||||
|
net_request.method(),
|
||||||
|
net_request.url(),
|
||||||
|
status_code,
|
||||||
|
response_body,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_for_text(
|
||||||
|
&self,
|
||||||
|
request: RequestBuilder,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status_code = response.status();
|
||||||
|
if status_code.is_success() {
|
||||||
|
tracing::trace!(status = ?status_code);
|
||||||
|
} else {
|
||||||
|
tracing::error!(status = ?status_code, request = ?net_request);
|
||||||
|
}
|
||||||
|
match net_request.response_type() {
|
||||||
|
ResponseType::Text => {
|
||||||
|
let text = response.text().await?;
|
||||||
|
Ok(Some(text))
|
||||||
|
}
|
||||||
|
_ => panic!("text response type not implemented - use send(...) instead"),
|
||||||
|
}
|
||||||
|
.map(|response_body| {
|
||||||
|
NetResponse::new(
|
||||||
|
net_request.method(),
|
||||||
|
net_request.url(),
|
||||||
|
status_code,
|
||||||
|
response_body,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_request(&self, net_request: &NetRequest) {
|
||||||
|
tracing::info!(?net_request, "RealNetworkEnv::request");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_response(
|
||||||
|
&self,
|
||||||
|
net_request: &NetRequest,
|
||||||
|
status_code: &StatusCode,
|
||||||
|
response_body: &str,
|
||||||
|
) {
|
||||||
|
tracing::info!(
|
||||||
|
?net_request,
|
||||||
|
status = ?status_code,
|
||||||
|
?response_body,
|
||||||
|
"RealNetworkEnv::response"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RealNetwork {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<RealNetwork> for Network {
|
||||||
|
fn from(real: RealNetwork) -> Self {
|
||||||
|
Self::Real(real)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl NetworkTrait for RealNetwork {
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn get<T: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<T>, NetworkError> {
|
||||||
|
tracing::debug!("RealNetworkEnv::get({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.get(url.to_string())
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
self.send(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn post_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::post_json({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let body = String::try_from(body)?;
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.post(url.to_string())
|
||||||
|
.body(body)
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
self.send(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn post_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::post_string({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let body = String::try_from(body)?;
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.post(url.to_string())
|
||||||
|
.body(body)
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send_for_text(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn put_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::put_json({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let body = String::try_from(body)?;
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.put(url.to_string())
|
||||||
|
.body(body)
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn put_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::put_string({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let body = String::try_from(body)?;
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.put(url.to_string())
|
||||||
|
.body(body)
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send_for_text(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn patch_json<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::put_json({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let body = net_request.body();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let body = String::try_from(body)?;
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.patch(url.to_string())
|
||||||
|
.body(body)
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn delete(&self, net_request: NetRequest) -> Result<NetResponse<()>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::delete({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.delete(url.to_string())
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn propfind<Reply: DeserializeOwned + std::fmt::Debug>(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<Reply>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::propfind({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.request(reqwest::Method::from_bytes(b"PROPFIND")?, url.to_string())
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
|
||||||
|
let status_code = response.status();
|
||||||
|
event!(Level::TRACE, status = %status_code);
|
||||||
|
let body = match response_type {
|
||||||
|
ResponseType::Xml => serde_xml_rs::from_str(response.text().await?.as_str()),
|
||||||
|
_ => Err(NetworkError::InvalidResponseType)?,
|
||||||
|
};
|
||||||
|
body.map_err(Into::into).map(|response_body| {
|
||||||
|
NetResponse::new(RequestMethod::Propfind, url, status_code, response_body)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn get_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::get_string({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.get(url.to_string())
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send_for_text(request, net_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(request = %net_request.as_trace()), level = "trace")]
|
||||||
|
async fn propfind_string(
|
||||||
|
&self,
|
||||||
|
net_request: NetRequest,
|
||||||
|
) -> Result<NetResponse<String>, NetworkError> {
|
||||||
|
// event!(Level::INFO, "RealNetworkEnv::propfind_string({:?})", net_request);
|
||||||
|
let url = net_request.url();
|
||||||
|
let auth = net_request.auth();
|
||||||
|
let response_type = net_request.response_type();
|
||||||
|
let headers = net_request.headers();
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.request(reqwest::Method::from_bytes(b"PROPFIND")?, url.to_string())
|
||||||
|
.auth(auth)
|
||||||
|
.response_type(response_type)
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
self.send_for_text(request, net_request).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::network::{
|
||||||
|
net_auth::{NetAuthPassword, NetAuthUsername},
|
||||||
|
NetUrl, NetworkError, StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
use assert2::let_assert;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio_test::block_on;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
pub struct GetResponse {
|
||||||
|
pub args: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
pub struct PostResponse {
|
||||||
|
pub args: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
pub data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
pub struct PutResponse {
|
||||||
|
pub args: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
pub data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_authentication_none() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client.get("https://httpbin.org/get").auth(None);
|
||||||
|
let_assert!(Ok(build) = request.build());
|
||||||
|
assert!(build.headers().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_authentication_some() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get("https://httpbin.org/get")
|
||||||
|
.auth(Some(NetAuth::new(
|
||||||
|
NetAuthUsername::new("user".into()),
|
||||||
|
NetAuthPassword::new("pass".into()),
|
||||||
|
)));
|
||||||
|
let_assert!(Ok(build) = request.build());
|
||||||
|
let headers = build.headers();
|
||||||
|
let_assert!(Some(authorization) = headers.get(reqwest::header::AUTHORIZATION));
|
||||||
|
assert_eq!(authorization, "Basic dXNlcjpwYXNz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_response_type_json() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get("https://httpbin.org/get")
|
||||||
|
.response_type(ResponseType::Json);
|
||||||
|
let_assert!(Ok(request) = request.build());
|
||||||
|
let headers = request.headers();
|
||||||
|
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
|
||||||
|
assert_eq!(accept, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_response_type_xml() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get("https://httpbin.org/get")
|
||||||
|
.response_type(ResponseType::Xml);
|
||||||
|
let_assert!(Ok(request) = request.build());
|
||||||
|
let headers = request.headers();
|
||||||
|
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
|
||||||
|
assert_eq!(accept, "application/xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_response_type_text() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get("https://httpbin.org/get")
|
||||||
|
.response_type(ResponseType::Text);
|
||||||
|
let_assert!(Ok(request) = request.build());
|
||||||
|
let headers = request.headers();
|
||||||
|
let_assert!(Some(accept) = headers.get(reqwest::header::ACCEPT));
|
||||||
|
assert_eq!(accept, "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_with_response_type_none() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get("https://httpbin.org/get")
|
||||||
|
.response_type(ResponseType::None);
|
||||||
|
let_assert!(Ok(request) = request.build());
|
||||||
|
let headers = request.headers();
|
||||||
|
assert!(headers.get(reqwest::header::ACCEPT).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_get() -> Result<(), NetworkError> {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let net_request =
|
||||||
|
NetRequest::get(NetUrl::new("https://httpbin.org/get?arg=baz".into())).build();
|
||||||
|
let response: NetResponse<GetResponse> = block_on(env.get(net_request))?;
|
||||||
|
let_assert!(Some(body) = response.response_body());
|
||||||
|
assert_eq!(
|
||||||
|
body.args.get("arg"),
|
||||||
|
Some(&"baz".to_string()),
|
||||||
|
"args from body"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_get_error() {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let net_request =
|
||||||
|
NetRequest::get(NetUrl::new("https://httpbin.org/status/400".into())).build();
|
||||||
|
let result: Result<NetResponse<String>, NetworkError> = block_on(env.get(net_request));
|
||||||
|
assert!(result.is_err(), "response is not a String");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_post_json() -> Result<(), NetworkError> {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let body = serde_json::json!({"foo":"bar"});
|
||||||
|
let net_request = NetRequest::post(NetUrl::new("https://httpbin.org/post?arg=baz".into()))
|
||||||
|
.json_body(body)?
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<PostResponse> = block_on(env.post_json(net_request))?;
|
||||||
|
let_assert!(Some(body) = response.response_body());
|
||||||
|
assert_eq!(
|
||||||
|
body.args.get("arg"),
|
||||||
|
Some(&"baz".to_string()),
|
||||||
|
"args from body"
|
||||||
|
);
|
||||||
|
assert_eq!(body.data, "{\"foo\":\"bar\"}".to_string(), "data from body");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_post_json_error() -> Result<(), NetworkError> {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let net_request =
|
||||||
|
NetRequest::post(NetUrl::new("https://httpbin.org/status/400".into())).build();
|
||||||
|
let response: Result<NetResponse<PostResponse>, NetworkError> =
|
||||||
|
block_on(env.post_json(net_request));
|
||||||
|
match response {
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
Err(e) => match e {
|
||||||
|
NetworkError::MockError(method, status, _url) => {
|
||||||
|
assert_eq!(method, RequestMethod::Post);
|
||||||
|
assert_eq!(status, StatusCode::BAD_REQUEST)
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected error type"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_put() -> Result<(), NetworkError> {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let body = serde_json::json!({"foo":"bar"});
|
||||||
|
let net_request = NetRequest::put(NetUrl::new("https://httpbin.org/put?arg=baz".into()))
|
||||||
|
.json_body(body)?
|
||||||
|
.build();
|
||||||
|
let response: NetResponse<PutResponse> = block_on(env.put_json(net_request))?;
|
||||||
|
let_assert!(Some(body) = response.response_body());
|
||||||
|
assert_eq!(
|
||||||
|
body.args.get("arg"),
|
||||||
|
Some(&"baz".to_string()),
|
||||||
|
"args from body"
|
||||||
|
);
|
||||||
|
assert_eq!(body.data, "{\"foo\":\"bar\"}".to_string(), "data from body");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_put_error() {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let net_request =
|
||||||
|
NetRequest::put(NetUrl::new("https://httpbin.org/status/400".into())).build();
|
||||||
|
let result: Result<NetResponse<String>, NetworkError> = block_on(env.put_json(net_request));
|
||||||
|
assert!(result.is_err(), "response is not a String");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_real_network_env_delete() {
|
||||||
|
let env = RealNetwork::new();
|
||||||
|
let net_request =
|
||||||
|
NetRequest::delete(NetUrl::new("https://httpbin.org/delete?arg=baz".into())).build();
|
||||||
|
let result = block_on(env.delete(net_request));
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
86
src/network/request_body.rs
Normal file
86
src/network/request_body.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum RequestBody {
|
||||||
|
None,
|
||||||
|
String(String),
|
||||||
|
Json(serde_json::Value),
|
||||||
|
Xml(String), // no separate type for XML, so store as string
|
||||||
|
}
|
||||||
|
impl Default for RequestBody {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<&RequestBody> for String {
|
||||||
|
type Error = serde_json::Error;
|
||||||
|
fn try_from(value: &RequestBody) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
RequestBody::None => Ok("".to_string()),
|
||||||
|
RequestBody::String(s) => Ok(s.to_string()),
|
||||||
|
RequestBody::Json(json) => serde_json::to_string(&json),
|
||||||
|
RequestBody::Xml(s) => Ok(s.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl RequestBody {
|
||||||
|
pub fn json<T: Serialize>(source: T) -> Result<Self, serde_json::Error> {
|
||||||
|
serde_json::to_value(source).map(Self::Json)
|
||||||
|
}
|
||||||
|
pub fn content_type(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Self::None => None,
|
||||||
|
Self::String(_) => Some("text/plain".to_string()),
|
||||||
|
Self::Json(_) => Some("application/json".to_string()),
|
||||||
|
Self::Xml(_) => Some("application/xml".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use assert2::let_assert;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_request_body_json() {
|
||||||
|
let_assert!(Ok(json) = RequestBody::json("hello"));
|
||||||
|
assert_eq!(
|
||||||
|
json,
|
||||||
|
RequestBody::Json(serde_json::Value::String("hello".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_request_body_content_type() {
|
||||||
|
assert_eq!(RequestBody::None.content_type(), None);
|
||||||
|
assert_eq!(
|
||||||
|
RequestBody::String("".to_string()).content_type(),
|
||||||
|
Some("text/plain".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RequestBody::Json(json!("")).content_type(),
|
||||||
|
Some("application/json".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RequestBody::Xml("".to_string()).content_type(),
|
||||||
|
Some("application/xml".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_request_body_try_from() {
|
||||||
|
let_assert!(Ok(value) = String::try_from(&RequestBody::None));
|
||||||
|
assert_eq!(value, "");
|
||||||
|
let_assert!(Ok(value) = String::try_from(&RequestBody::String("hello".to_string())));
|
||||||
|
assert_eq!(value, "hello");
|
||||||
|
let_assert!(Ok(value) = String::try_from(&RequestBody::Json(json!("hello"))));
|
||||||
|
assert_eq!(value, "\"hello\"");
|
||||||
|
let_assert!(Ok(value) = String::try_from(&RequestBody::Xml("hello".to_string())));
|
||||||
|
assert_eq!(value, "hello");
|
||||||
|
}
|
||||||
|
}
|
26
src/network/request_method.rs
Normal file
26
src/network/request_method.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum RequestMethod {
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Propfind,
|
||||||
|
}
|
||||||
|
impl Default for RequestMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for RequestMethod {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Get => write!(f, "GET"),
|
||||||
|
Self::Post => write!(f, "POST"),
|
||||||
|
Self::Put => write!(f, "PUT"),
|
||||||
|
Self::Patch => write!(f, "PATCH"),
|
||||||
|
Self::Delete => write!(f, "DELETE"),
|
||||||
|
Self::Propfind => write!(f, "PROPFIND"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/network/response_type.rs
Normal file
22
src/network/response_type.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum ResponseType {
|
||||||
|
Json,
|
||||||
|
Xml,
|
||||||
|
None,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
impl ResponseType {
|
||||||
|
pub fn accept(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Self::Json => Some("application/json".to_string()),
|
||||||
|
Self::Text => Some("text/plain".to_string()),
|
||||||
|
Self::Xml => Some("application/xml".to_string()),
|
||||||
|
Self::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for ResponseType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Json
|
||||||
|
}
|
||||||
|
}
|
113
src/network/saved_request.rs
Normal file
113
src/network/saved_request.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use super::{RequestBody, RequestMethod};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SavedRequest {
|
||||||
|
method: RequestMethod,
|
||||||
|
url: String,
|
||||||
|
body: RequestBody,
|
||||||
|
}
|
||||||
|
impl SavedRequest {
|
||||||
|
pub fn new(method: RequestMethod, url: &str, body: RequestBody) -> Self {
|
||||||
|
Self {
|
||||||
|
method,
|
||||||
|
url: url.to_string(),
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn method(&self) -> RequestMethod {
|
||||||
|
self.method
|
||||||
|
}
|
||||||
|
pub fn url(&self) -> &str {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
pub const fn body(&self) -> &RequestBody {
|
||||||
|
&self.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert2::let_assert;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request() {
|
||||||
|
let request =
|
||||||
|
SavedRequest::new(RequestMethod::Get, "https://httpbin.org", RequestBody::None);
|
||||||
|
assert_eq!(request.method, RequestMethod::Get);
|
||||||
|
assert_eq!(request.url, "https://httpbin.org");
|
||||||
|
assert_eq!(request.body, RequestBody::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request_body() {
|
||||||
|
let request = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
let_assert!(RequestBody::String(body) = request.body());
|
||||||
|
assert_eq!(body, "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request_eq() {
|
||||||
|
let request1 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
let request2 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
assert_eq!(request1, request2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request_ne_method() {
|
||||||
|
let request1 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
let request2 = SavedRequest::new(
|
||||||
|
RequestMethod::Post,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
assert_ne!(request1, request2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request_ne_url() {
|
||||||
|
let request1 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
let request2 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org2",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
assert_ne!(request1, request2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn test_saved_request_ne_body() {
|
||||||
|
let request1 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body".to_string()),
|
||||||
|
);
|
||||||
|
let request2 = SavedRequest::new(
|
||||||
|
RequestMethod::Get,
|
||||||
|
"https://httpbin.org",
|
||||||
|
RequestBody::String("body2".to_string()),
|
||||||
|
);
|
||||||
|
assert_ne!(request1, request2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
use derive_more::{Display, From};
|
|
||||||
|
|
||||||
/// Represents a error accessing the file system or network.
|
|
||||||
#[derive(Debug, From, Display)]
|
|
||||||
pub enum Error {
|
|
||||||
Fs(#[from] crate::fs::Error),
|
|
||||||
Net(#[from] crate::net::Error),
|
|
||||||
Reqwest(#[from] reqwest::Error),
|
|
||||||
}
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
/// Represents a success or a failure using `fs` or `net`.
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
920
tests/fs.rs
920
tests/fs.rs
|
@ -1,920 +0,0 @@
|
||||||
use assert2::let_assert;
|
|
||||||
|
|
||||||
use kxio::fs;
|
|
||||||
|
|
||||||
type TestResult = Result<(), fs::Error>;
|
|
||||||
|
|
||||||
mod path {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
mod is_link {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_soft_link() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("content").expect("write");
|
|
||||||
|
|
||||||
let link_path = fs.base().join("bar");
|
|
||||||
let link = fs.path(&link_path);
|
|
||||||
file.soft_link(&link).expect("soft_link");
|
|
||||||
|
|
||||||
let exists = link.exists().expect("exists");
|
|
||||||
assert!(exists);
|
|
||||||
|
|
||||||
let is_link = link.is_link().expect("is_link");
|
|
||||||
assert!(is_link);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dir_is_not_a_link() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let dir_path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&dir_path);
|
|
||||||
dir.create().expect("create");
|
|
||||||
|
|
||||||
let is_link = dir.is_link().expect("is link");
|
|
||||||
assert!(!is_link);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn file_is_not_a_link() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("content").expect("write");
|
|
||||||
|
|
||||||
let is_link = file.is_link().expect("is link");
|
|
||||||
assert!(!is_link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod set_permissions {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_set_permissions() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
let md = file.metadata().expect("metadata");
|
|
||||||
assert!(md.is_file());
|
|
||||||
|
|
||||||
let mut perms = md.permissions();
|
|
||||||
perms.set_readonly(true);
|
|
||||||
|
|
||||||
let path = fs.path(&path);
|
|
||||||
path.set_permissions(perms).expect("set_permissions");
|
|
||||||
|
|
||||||
let md = file.metadata().expect("metadata");
|
|
||||||
assert!(md.is_file());
|
|
||||||
assert!(md.permissions().readonly());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod read_link {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_read_link() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
|
|
||||||
let link_path = fs.base().join("bar");
|
|
||||||
let link = fs.path(&link_path);
|
|
||||||
file.soft_link(&link).expect("soft_link");
|
|
||||||
|
|
||||||
let read_link = link.read_link().expect("read_link");
|
|
||||||
assert_eq!(read_link.as_pathbuf(), file_path);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.path(&path).read_link()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod metadata {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_return_metadata() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
let md = file.metadata().expect("metadata");
|
|
||||||
assert!(md.is_file());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.file(&path).metadata()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod path_of {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal { base, path: _path }) = fs.path_of("..".into())
|
|
||||||
);
|
|
||||||
assert_eq!(base, fs.base());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn matches_joins() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let joined = fs.base().join("foo").join("bar");
|
|
||||||
let path_of = fs
|
|
||||||
.path_of("foo/bar".into())
|
|
||||||
.expect("parse foo/bar into path");
|
|
||||||
|
|
||||||
assert_eq!(joined, path_of);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod as_dir {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn path_is_dir_as_dir_some() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&path);
|
|
||||||
dir.create().expect("create");
|
|
||||||
|
|
||||||
let_assert!(Ok(Some(as_dir)) = fs.path(&path).as_dir());
|
|
||||||
|
|
||||||
assert_eq!(dir.as_pathbuf(), as_dir.as_pathbuf());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn path_is_file_as_dir_none() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("contents").expect("create");
|
|
||||||
|
|
||||||
let_assert!(Ok(Some(as_file)) = fs.path(&path).as_file());
|
|
||||||
|
|
||||||
assert_eq!(file.as_pathbuf(), as_file.as_pathbuf());
|
|
||||||
assert_eq!(as_file.reader().expect("reader").to_string(), "contents");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod as_file {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn path_is_dir_as_file_none() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&path);
|
|
||||||
dir.create().expect("create");
|
|
||||||
|
|
||||||
let_assert!(Ok(None) = fs.path(&path).as_file());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn path_is_file_as_file_some() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("contents").expect("create");
|
|
||||||
|
|
||||||
let_assert!(Ok(None) = fs.path(&path).as_dir());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod is_dir {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_be_true_when_is_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.dir(&path).create().expect("create");
|
|
||||||
let is_dir = fs.path(&path).is_dir().expect("is_dir");
|
|
||||||
assert!(is_dir);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_be_false_when_is_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.file(&path).write("bar").expect("write");
|
|
||||||
let is_dir = fs.path(&path).is_dir().expect("is_dir");
|
|
||||||
assert!(!is_dir);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn should_be_false_when_is_a_link() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
|
|
||||||
let is_dir = fs.path(&path).is_dir().expect("is_dir");
|
|
||||||
assert!(!is_dir);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).is_dir()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod exists {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_be_true_when_it_exists() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.file(&path).write("bar").expect("write");
|
|
||||||
let exists = fs.path(&path).exists().expect("exists");
|
|
||||||
assert!(exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_be_false_when_it_does_not_exist() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let exists = fs.path(&path).exists().expect("exists");
|
|
||||||
assert!(!exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.path(&path).exists()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod is_file {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_be_true_when_is_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.file(&path).write("bar").expect("write");
|
|
||||||
|
|
||||||
let is_file = fs.path(&path).is_file().expect("is_file");
|
|
||||||
assert!(is_file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_be_false_when_is_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.dir(&path).create().expect("create");
|
|
||||||
let is_file = fs.path(&path).is_file().expect("is_file");
|
|
||||||
assert!(!is_file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn should_be_false_when_is_a_link() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
|
|
||||||
let is_file = fs.path(&path).is_file().expect("is_file");
|
|
||||||
assert!(!is_file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.file(&path).is_file()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod rename {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_rename_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let src_path = fs.base().join("foo");
|
|
||||||
let src = fs.file(&src_path);
|
|
||||||
src.write("bar").expect("write");
|
|
||||||
let src_contents = src.reader().expect("reader").to_string();
|
|
||||||
let dst_path = fs.base().join("bar");
|
|
||||||
let dst = fs.file(&dst_path);
|
|
||||||
src.rename(&dst).expect("rename");
|
|
||||||
let dst_contents = dst.reader().expect("reader").to_string();
|
|
||||||
assert_eq!(src_contents, dst_contents);
|
|
||||||
let src_exists = src.exists().expect("exists");
|
|
||||||
assert!(!src_exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let src_path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.file(&src_path).rename(&fs.file(&src_path))
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod from_pathbuf {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_convert_from_pathbuf() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let src_pathbuf = fs.base().join("foo");
|
|
||||||
let dst_pathbuf: PathBuf = fs.path(&src_pathbuf).into();
|
|
||||||
|
|
||||||
assert_eq!(src_pathbuf, dst_pathbuf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod file {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// // test for reading the symlink metadata
|
|
||||||
// #[test]
|
|
||||||
// fn symlink_metadata() -> TestResult {
|
|
||||||
// let fs = fs::temp().expect("temp fs");
|
|
||||||
// let file_path = fs.base().join("foo");
|
|
||||||
// let file = fs.file(&file_path);
|
|
||||||
// file.write("bar").expect("write");
|
|
||||||
// let link_path = fs.base().join("bar");
|
|
||||||
// let link = fs.path(&link_path);
|
|
||||||
// file.soft_link(&link).expect("soft_link");
|
|
||||||
// let md = link.symlink_metadata().expect("symlink metadata");
|
|
||||||
// assert!(md.is_file());
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_hard_link() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("content").expect("write");
|
|
||||||
let link_path = fs.base().join("bar");
|
|
||||||
let link = fs.file(&link_path);
|
|
||||||
file.hard_link(&link).expect("hard_link");
|
|
||||||
let exists = link.exists().expect("exists");
|
|
||||||
assert!(exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Write to a file, read it, verify it exists, is a file and has the expected contents
|
|
||||||
fn write_read_file_exists() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let pathbuf = fs.base().join("foo");
|
|
||||||
|
|
||||||
let file = fs.file(&pathbuf);
|
|
||||||
file.write("content").expect("write");
|
|
||||||
let c = file.reader().expect("reader").to_string();
|
|
||||||
assert_eq!(c, "content");
|
|
||||||
|
|
||||||
let path = fs.path(&pathbuf);
|
|
||||||
let exists = path.exists().expect("exists");
|
|
||||||
assert!(exists);
|
|
||||||
|
|
||||||
let is_file = path.is_file().expect("is_file");
|
|
||||||
assert!(is_file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn use_file_as_file() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("contents").expect("write");
|
|
||||||
|
|
||||||
file.remove().expect("remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn use_dir_as_file() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let dir_path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&dir_path);
|
|
||||||
dir.create().expect("create");
|
|
||||||
|
|
||||||
let file = fs.file(&dir_path);
|
|
||||||
let_assert!(Err(fs::Error::NotAFile { path }) = file.remove());
|
|
||||||
assert_eq!(path, dir_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn use_link_as_file() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("contents").expect("write");
|
|
||||||
|
|
||||||
let link_path = fs.base().join("bar");
|
|
||||||
let link = fs.path(&link_path);
|
|
||||||
|
|
||||||
file.soft_link(&link).expect("soft_link");
|
|
||||||
|
|
||||||
let path = fs.file(&link_path);
|
|
||||||
|
|
||||||
let contents = path.reader().expect("reader").to_string();
|
|
||||||
assert_eq!(contents, "contents");
|
|
||||||
path.remove().expect("remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
mod remove {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_remove_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
file.remove().expect("remove");
|
|
||||||
let exists = file.exists().expect("exists");
|
|
||||||
assert!(!exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.file(&path).remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod copy {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_copy_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let src_path = fs.base().join("foo");
|
|
||||||
let src = fs.file(&src_path);
|
|
||||||
src.write("bar").expect("write");
|
|
||||||
let dst_path = fs.base().join("bar");
|
|
||||||
let dst = fs.file(&dst_path);
|
|
||||||
src.copy(&dst).expect("copy");
|
|
||||||
let src_contents = src.reader().expect("reader").to_string();
|
|
||||||
let dst_contents = dst.reader().expect("reader").to_string();
|
|
||||||
assert_eq!(src_contents, dst_contents);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod symlink_metadata {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_return_metadata_for_a_file() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
let md = file.symlink_metadata().expect("metadata");
|
|
||||||
assert!(md.is_file());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_return_metadata_for_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&path);
|
|
||||||
dir.create().expect("create");
|
|
||||||
let md = dir.symlink_metadata().expect("metadata");
|
|
||||||
assert!(md.is_dir());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_return_metadata_for_a_symlink() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
|
|
||||||
let link_path = fs.base().join("bar");
|
|
||||||
let link = fs.path(&link_path);
|
|
||||||
file.soft_link(&link).expect("soft_link");
|
|
||||||
|
|
||||||
let md = link.symlink_metadata().expect("metadata");
|
|
||||||
assert!(md.is_symlink());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.file(&path).symlink_metadata()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod reader {
|
|
||||||
use super::*;
|
|
||||||
mod to_string {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_file_to_string() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
let line3 = SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.expect("duration")
|
|
||||||
.as_millis()
|
|
||||||
.to_string();
|
|
||||||
let contents = format!("line 1\nline 2\n{line3}");
|
|
||||||
file.write(&contents).expect("write");
|
|
||||||
|
|
||||||
let reader = file.reader().expect("reader");
|
|
||||||
let string = reader.as_str();
|
|
||||||
|
|
||||||
assert_eq!(string, contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_file_lines() {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
let line3 = SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.expect("duration")
|
|
||||||
.as_millis()
|
|
||||||
.to_string();
|
|
||||||
let contents = format!("line 1\nline 2\n{line3}");
|
|
||||||
file.write(&contents).expect("write");
|
|
||||||
|
|
||||||
let reader = file.reader().expect("reader");
|
|
||||||
let lines = reader.lines().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(lines, vec!["line 1", "line 2", &line3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod lines {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn read_file_lines() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("line 1\nline 2").expect("write");
|
|
||||||
|
|
||||||
let reader = file.reader().expect("reader");
|
|
||||||
let lines = reader.lines().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(lines, vec!["line 1", "line 2"]);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod bytes {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_return_bytes() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
let reader = file.reader().expect("reader");
|
|
||||||
let bytes = reader.bytes();
|
|
||||||
assert_eq!(bytes.len(), 3);
|
|
||||||
assert_eq!(bytes[0], b'b');
|
|
||||||
assert_eq!(bytes[1], b'a');
|
|
||||||
assert_eq!(bytes[2], b'r');
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod dir {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
mod create {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_create_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let pathbuf = fs.base().join("subdir");
|
|
||||||
|
|
||||||
fs.dir(&pathbuf).create().expect("create");
|
|
||||||
|
|
||||||
let exists = fs.path(&pathbuf).exists().expect("exitss");
|
|
||||||
assert!(exists);
|
|
||||||
|
|
||||||
let is_dir = fs.path(&pathbuf).is_dir().expect("is dir");
|
|
||||||
assert!(is_dir);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).create()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod create_all {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_create_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let pathbuf = fs.base().join("subdir").join("child");
|
|
||||||
|
|
||||||
fs.dir(&pathbuf).create_all().expect("create_all");
|
|
||||||
|
|
||||||
let path = fs.path(&pathbuf);
|
|
||||||
let exists = path.exists().expect("exists");
|
|
||||||
assert!(exists, "path exists");
|
|
||||||
|
|
||||||
let is_dir = path.is_dir().expect("is_dir");
|
|
||||||
assert!(is_dir, "path is a directory");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).create_all()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod read {
|
|
||||||
use crate::fs::DirItem;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_return_dir_items() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let file1 = fs.base().join("file-1");
|
|
||||||
let dir = fs.base().join("dir");
|
|
||||||
let file2 = dir.join("file-2");
|
|
||||||
fs.file(&file1).write("file-1").expect("write: file-1");
|
|
||||||
fs.dir(&dir).create().expect("create dir");
|
|
||||||
fs.file(&file2).write("file-2").expect("write: file-2");
|
|
||||||
|
|
||||||
let items = fs
|
|
||||||
.dir(fs.base())
|
|
||||||
.read()
|
|
||||||
.expect("dir.read")
|
|
||||||
.filter_map(|i| i.ok())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(items.len(), 2);
|
|
||||||
assert!(items.contains(&DirItem::File(file1)));
|
|
||||||
assert!(items.contains(&DirItem::Dir(dir)));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_not_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("file");
|
|
||||||
fs.file(&path).write("contents").expect("write");
|
|
||||||
|
|
||||||
let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir(&path).read());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).read()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod remove {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_remove_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
fs.dir(&path).create().expect("create");
|
|
||||||
fs.dir(&path).remove().expect("remove");
|
|
||||||
let exists = fs.path(&path).exists().expect("exists");
|
|
||||||
assert!(!exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod remove_all {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn should_remove_a_dir() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("foo");
|
|
||||||
let dir = fs.dir(&path);
|
|
||||||
dir.create().expect("create dir");
|
|
||||||
let sub_path = path.join("sub");
|
|
||||||
let sub = fs.dir(&sub_path);
|
|
||||||
sub.create().expect("create sub");
|
|
||||||
let file = sub_path.join("file");
|
|
||||||
fs.file(&file).write("contents").expect("write");
|
|
||||||
|
|
||||||
dir.remove_all().expect("remove");
|
|
||||||
let exists = dir.exists().expect("exists");
|
|
||||||
assert!(!exists);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn should_fail_on_path_traversal() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
let path = fs.base().join("..").join("foo");
|
|
||||||
let_assert!(
|
|
||||||
Err(fs::Error::PathTraversal {
|
|
||||||
base: _base,
|
|
||||||
path: _path
|
|
||||||
}) = fs.dir(&path).remove_all()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod canonicalize {
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_resolve_symlinks() -> TestResult {
|
|
||||||
let fs = fs::temp().expect("temp fs");
|
|
||||||
|
|
||||||
let file_path = fs.base().join("foo");
|
|
||||||
let file = fs.file(&file_path);
|
|
||||||
file.write("bar").expect("write");
|
|
||||||
|
|
||||||
let link_path = fs.base().join("link");
|
|
||||||
let link = fs.path(&link_path);
|
|
||||||
file.soft_link(&link).expect("create");
|
|
||||||
let canonical = link.canonicalize().expect("canonicalize");
|
|
||||||
let canonical = if canonical.starts_with("/private") {
|
|
||||||
// INFO: macos puts all temp files under /private
|
|
||||||
Path::new("/").join(canonical.strip_prefix("/private").unwrap())
|
|
||||||
} else {
|
|
||||||
canonical
|
|
||||||
};
|
|
||||||
assert_eq!(canonical, file_path);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
277
tests/net.rs
277
tests/net.rs
|
@ -1,277 +0,0 @@
|
||||||
use assert2::let_assert;
|
|
||||||
//
|
|
||||||
use kxio::net::{Error, MatchOn, Net};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_url() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.get(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(client.get(url))
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(response.bytes().await.expect("response body"), "Get OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_wrong_url() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.get(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
let_assert!(
|
|
||||||
Err(Error::UnexpectedMockRequest(invalid_request)) =
|
|
||||||
net.send(client.get("https://some.other.url/")).await
|
|
||||||
);
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(invalid_request.url().to_string(), "https://some.other.url/");
|
|
||||||
|
|
||||||
// remove pending unmatched request - we never meant to match against it
|
|
||||||
net.reset().expect("reset");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_post_url() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.post(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(client.post(url))
|
|
||||||
.await
|
|
||||||
.expect("reponse");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(response.bytes().await.expect("response body"), "Post OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_post_by_method() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.post(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.match_on(vec![
|
|
||||||
MatchOn::Method,
|
|
||||||
// MatchOn::Url
|
|
||||||
])
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// This request is a different url - but should still match
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(client.post("https://some.other.url"))
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(response.bytes().await.expect("response body"), "Post OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_post_by_url() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.post(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.match_on(vec![
|
|
||||||
// MatchOn::Method,
|
|
||||||
MatchOn::Url,
|
|
||||||
])
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// This request is a GET, not POST - but should still match
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(client.get(url))
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(response.bytes().await.expect("response body"), "Post OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_post_by_body() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client
|
|
||||||
.post(url)
|
|
||||||
.body("match on body")
|
|
||||||
.build()
|
|
||||||
.expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("response body")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.match_on(vec![
|
|
||||||
// MatchOn::Method,
|
|
||||||
// MatchOn::Url
|
|
||||||
MatchOn::Body,
|
|
||||||
])
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// This request is a GET, not POST - but should still match
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(client.get("https://some.other.url").body("match on body"))
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(
|
|
||||||
response.bytes().await.expect("response body"),
|
|
||||||
"response body"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_post_by_headers() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client
|
|
||||||
.post(url)
|
|
||||||
.body("foo")
|
|
||||||
.header("test", "match")
|
|
||||||
.build()
|
|
||||||
.expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("response body")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.match_on(vec![
|
|
||||||
// MatchOn::Method,
|
|
||||||
// MatchOn::Url
|
|
||||||
MatchOn::Headers,
|
|
||||||
])
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// This request is a GET, not POST - but should still match
|
|
||||||
let response = Net::from(net)
|
|
||||||
.send(
|
|
||||||
client
|
|
||||||
.get("https://some.other.url")
|
|
||||||
.body("match on body")
|
|
||||||
.header("test", "match"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
|
||||||
assert_eq!(
|
|
||||||
response.bytes().await.expect("response body"),
|
|
||||||
"response body"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[should_panic]
|
|
||||||
async fn test_unused_post() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::mock();
|
|
||||||
let client = net.client();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
let request = client.post(url).build().expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
|
||||||
.respond(my_response.into())
|
|
||||||
.expect("on request, respond");
|
|
||||||
|
|
||||||
//when
|
|
||||||
// don't send the planned request
|
|
||||||
// let _response = Net::from(net).send(client.post(url)).await.expect("send");
|
|
||||||
|
|
||||||
//then
|
|
||||||
// Drop implementation for net should panic
|
|
||||||
}
|
|
Loading…
Reference in a new issue