Compare commits

..

71 commits
v1.2.0 ... main

Author SHA1 Message Date
Renovate Bot
bfbf2b7e1d chore(deps): update kemitix/rust action to v2.4.1
All checks were successful
Rust / build (map[name:nightly]) (pull_request) Successful in 6m29s
Rust / build (map[name:stable]) (pull_request) Successful in 3m26s
Release Please / Release-plz (push) Successful in 40s
Rust / build (map[name:stable]) (push) Successful in 3m50s
Rust / build (map[name:nightly]) (push) Successful in 6m24s
2024-11-14 11:33:03 +00:00
fdb5ddb48e build: add cargo-mutants step
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m39s
Rust / build (map[name:stable]) (push) Successful in 6m35s
Release Please / Release-plz (push) Successful in 1m0s
2024-11-14 10:42:35 +00:00
76c3e1bee2 build: bump rust image to 2.4.1
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m13s
Rust / build (map[name:stable]) (push) Successful in 2m31s
Release Please / Release-plz (push) Successful in 54s
2024-11-14 10:42:35 +00:00
212aa7e0ae feat(net): mock matcher no longer uses a prebuilt request
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m3s
Rust / build (map[name:nightly]) (push) Successful in 4m15s
Release Please / Release-plz (push) Successful in 44s
2024-11-14 07:31:42 +00:00
8b6bfefbf2 docs(net): fix gramar
Some checks failed
Rust / build (map[name:stable]) (push) Successful in 2m12s
Rust / build (map[name:nightly]) (push) Successful in 4m15s
Release Please / Release-plz (push) Failing after 1m7s
2024-11-12 07:14:56 +00:00
415c37a700 refactor(net): remove inner from Net
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m7s
Rust / build (map[name:nightly]) (push) Successful in 3m59s
Release Please / Release-plz (push) Successful in 1m35s
2024-11-11 22:27:42 +00:00
dd61d39635 doc(net): added
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m1s
Rust / build (map[name:nightly]) (push) Successful in 3m33s
Release Please / Release-plz (push) Successful in 39s
2024-11-11 22:27:42 +00:00
dc74920dc8 feat(net): cleaner mock.on syntax
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m48s
Rust / build (map[name:stable]) (push) Successful in 6m28s
Release Please / Release-plz (push) Successful in 49s
2024-11-11 21:41:32 +00:00
aad02be6cb feat(net): be more permisive in what parameters are accepted
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m57s
Rust / build (map[name:stable]) (push) Successful in 2m12s
Release Please / Release-plz (push) Successful in 1m20s
2024-11-10 12:29:31 +00:00
7285cff6e7 doc(fs): minor tidy up broken links
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 4m3s
Rust / build (map[name:nightly]) (push) Successful in 1m42s
Release Please / Release-plz (push) Successful in 1m30s
2024-11-10 12:06:53 +00:00
ff8b6c64b6 fix(fs): make TempFileSystem public
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m19s
Rust / build (map[name:stable]) (push) Successful in 4m25s
Release Please / Release-plz (push) Successful in 51s
2024-11-10 12:06:53 +00:00
a0262a4d05 feat(fs): kxio::fs::new(...) now accepts impl Into<PathBuf>
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m22s
Rust / build (map[name:nightly]) (push) Successful in 3m50s
Release Please / Release-plz (push) Successful in 32s
2024-11-09 19:35:03 +00:00
4f990f907c docs(examples): add annotations to the get example
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m23s
Rust / build (map[name:stable]) (push) Successful in 3m48s
Release Please / Release-plz (push) Successful in 29s
2024-11-09 19:20:57 +00:00
9c0cf07bcc docs(readme): write a proper readme
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m30s
Rust / build (map[name:nightly]) (push) Successful in 1m57s
Release Please / Release-plz (push) Successful in 31s
2024-11-09 18:27:39 +00:00
d5340bec78 build: add cargo mutants to local dev test build step
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m48s
Rust / build (map[name:nightly]) (push) Successful in 1m59s
Release Please / Release-plz (push) Successful in 1m28s
2024-11-09 17:30:18 +00:00
4594a792e0 fix: use Default to create reqwest client
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m52s
Rust / build (map[name:stable]) (push) Successful in 4m19s
Release Please / Release-plz (push) Successful in 1m37s
2024-11-09 17:30:18 +00:00
02adc7dcd2 docs(example): get and save
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m52s
Rust / build (map[name:stable]) (push) Successful in 3m52s
Release Please / Release-plz (push) Successful in 39s
2024-11-09 15:42:53 +00:00
ac3527ce90 feat: Net and MockNet wrappers for InnerNet<Mocker> and InnerNet<Unmocked>
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m27s
Rust / build (map[name:nightly]) (push) Successful in 1m43s
Release Please / Release-plz (push) Successful in 37s
2024-11-09 15:42:53 +00:00
69c1ac8565 feat: Net<Mocked> uses internal mutability
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m24s
Rust / build (map[name:nightly]) (push) Successful in 3m52s
Release Please / Release-plz (push) Successful in 1m36s
2024-11-09 15:42:53 +00:00
17c1b4ff6d feat: add kxio::Result;
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m12s
Rust / build (map[name:stable]) (push) Successful in 4m14s
Release Please / Release-plz (push) Successful in 1m23s
2024-11-09 08:02:50 +00:00
742924e44c build: remove unlinked files
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 3m1s
Rust / build (map[name:nightly]) (push) Successful in 4m43s
Release Please / Release-plz (push) Successful in 44s
UNLINK
2024-11-08 19:26:03 +00:00
74f4954535 feat(network)!: remove legacy network interface
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m12s
Rust / build (map[name:stable]) (push) Successful in 4m4s
Release Please / Release-plz (push) Successful in 42s
2024-11-08 19:26:03 +00:00
c81fe6753f feat(net)!: fluent api
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m37s
Rust / build (map[name:nightly]) (push) Successful in 1m57s
Release Please / Release-plz (push) Successful in 27s
Closes kemitix/kxio#43
2024-11-08 19:11:23 +00:00
17816fa6ed build: ignore cargo-mutants output
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m15s
Rust / build (map[name:nightly]) (push) Successful in 4m25s
Release Please / Release-plz (push) Successful in 38s
2024-11-08 18:12:25 +00:00
ed6d83cb7b tests(fs): add more test
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m1s
Rust / build (map[name:stable]) (push) Successful in 4m5s
Release Please / Release-plz (push) Successful in 42s
2024-11-08 18:12:12 +00:00
Renovate Bot
e264bea729 fix(deps): update rust crate thiserror to v2
All checks were successful
Rust / build (map[name:stable]) (pull_request) Successful in 2m5s
Rust / build (map[name:nightly]) (pull_request) Successful in 3m51s
Release Please / Release-plz (push) Successful in 38s
Rust / build (map[name:nightly]) (push) Successful in 3m57s
Rust / build (map[name:stable]) (push) Successful in 3m53s
2024-11-06 03:01:45 +00:00
838ae0755b feat(fs): add .path(path).read_link()
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m2s
Rust / build (map[name:nightly]) (push) Successful in 6m24s
Release Please / Release-plz (push) Successful in 1m19s
2024-11-04 07:18:57 +00:00
b512b0a0d8 refactor(fs): PathReal owns its own data
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m4s
Rust / build (map[name:nightly]) (push) Successful in 4m2s
Release Please / Release-plz (push) Successful in 1m25s
2024-11-04 07:18:57 +00:00
de46ff57c1 feat(fs): add .path(path).set_permissions(perms)
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 3m56s
Rust / build (map[name:stable]) (push) Successful in 5m42s
Release Please / Release-plz (push) Successful in 45s
2024-11-04 07:18:57 +00:00
abd854f749 docs(fs): make it clearer what the std::fs functions map to
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m52s
Rust / build (map[name:nightly]) (push) Successful in 6m6s
Release Please / Release-plz (push) Successful in 39s
2024-11-04 07:18:57 +00:00
ecb61490f7 docs(fs): move checklist/std::fs mapping to rustdoc
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 3m50s
Rust / build (map[name:stable]) (push) Successful in 5m52s
Release Please / Release-plz (push) Successful in 1m23s
2024-11-04 07:18:57 +00:00
c0e40e6c2d feat(fs): add .path(path).symlink_metadata()
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m55s
Rust / build (map[name:nightly]) (push) Successful in 3m52s
Release Please / Release-plz (push) Successful in 41s
2024-11-04 07:18:57 +00:00
f810927faf feat(fs): add .path(path).canonicalize()
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m51s
Rust / build (map[name:nightly]) (push) Successful in 3m33s
Release Please / Release-plz (push) Successful in 2m24s
2024-11-04 07:18:57 +00:00
c4df3d18c7 feat(fs): add .path(path).soft_link(other), .path(path).is_link()
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m4s
Rust / build (map[name:stable]) (push) Successful in 3m41s
Release Please / Release-plz (push) Successful in 38s
2024-11-03 22:10:34 +00:00
d3f3a9e909 feat(fs): add .path(path).metadata()
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m2s
Rust / build (map[name:stable]) (push) Successful in 3m41s
Release Please / Release-plz (push) Successful in 42s
2024-11-03 22:09:40 +00:00
10e6243f6e feat(fs): add .file(path).hard_link(path)
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 3m59s
Rust / build (map[name:stable]) (push) Successful in 1m45s
Release Please / Release-plz (push) Successful in 1m26s
2024-11-03 22:08:18 +00:00
015c28632e refactor: regroup integration tests into modules
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m28s
Rust / build (map[name:stable]) (push) Successful in 4m28s
Release Please / Release-plz (push) Successful in 1m38s
2024-11-03 15:01:46 +00:00
f825aad327 feat(fs): add .path(path).rename()
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m22s
Rust / build (map[name:nightly]) (push) Successful in 4m5s
Release Please / Release-plz (push) Successful in 1m23s
2024-11-03 14:48:19 +00:00
afee181872 feat(fs): add .file(path).remove()
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m9s
Rust / build (map[name:stable]) (push) Successful in 1m50s
Release Please / Release-plz (push) Successful in 47s
2024-11-03 14:17:59 +00:00
d7725d63ee feat(fs): add .dir(path).remove_all()
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m3s
Rust / build (map[name:stable]) (push) Successful in 4m22s
Release Please / Release-plz (push) Successful in 42s
2024-11-03 14:12:03 +00:00
ba3a388705 feat(fs): add .dir(path).remove()
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m50s
Rust / build (map[name:stable]) (push) Successful in 4m25s
Release Please / Release-plz (push) Successful in 1m42s
2024-11-03 14:05:45 +00:00
16e57b8ca9 feat(fs): add .reader().bytes()
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m4s
Rust / build (map[name:nightly]) (push) Successful in 4m13s
Release Please / Release-plz (push) Successful in 1m27s
2024-11-03 14:03:16 +00:00
6aeb4521fc refactor(fs): use type aliases
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m2s
Rust / build (map[name:stable]) (push) Successful in 3m54s
Release Please / Release-plz (push) Successful in 41s
2024-11-03 14:01:53 +00:00
2f236b752c feat(fs): add .copy(dest)
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m4s
Rust / build (map[name:nightly]) (push) Successful in 3m45s
Release Please / Release-plz (push) Successful in 1m35s
2024-11-03 11:46:09 +00:00
b593a8f67e refactor(fs): use type aliases
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m53s
Rust / build (map[name:nightly]) (push) Successful in 3m35s
Release Please / Release-plz (push) Successful in 1m25s
2024-11-03 11:41:59 +00:00
3be53a969d docs(readme): apply formatting to std::fs::* todo list
All checks were successful
Release Please / Release-plz (push) Successful in 37s
Rust / build (map[name:nightly]) (push) Successful in 1m57s
Rust / build (map[name:stable]) (push) Successful in 3m40s
2024-11-03 11:08:22 +00:00
1d947862f6 docs(readme): reformat todo list for std::fs::*
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m5s
Rust / build (map[name:nightly]) (push) Successful in 3m49s
Release Please / Release-plz (push) Successful in 1m36s
2024-11-03 11:05:11 +00:00
fa4232de6b refactor: cleanup
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m58s
Rust / build (map[name:nightly]) (push) Successful in 3m48s
Release Please / Release-plz (push) Successful in 35s
2024-11-02 21:29:27 +00:00
76d75cabd9 feat: remove need for mutability
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m54s
Rust / build (map[name:nightly]) (push) Successful in 3m38s
Release Please / Release-plz (push) Successful in 1m25s
adds rustdocs
2024-11-02 20:02:52 +00:00
d2ee798f25 refactor: use generics for path type
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m59s
Rust / build (map[name:stable]) (push) Successful in 3m25s
Release Please / Release-plz (push) Successful in 34s
2024-11-02 12:16:42 +00:00
be7a6febcb docs: update readme
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m2s
Rust / build (map[name:stable]) (push) Successful in 3m56s
Release Please / Release-plz (push) Successful in 1m23s
2024-11-02 07:13:44 +00:00
6f95c04eb6 feat(fs): add lines to reader
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m8s
Rust / build (map[name:stable]) (push) Successful in 3m54s
Release Please / Release-plz (push) Successful in 1m33s
2024-11-01 21:49:22 +00:00
587d60ee3d feat(fs): add as_dir/as_file to convert from path
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m5s
Rust / build (map[name:stable]) (push) Successful in 3m47s
Release Please / Release-plz (push) Successful in 1m32s
2024-11-01 21:49:22 +00:00
09e1d91a9e test: remove unit tests
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m7s
Rust / build (map[name:nightly]) (push) Successful in 4m17s
Release Please / Release-plz (push) Successful in 40s
These have all been moved over to integration tests.
Integrtaion
2024-11-01 21:32:37 +00:00
d50164931d refactor: split real module into sub-modules
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m9s
Rust / build (map[name:stable]) (push) Successful in 4m7s
Release Please / Release-plz (push) Successful in 38s
2024-11-01 21:32:37 +00:00
791fa74e78 refactor: move new fns to their struct
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m12s
Rust / build (map[name:nightly]) (push) Successful in 4m22s
Release Please / Release-plz (push) Successful in 38s
2024-11-01 21:16:32 +00:00
17f6f877b6 test(fs): integration tests
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m6s
Rust / build (map[name:stable]) (push) Successful in 4m21s
Release Please / Release-plz (push) Successful in 44s
2024-11-01 21:16:32 +00:00
b3118173ad feat(fs)!: remove legacy filesystem module
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m8s
Rust / build (map[name:nightly]) (push) Successful in 2m16s
Release Please / Release-plz (push) Successful in 32s
2024-11-01 08:59:09 +00:00
b756881a60 refactor: extract result module
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 1m55s
Rust / build (map[name:stable]) (push) Successful in 2m5s
Release Please / Release-plz (push) Successful in 1m33s
2024-11-01 08:33:02 +00:00
a629950d4d feat(fs)!: new fluent API
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m5s
Rust / build (map[name:stable]) (push) Successful in 4m31s
Release Please / Release-plz (push) Successful in 1m23s
2024-11-01 08:20:59 +00:00
8c76ce49e0 test: verify path_of normal behaviour
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m0s
Rust / build (map[name:stable]) (push) Successful in 4m30s
Release Please / Release-plz (push) Successful in 37s
2024-11-01 07:32:39 +00:00
Renovate Bot
2a5f47a9c9 fix(deps): update rust crate secrecy to 0.10
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 2m5s
Rust / build (map[name:stable]) (push) Successful in 4m23s
Release Please / Release-plz (push) Successful in 36s
2024-10-30 06:38:46 +00:00
3699733e32 build: switch to forgejo actions
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 2m23s
Rust / build (map[name:nightly]) (push) Successful in 4m20s
Release Please / Release-plz (push) Successful in 36s
- Replace the woodpecker config with forgejo actions.
- Add issue number to TODOs to pass CI check
- Remove special clang/mold linker commands
- Remove docker builder
- Add lints.rust.unexoected_cfgs to allo tarpaulin_include
- fix: revert upgrade to secrecy 0.10
2024-10-29 22:30:17 +00:00
Renovate Bot
d90a1c42c8 fix(deps): update rust crate secrecy to 0.10
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-09-17 23:16:55 +00:00
Renovate Bot
9943a0fe4e chore(deps): update docker.io/rust docker tag to v1.81
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-09-05 23:46:32 +00:00
Renovate Bot
a2e91a871b fix(deps): update rust crate derive_more to 1.0.0-beta
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-08-01 18:15:41 +00:00
Renovate Bot
ca7be5c484 chore(deps): update docker.io/rust docker tag to v1.80
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-25 20:45:42 +00:00
Renovate Bot
7335e78552 chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4.2
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-16 16:30:56 +00:00
Renovate Bot
9c819fd856 chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4.1
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-09 20:00:36 +00:00
Renovate Bot
3f805b3ad5 chore(deps): update docker.io/rust docker tag to v1.79
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-06-13 17:45:39 +00:00
274f70e485 feat: network: add from impl to help discard unit NetResponses
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-06-06 07:09:43 +01:00
45 changed files with 3242 additions and 3416 deletions

View file

@ -1,7 +1,4 @@
# ./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

View file

@ -0,0 +1,36 @@
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.4.1
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.4.1
with:
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View file

@ -0,0 +1,58 @@
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.4.1
with:
args: ${{ matrix.toolchain.name }} cargo machete
- name: Format
uses: https://git.kemitix.net/kemitix/rust@v2.4.1
with:
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
- name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v2.4.1
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
- name: Build
uses: https://git.kemitix.net/kemitix/rust@v2.4.1
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
- name: Test
uses: https://git.kemitix.net/kemitix/rust@v2.4.1
with:
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test
- name: Mutations
uses: https://git.kemitix.net/kemitix/rust@v2.4.1
with:
args: ${{ matrix.toolchain.name }} cargo mutants -vV --in-place

3
.gitignore vendored
View file

@ -16,3 +16,6 @@ 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/

View file

@ -1,72 +0,0 @@
steps:
update-builder-image:
when:
- event: cron
image: docker.io/woodpeckerci/plugin-docker-buildx:4.0
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
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 --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
- cargo build --features "fs,network"
test:
when:
- event: push
branch: [main, next]
- event: tag
image: git.kemitix.net/kemitix/kxio-builder:latest
environment:
CARGO_TERM_COLOR: always
commands:
- cargo test --features "fs,network"
publish_to_crates_io:
when:
- event: tag
image: docker.io/rust:1.78
commands:
- cargo login "$CARGO_REGISTRY_TOKEN"
- cargo publish --registry crates-io --no-verify
secrets: [cargo_registry_token]
publish_to_forgejo:
when:
- event: tag
# INFO: https://woodpecker-ci.org/plugins/Gitea%20Release
image: docker.io/woodpeckerci/plugin-gitea-release:latest
settings:
base_url: https://git.kemitix.net
api_key:
from_secret: FORGEJO_RELEASE_PLUGIN
target: main
prerelease: false

View file

@ -8,43 +8,31 @@ license = "MIT"
repository = "https://git.kemitix.net/kemitix/kxio" repository = "https://git.kemitix.net/kemitix/kxio"
exclude = [".cargo_home"] exclude = [".cargo_home"]
[features] [lints.rust]
default = ["fs", "network"] 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]
# logging derive_more = { version = "1.0", features = [
tracing = "0.1"
# network
async-trait = "0.1"
http = "1.1"
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"
path-clean = "1.0"
# boilerplate
derive_more = { version = "1.0.0-beta.6", features = [
"from",
"display",
"constructor", "constructor",
"display",
"from"
] } ] }
http = "1.1"
path-clean = "1.0"
reqwest = "0.12"
url = "2.5"
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]

View file

@ -1,23 +0,0 @@
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

View file

@ -1,40 +1,69 @@
## kxio # kxio
[![status-badge](https://ci.kemitix.net/api/badges/53/status.svg)](https://ci.kemitix.net/repos/53) `kxio` is a Rust library that provides injectable `FileSystem` and `Network`
resources to enhance the testability of your code. By abstracting system-level
interactions, `kxio` enables easier mocking and testing of code that relies on
file system and network operations.
Provides injectable Filesystem and Network resources to make code more testable. ## Features
### FileSystem - **Filesystem Abstraction**
- **Network Abstraction**
- **Enhanced Testability**
There are two FileSystem implementation: [filesystem] and [fs]. ## Filesystem
- [filesystem] is the legacy implementation and will be removed in a future version. The Filesystem module offers a clean abstraction over `std::fs`, the standard
- [fs] is the current version and is intended to stand-in for and extend the [std::fs] module from the Standard Library. file system operations. For comprehensive documentation and usage examples,
please refer to <https://docs.rs/kxio/latest/kxio/fs/>.
#### std::fs alternatives ### Key Filesystem Features:
| To Do | [std::fs] | [kxio::fs::FileSystem] | | - File reading and writing
| ----- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | - Directory operations
| [ ] | canonicalize | path_canonicalize | Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved. | - File metadata access
| [ ] | copy | file_copy | Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. | - Fluent API for operations like `.reader().bytes()`
| [ ] | create_dir | dir_create | Creates a new, empty directory at the provided path |
| [ ] | create_dir_all | dir_create_all | Recursively create a directory and all of its parent components if they are missing. |
| [ ] | hard_link | link_create | Creates a new hard link on the filesystem. |
| [ ] | metadata | path_metadata | Given a path, query the file system to get information about a file, directory, etc. |
| [ ] | read | file_read | Read the entire contents of a file into a bytes vector. |
| [ ] | read_dir | dir_read | Returns an iterator over the entries within a directory. |
| [ ] | read_link | link_read | Reads a symbolic link, returning the file that the link points to. |
| [x] | read_to_string | file_read_to_string | Read the entire contents of a file into a string. |
| [ ] | remove_dir | dir_remove | Removes an empty directory. |
| [ ] | remove_dir_all | dir_remove_all | Removes a directory at this path, after removing all its contents. Use carefully! |
| [ ] | remove_file | file_remove | Removes a file from the filesystem. |
| [ ] | rename | path_rename | Rename a file or directory to a new name, replacing the original file if to already exists. |
| [ ] | set_permissions | path_set_permissions | Changes the permissions found on a file or a directory. |
| [ ] | symlink_metadata | link_metadata | Query the metadata about a file without following symlinks. |
| [x] | write | file_write | Write a slice as the entire contents of a file. |
### Network ## Network
The Network module offers a testable interface over the `reqwest` crate. For
comprehensive documentation and usage examples, please refer to
<https://docs.rs/kxio/latest/kxio/net/>
## Getting Started
Add `kxio` to your `Cargo.toml`:
```toml
[dependencies]
kxio = "x.y.z"
```
## Usage
See the example [get.rs](./examples/get.rs) for an annotated example on how to use the `kxio` library.
It covers both the `net` and `fs` modules.
## Development
- The project uses [Cargo Mutants](https://crates.io/crates/cargo-mutants) for mutation testing.
- [ForgeJo Actions](https://forgejo.org/docs/next/user/actions/) are used for continuous testing and linting.
## Contributing
Contributions are welcome! Please check our [issue tracker](https://git.kemitix.net/kemitix/kxio/issues) for open tasks or
submit your own ideas.
## License
This project is licensed under the terms specified in the `LICENSE` file in the
repository root.
## Acknowledgements
- Built with Rust
---
For more information, bug reports, or feature requests, please visit our [repository](https://git.kemitix.net/kemitix/kxio).
The entire [network] module needs to be completly rewritten
It's use is strongly discouraged.
A new [net] module will likely be its replacement.

149
examples/get.rs Normal file
View file

@ -0,0 +1,149 @@
/// 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 mock_net: kxio::net::MockNet = kxio::net::mock();
let url = "http://localhost:8080";
// declare what response should be made for a given request
let response = mock_net.response().body("contents").expect("response body");
mock_net
.on(http::Method::GET)
.url(url::Url::parse(url).expect("parse url"))
.respond(response);
// 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");
// Create a [Net] from the [MockNet] to pass to the system under tets
let net = kxio::net::Net::from(mock_net);
//when
// Pass the file sytsem and network abstractions to the code to be tested
download_and_save_to_file(url, &file_path, &fs, &net)
.await
.expect("system under test");
//then
// Read the file
let file = fs.file(&file_path);
let reader = file.reader().expect("reader");
let contents = reader.as_str();
assert_eq!(contents, "contents");
// not needed for this test, but should it be needed, we can avoid checking for any
// unconsumed request matches.
// let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
// mock_net.reset();
}
}

View file

@ -1,3 +1,21 @@
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
doc-test:
cargo doc
cargo test
cargo test --example get
install-hooks: install-hooks:
@echo "Installing git hooks" @echo "Installing git hooks"
git config core.hooksPath .git-hooks git config core.hooksPath .git-hooks

View file

@ -1,167 +0,0 @@
#![allow(deprecated)]
use std::{
ops::Deref,
path::PathBuf,
sync::{Arc, Mutex},
};
use tempfile::{tempdir, TempDir};
use tracing::{debug, info};
pub fn real(cwd: Option<PathBuf>) -> FileSystem {
let cwd = cwd.unwrap_or_default();
FileSystem::Real(RealFileSystem::new(cwd))
}
pub fn temp() -> std::io::Result<FileSystem> {
TempFileSystem::new().map(FileSystem::Temp)
}
#[derive(Clone, Debug)]
#[deprecated(since = "1.1.0", note = "Use [kxio::fs::FileSystem] instead")]
pub enum FileSystem {
Real(RealFileSystem),
Temp(TempFileSystem),
}
impl FileSystem {
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::real()] instead")]
pub fn new_real(cwd: Option<PathBuf>) -> Self {
real(cwd)
}
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::temp()] instead")]
pub fn new_temp() -> std::io::Result<Self> {
temp()
}
}
impl Deref for FileSystem {
type Target = dyn FileSystemLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(env) => env,
Self::Temp(env) => env,
}
}
}
pub trait FileSystemLike: 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 RealFileSystem {
cwd: PathBuf,
}
#[derive(Clone, Debug)]
pub struct TempFileSystem {
cwd: PathBuf,
// Handle to the temporary directory
// When this handle is dropped the directory is deleted
_temp_dir: Arc<Mutex<TempDir>>,
}
impl FileSystemLike for TempFileSystem {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl FileSystemLike for RealFileSystem {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl RealFileSystem {
const fn new(cwd: PathBuf) -> Self {
Self { cwd }
}
}
impl TempFileSystem {
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: 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 = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_create_on_temp_fs() -> std::io::Result<()> {
let env = TempFileSystem::new()?;
assert!(env.cwd().exists());
Ok(())
}
#[test_log::test]
fn test_create_on_real_fs() {
let cwd = PathBuf::from("/tmp");
let env = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_write_and_read_file() -> std::io::Result<()> {
let env = TempFileSystem::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 Normal file
View file

@ -0,0 +1,121 @@
//
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),
}
}
}

View file

@ -1,8 +1,12 @@
//
use std::{ use std::{
fs::{DirEntry, ReadDir}, fs::{DirEntry, ReadDir},
path::PathBuf, path::PathBuf,
}; };
use super::Error;
/// Represents an item in a directory
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DirItem { pub enum DirItem {
File(PathBuf), File(PathBuf),
@ -12,6 +16,7 @@ pub enum DirItem {
Unsupported(PathBuf), Unsupported(PathBuf),
} }
/// An iterator for items in a directory.
#[derive(Debug, derive_more::Constructor)] #[derive(Debug, derive_more::Constructor)]
pub struct DirItemIterator(ReadDir); pub struct DirItemIterator(ReadDir);
impl Iterator for DirItemIterator { impl Iterator for DirItemIterator {
@ -23,8 +28,8 @@ impl Iterator for DirItemIterator {
} }
fn map_dir_item(item: std::io::Result<DirEntry>) -> super::Result<DirItem> { fn map_dir_item(item: std::io::Result<DirEntry>) -> super::Result<DirItem> {
let item = item?; let item = item.map_err(Error::Io)?;
let file_type = item.file_type()?; let file_type = item.file_type().map_err(Error::Io)?;
if file_type.is_dir() { if file_type.is_dir() {
Ok(DirItem::Dir(item.path())) Ok(DirItem::Dir(item.path()))
} else if file_type.is_file() { } else if file_type.is_file() {

101
src/fs/file.rs Normal file
View file

@ -0,0 +1,101 @@
//
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)
}
}

View file

@ -1,23 +0,0 @@
use crate::fs::DirItem;
use super::Result;
use std::path::{Path, PathBuf};
pub trait FileSystemLike {
fn base(&self) -> &Path;
fn dir_create(&self, path: &Path) -> Result<()>;
fn dir_create_all(&self, path: &Path) -> Result<()>;
/// Reads the items in a directory and returns them as an iterator.
fn dir_read(&self, path: &Path) -> Result<Box<dyn Iterator<Item = Result<DirItem>>>>;
fn file_read_to_string(&self, path: &Path) -> Result<String>;
fn file_write(&self, path: &Path, contents: &str) -> Result<()>;
fn path_exists(&self, path: &Path) -> Result<bool>;
fn path_is_dir(&self, path: &Path) -> Result<bool>;
fn path_is_file(&self, path: &Path) -> Result<bool>;
fn path_of(&self, path: PathBuf) -> Result<PathBuf>;
}

View file

@ -1,56 +1,100 @@
//! Provides an injectable reference to part of the filesystem.
//!
//! Create a new [FileSystem] to access a directory using [crate::fs::new].
//! Create a new [TempFileSystem] to access a temporary directory using [crate::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");
//! 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; use std::path::PathBuf;
use derive_more::From; mod dir;
mod dir_item;
use crate::fs::like::FileSystemLike; mod file;
mod path;
mod like; mod reader;
mod real; mod result;
mod system;
mod temp; mod temp;
mod dir_item; pub use dir_item::{DirItem, DirItemIterator};
pub use dir_item::DirItem; pub use path::{DirMarker, FileMarker, PathMarker, PathReal};
pub use dir_item::DirItemIterator; pub use reader::Reader;
pub use result::{Error, Result};
pub use system::{DirHandle, FileHandle, FileSystem, PathHandle};
pub use temp::TempFileSystem;
#[derive(Debug, From, derive_more::Display)] /// Creates a new `FileSystem` for the path.
pub enum Error { ///
Io(std::io::Error), /// This will create a `FileSystem` that provides access to the
/// filesystem under the given path.
#[display("Path access attempted outside of base ({base:?}): {path:?}")] ///
PathTraversal { /// Any attempt to access outside this base will result in a
base: PathBuf, /// `error::Error::PathTraversal` error when attempting the
path: PathBuf, /// opertation.
}, pub fn new(base: impl Into<PathBuf>) -> FileSystem {
FileSystem::new(base.into())
#[display("Path must be a directory: {path:?}")]
NotADirectory {
path: PathBuf,
},
}
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
pub const fn new(base: PathBuf) -> FileSystem {
FileSystem::Real(real::new(base))
} }
pub fn temp() -> Result<FileSystem> { /// Creates a new `TempFileSystem` for a temporary directory.
temp::new().map(FileSystem::Temp) ///
} /// The `TempFileSystem` provides a `Deref` to a `FileSystem` for
/// the temporary directory.
#[derive(Clone, Debug)] ///
pub enum FileSystem { /// When the `TempFileSystem` is dropped, the temporary directory
Real(real::RealFileSystem), /// is deleted.
Temp(temp::TempFileSystem), ///
} /// Returns an error if the temporary directory cannot be created.
impl std::ops::Deref for FileSystem { pub fn temp() -> Result<temp::TempFileSystem> {
type Target = dyn FileSystemLike; temp::TempFileSystem::new()
fn deref(&self) -> &Self::Target {
match self {
Self::Real(fs) => fs,
Self::Temp(fs) => fs.deref(),
}
}
} }

369
src/fs/path.rs Normal file
View file

@ -0,0 +1,369 @@
//
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)
}
}

73
src/fs/reader.rs Normal file
View file

@ -0,0 +1,73 @@
//
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)
}
}

View file

@ -1,98 +0,0 @@
use std::path::{Path, PathBuf};
use crate::fs::{DirItem, DirItemIterator};
pub const fn new(base: PathBuf) -> RealFileSystem {
RealFileSystem { base }
}
#[derive(Clone, Debug)]
pub struct RealFileSystem {
base: PathBuf,
}
impl super::FileSystemLike for RealFileSystem {
fn base(&self) -> &Path {
&self.base
}
fn dir_create(&self, path: &Path) -> super::Result<()> {
self.validate(path)?;
std::fs::create_dir(path).map_err(Into::into)
}
fn dir_create_all(&self, path: &Path) -> super::Result<()> {
self.validate(path)?;
std::fs::create_dir_all(path).map_err(Into::into)
}
fn dir_read(
&self,
path: &Path,
) -> super::Result<Box<dyn Iterator<Item = super::Result<DirItem>>>> {
self.validate(path)?;
if !self.path_is_dir(path)? {
return Err(super::Error::NotADirectory {
path: path.to_path_buf(),
});
}
let read_dir = std::fs::read_dir(path)?;
Ok(Box::new(DirItemIterator::new(read_dir)))
}
fn file_read_to_string(&self, path: &Path) -> super::Result<String> {
self.validate(path)?;
std::fs::read_to_string(path).map_err(Into::into)
}
fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> {
self.validate(path)?;
std::fs::write(path, contents).map_err(Into::into)
}
fn path_exists(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.exists())
}
fn path_is_dir(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.is_dir())
}
fn path_is_file(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.is_file())
}
fn path_of(&self, path: PathBuf) -> super::Result<PathBuf> {
let path_of = self.base.as_path().join(path);
self.validate(&path_of)?;
Ok(path_of)
}
}
impl RealFileSystem {
fn validate(&self, path: &Path) -> super::Result<()> {
let path = self.clean_path(path)?;
if !path.starts_with(&self.base) {
return Err(super::Error::PathTraversal {
base: self.base.clone(),
path,
});
}
Ok(())
}
fn clean_path(&self, path: &Path) -> super::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()?.join(path)
}
.clean();
Ok(abs_path)
}
}

52
src/fs/result.rs Normal file
View file

@ -0,0 +1,52 @@
//
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 Normal file
View file

@ -0,0 +1,153 @@
//
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)
}
}

View file

@ -2,26 +2,28 @@ use std::sync::{Arc, Mutex};
use tempfile::TempDir; use tempfile::TempDir;
pub(super) fn new() -> super::Result<TempFileSystem> { use super::{Error, FileSystem};
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir));
let real = super::real::new(base);
Ok(TempFileSystem {
real,
_temp_dir: temp_dir,
})
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TempFileSystem { pub struct TempFileSystem {
real: super::real::RealFileSystem, real: FileSystem,
_temp_dir: Arc<Mutex<TempDir>>, _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 { impl std::ops::Deref for TempFileSystem {
type Target = dyn super::FileSystemLike; type Target = FileSystem;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.real &self.real

View file

@ -1,11 +1,70 @@
#[cfg(feature = "fs")] //! # kxio
pub mod filesystem; //!
//! `kxio` is a Rust library that provides injectable `FileSystem` and `Network`
//! resources to enhance the testability of your code. By abstracting system-level
//! interactions, `kxio` enables easier mocking and testing of code that relies on
//! file system and network operations.
//!
//! ## Features
//!
//! - Filesystem Abstraction
//! - Network Abstraction
//! - Enhanced Testability
//!
//! ## Filesystem
//!
//! The Filesystem module offers a clean abstraction over `std::fs`, the standard
//! file system operations. For comprehensive documentation and usage examples,
//! please refer to <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](https://git.kemitix.net/kemitix/kxio/src/branch/main/examples/get.rs) for an annotated example on how to use the `kxio` library.
//! It covers both the `net` and `fs` modules.
//!
//! ## Development
//!
//! - The project uses [Cargo Mutants](https://crates.io/crates/cargo-mutants) for mutation testing.
//! - [ForgeJo Actions](https://forgejo.org/docs/next/user/actions/) are used for continuous testing and linting.
//!
//! ## Contributing
//!
//! Contributions are welcome! Please check our [issue tracker](https://git.kemitix.net/kemitix/kxio/issues) for open tasks or
//! submit your own ideas.
//!
//! ## License
//!
//! This project is licensed under the terms specified in the `LICENSE` file in the
//! repository root.
//!
//! ---
//!
//! For more information, bug reports, or feature requests, please visit our [repository](https://git.kemitix.net/kemitix/kxio).
#[cfg(feature = "fs")]
pub mod fs; pub mod fs;
pub mod net;
mod result;
#[cfg(feature = "network")] pub use result::{Error, Result};
pub mod network;
#[cfg(test)]
mod tests;

300
src/net/mod.rs Normal file
View file

@ -0,0 +1,300 @@
//! Provides a generic interface for network operations.
//!
//!
//! Provides a testable interface over the [reqwest] crate.
//!
//! ## Overview
//!
//! The `net` module provides a testable interface for network operations.
//! It includes implementations for both real network interactions and mocked network operations for testing purposes.
//!
//! ## Key methods and types:
//!
//! - [kxio::net::new()][new()]: Creates a new `Net` instance
//! - [kxio::net::mock()][mock()]: Creates a new `MockNet` instance for use in tests
//! - [Error]: enum for network-related errors
//! - [Result]: an alias for `core::result::Result<T, Error>`
//! - [Net]: struct for real and mocked network operations
//! - [MockNet]: struct for defining behaviours of mocked network operations
//!
//! ## Usage
//!
//! Write your program to take a reference to [Net].
//!
//! Use the [Net::client] functionto create a [reqwest::RequestBuilder] which you should then pass to the [Net::send] method.
//! This is rather than building the request and calling its own `send` method, doing so would result in the network request being sent, even under-test.
//!
//! ```rust
//! use kxio::net;
//! async fn get_example(net: &net::Net) -> net::Result<()> {
//! let response = net.send(net.client().get("https://example.com")).await?;
//! ///...
//! Ok(())
//! }
//! ```
//!
//! ### Real Network Operations
//!
//! In your production code you will want to make real network requests.
//!
//! Construct a [Net] using [kxio::net::new()][new()]. Then pass as a reference to your program.
//!
//! ```rust
//! use kxio::net;
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! let net = net::new();
//!
//! get_example(&net).await?;
//! # Ok(())
//! # }
//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
//! ```
//!
//! ### Mocked Network Operations
//!
//! In your tests you will want to mock your network requests and responses.
//!
//! Construct a [MockNet] using [kxio::net::mock()][mock()].
//!
//! ```rust
//! use kxio::net;
//! let mock_net = net::mock();
//! ```
//!
//! Create a [reqwest::Client] using [MockNet::client()].
//!
//! ```rust
//! # let mock_net = kxio::net::mock();
//! let client = mock_net.client();
//! // this is the same as:
//! let client = reqwest::Client::new();
//! ```
//!
//! Define the expected responses for each request, using the [MockNet::on],
//! that you expect you program to make during the test. You can choose what each request should be
//! matched against. The default is to the match when both the Method and Url are the same.
//!
//! ```rust
//! use kxio::net;
//! use kxio::net::MatchOn;
//!
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! # let mock_net = net::mock();
//! mock_net.on(http::Method::GET)
//! .url(url::Url::parse("https://example.com")?)
//! .respond(mock_net.response().status(200).body("")?);
//! mock_net.on(http::Method::GET)
//! .url(url::Url::parse("https://example.com/foo")?)
//! .respond(mock_net.response().status(500).body("Mocked response")?);
//! # mock_net.reset();
//! # Ok(())
//! # }
//! ```
//!
//! All [MatchOn] options:
//! - [MatchOn::Method] (default)
//! - [MatchOn::Url] (default)
//! - [MatchOn::Headers]
//! - [MatchOn::Body].
//!
//! Once you have defined all your expected responses, convert the [MockNet] into a [Net].
//!
//! ```rust
//! # use kxio::net;
//! # let mock_net = net::mock();
//! let net: net::Net = mock_net.into();
//! // or
//! # let mock_net = net::mock();
//! let net = net::Net::from(mock_net);
//! ```
//!
//! Now you can pass a reference to `net` to your program.
//!
//! ```rust
//! # use kxio::net;
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! # let mock_net = net::mock();
//! # let net = net::Net::from(mock_net);
//! get_example(&net).await?;
//! # Ok(())
//! # }
//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
//! ```
//!
//! When your test is finished, the [MockNet] will check that all the expected requests were
//! actually made. If there were any missed, then the test will [panic].
//!
//! If you don't want this to happen, then call [MockNet::reset] before your test finishes.
//! You will need to recover the [MockNet] from the [Net].
//!
//! ```rust
//! # use kxio::net;
//! # let mock_net = net::mock();
//! # let net = net::Net::from(mock_net);
//! let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
//! mock_net.reset();
//! ````
//!
//! ## Error Handling
//!
//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum,
//! allowing for specific error handling related to network operations.
//! Provides a testable interface over the [reqwest] crate.
//!
//! ## Overview
//!
//! The `net` module provides a testable interface for network operations.
//! It includes implementations for both real network interactions and mocked network operations for testing purposes.
//!
//! ## Key methods and types:
//!
//! - [kxio::net::new()][new()]: Creates a new `Net` instance
//! - [kxio::net::mock()][mock()]: Creates a new `MockNet` instance for use in tests
//! - [Error]: enum for network-related errors
//! - [Result]: an alias for `core::result::Result<T, Error>`
//! - [Net]: struct for real and mocked network operations
//! - [MockNet]: struct for defining behaviours of mocked network operations
//!
//! ## Usage
//!
//! Write your program to take a reference to [Net].
//!
//! Use the [Net::client] functionto create a [reqwest::RequestBuilder] which you should then pass to the [Net::send] method.
//! This is rather than building the request and calling its own `send` method, doing so would result in the network request being sent, even under-test.
//!
//! ```rust
//! use kxio::net;
//! async fn get_example(net: &net::Net) -> net::Result<()> {
//! let response = net.send(net.client().get("https://example.com")).await?;
//! ///...
//! Ok(())
//! }
//! ```
//!
//! ### Real Network Operations
//!
//! In your production code you will want to make real network requests.
//!
//! Construct a [Net] using [kxio::net::new()][new()]. Then pass as a reference to your program.
//!
//! ```rust
//! use kxio::net;
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! let net = net::new();
//!
//! get_example(&net).await?;
//! # Ok(())
//! # }
//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
//! ```
//!
//! ### Mocked Network Operations
//!
//! In your tests you will want to mock your network requests and responses.
//!
//! Construct a [MockNet] using [kxio::net::mock()][mock()].
//!
//! ```rust
//! use kxio::net;
//! let mock_net = net::mock();
//! ```
//!
//! Create a [reqwest::Client] using [MockNet::client()].
//!
//! ```rust
//! # let mock_net = kxio::net::mock();
//! let client = mock_net.client();
//! // this is the same as:
//! let client = reqwest::Client::new();
//! ```
//!
//! Define the expected responses for each request, using the [MockNet::on],
//! that you expect you program to make during the test. You can choose what each request should be
//! matched against. The default is to the match when both the Method and Url are the same.
//!
//! ```rust
//! use kxio::net;
//! use kxio::net::MatchOn;
//!
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! # let mock_net = net::mock();
//! # let client = mock_net.client();
//! mock_net.on(http::Method::GET)
//! .url(url::Url::parse("https://example.com")?)
//! .respond(mock_net.response().status(200).body("Mocked response")?);
//! # mock_net.reset();
//! # Ok(())
//! # }
//! ```
//!
//! All [MatchOn] options:
//! - [MatchOn::Method] (default)
//! - [MatchOn::Url] (default)
//! - [MatchOn::Headers]
//! - [MatchOn::Body].
//!
//! Once you have defined all your expected responses, convert the [MockNet] into a [Net].
//!
//! ```rust
//! # use kxio::net;
//! # let mock_net = net::mock();
//! let net: net::Net = mock_net.into();
//! // or
//! # let mock_net = net::mock();
//! let net = net::Net::from(mock_net);
//! ```
//!
//! Now you can pass a reference to `net` to your program.
//!
//! ```rust
//! # use kxio::net;
//! # #[tokio::main]
//! # async fn main() -> net::Result<()> {
//! # let mock_net = net::mock();
//! # let net = net::Net::from(mock_net);
//! get_example(&net).await?;
//! # Ok(())
//! # }
//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())}
//! ```
//!
//! When your test is finished, the [MockNet] will check that all the expected requests were
//! actually made. If there were any missed, then the test will [panic].
//!
//! If you don't want this to happen, then call [MockNet::reset] before your test finishes.
//! You will need to recover the [MockNet] from the [Net].
//!
//! ```rust
//! # use kxio::net;
//! # let mock_net = net::mock();
//! # let net = net::Net::from(mock_net);
//! let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
//! mock_net.reset();
//! ````
//!
//! ## Error Handling
//!
//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum,
//! allowing for specific error handling related to network operations.
mod result;
mod system;
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()
}

49
src/net/result.rs Normal file
View file

@ -0,0 +1,49 @@
//
use derive_more::derive::From;
/// The Errors that may occur within [kxio::net][crate::net].
#[derive(Debug, From, derive_more::Display)]
pub enum Error {
/// The Errors that may occur when processing a `Request`.
///
/// Note: Errors may include the full URL used to make the `Request`. If the URL
/// contains sensitive information (e.g. an API key as a query parameter), be
/// sure to remove it ([`without_url`](reqwest::Error::without_url))
Reqwest(reqwest::Error),
/// The Errors that may occur when processing a `Request`.
///
/// The cause has been converted to a String.
Request(String),
Url(url::ParseError),
/// There was network request that doesn't match any that were expected
#[display("Unexpected request: {0}", 0.to_string())]
UnexpectedMockRequest(reqwest::Request),
/// There was an error accessing the list of expected requests.
RwLockLocked,
/// There was an error making a network request.
Http(http::Error),
/// Attempted to extract a [MockNet][super::MockNet] from a [Net][super::Net] that does not contain one.
NetIsNotAMock,
}
impl std::error::Error for Error {}
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 within [kxio::net][crate::net].
///
/// Any failure is related to `std::io`, a Path Traversal
/// (i.e. trying to escape the base of the `FileSystem`),
/// or attempting to use a file as a directory or /vise versa/.
pub type Result<T> = core::result::Result<T, Error>;

302
src/net/system.rs Normal file
View file

@ -0,0 +1,302 @@
//
use std::cell::RefCell;
use reqwest::{Body, Client};
use super::{Error, Result};
/// A list of planned requests and responses
type Plans = Vec<Plan>;
/// The different ways to match a request.
#[derive(Debug, PartialEq, Eq)]
pub enum MatchOn {
/// The request must have a specific HTTP Request Method.
Method,
/// The request must have a specific URL.
Url,
/// The request must have a specify HTTP Body.
Body,
/// The request must have a specific set of HTTP Headers.
Headers,
}
/// A planned request and the response to return
///
/// Contains a list of the criteria that a request must meet before being considered a match.
struct Plan {
match_request: Vec<MatchRequest>,
response: reqwest::Response,
}
impl Plan {
fn matches(&self, request: &reqwest::Request) -> bool {
self.match_request.iter().all(|criteria| match criteria {
MatchRequest::Method(method) => request.method() == method,
MatchRequest::Url(uri) => request.url() == uri,
MatchRequest::Header { name, value } => {
request
.headers()
.iter()
.any(|(request_header_name, request_header_value)| {
let Ok(request_header_value) = request_header_value.to_str() else {
return false;
};
request_header_name.as_str() == name && request_header_value == value
})
}
MatchRequest::Body(body) => {
request.body().and_then(Body::as_bytes) == Some(body.as_bytes())
}
})
}
}
/// An abstraction for the network
pub struct Net {
plans: Option<RefCell<Plans>>,
}
impl Net {
/// Creates a new unmocked [Net] for creating real network requests.
pub(super) const fn new() -> Self {
Self { plans: None }
}
/// Creats a new [MockNet] for use in tests.
pub(super) const fn mock() -> MockNet {
MockNet {
plans: RefCell::new(vec![]),
}
}
}
impl Net {
/// Helper to create a default [reqwest::Client].
///
/// # Example
///
/// ```rust
/// # use kxio::net::Result;
/// let net = kxio::net::new();
/// let client = net.client();
/// let request = client.get("https://hyper.rs");
/// ```
pub fn client(&self) -> reqwest::Client {
Default::default()
}
/// Constructs the Request and sends it to the target URL, returning a
/// future Response.
///
/// # Errors
///
/// This method fails if there was an error while sending request,
/// redirect loop was detected or redirect limit was exhausted.
///
/// # Example
///
/// ```no_run
/// # use kxio::net::Result;
/// # async fn run() -> Result<()> {
/// let net = kxio::net::new();
/// let request = net.client().get("https://hyper.rs");
/// let response = net.send(request).await?;
/// # Ok(())
/// # }
/// ```
pub async fn send(
&self,
request: impl Into<reqwest::RequestBuilder>,
) -> Result<reqwest::Response> {
let Some(plans) = &self.plans else {
return request.into().send().await.map_err(Error::from);
};
let request = request.into().build()?;
let index = plans
.borrow()
.iter()
.position(|plan| plan.matches(&request));
match index {
Some(i) => {
let response = plans.borrow_mut().remove(i).response;
Ok(response)
}
None => Err(Error::UnexpectedMockRequest(request)),
}
}
}
impl TryFrom<Net> for MockNet {
type Error = super::Error;
fn try_from(net: Net) -> std::result::Result<Self, Self::Error> {
match &net.plans {
Some(plans) => Ok(MockNet {
plans: RefCell::new(plans.take()),
}),
None => Err(Self::Error::NetIsNotAMock),
}
}
}
/// A struct for defining the expected requests and their responses that should be made
/// during a test.
///
/// When the [MockNet] goes out of scope it will verify that all expected requests were consumed,
/// otherwise it will `panic`.
///
/// # Example
///
/// ```rust
/// # use kxio::net::Result;
/// # fn run() -> Result<()> {
/// let mock_net = kxio::net::mock();
/// let client = mock_net.client();
/// // define an expected requet, and the response that should be returned
/// mock_net.on(http::Method::GET)
/// .url(url::Url::parse("https://hyper.rs")?)
/// .respond(mock_net.response().status(200).body("Ok")?);
/// let net: kxio::net::Net = mock_net.into();
/// // use 'net' in your program, by passing it as a reference
///
/// // In some rare cases you don't want to assert that all expected requests were made.
/// // You should recover the `MockNet` from the `Net` and `MockNet::reset` it.
/// let mock_net = kxio::net::MockNet::try_from(net)?;
/// mock_net.reset(); // only if explicitly needed
/// # Ok(())
/// # }
/// ```
pub struct MockNet {
plans: RefCell<Plans>,
}
impl MockNet {
/// Helper to create a default [reqwest::Client].
///
/// # Example
///
/// ```rust
/// let mock_net = kxio::net::mock();
/// let client = mock_net.client();
/// let request = client.get("https://hyper.rs");
/// ```
pub fn client(&self) -> Client {
Default::default()
}
/// Specify an expected request.
///
/// # Example
///
/// ```rust
/// # use kxio::net::Result;
/// # fn run() -> Result<()> {
/// let mock_net = kxio::net::mock();
/// let client = mock_net.client();
/// mock_net.on(http::Method::GET)
/// .url(url::Url::parse("https://hyper.rs")?)
/// .respond(mock_net.response().status(200).body("Ok")?);
/// # Ok(())
/// # }
/// ```
pub fn on(&self, method: impl Into<http::Method>) -> WhenRequest {
WhenRequest::new(self, method)
}
fn _when(&self, plan: Plan) {
self.plans.borrow_mut().push(plan);
}
/// Creates a [http::response::Builder] to be extended and returned by a mocked network request.
pub fn response(&self) -> http::response::Builder {
Default::default()
}
/// Clears all the expected requests and responses from the [MockNet].
///
/// When the [MockNet] goes out of scope it will assert that all expected requests and
/// responses were consumed. If there are any left unconsumed, then it will `panic`.
///
/// # Example
///
/// ```rust
/// # use kxio::net::Result;
/// # fn run() -> Result<()> {
/// # let mock_net = kxio::net::mock();
/// # let net: kxio::net::Net = mock_net.into();
/// let mock_net = kxio::net::MockNet::try_from(net)?;
/// mock_net.reset(); // only if explicitly needed
/// # Ok(())
/// # }
/// ```
pub fn reset(&self) {
self.plans.take();
}
}
impl From<MockNet> for Net {
fn from(mock_net: MockNet) -> Self {
Self {
// keep the original `inner` around to allow it's Drop impelmentation to run when we go
// out of scope at the end of the test
plans: Some(RefCell::new(mock_net.plans.take())),
}
}
}
impl Drop for MockNet {
fn drop(&mut self) {
assert!(self.plans.borrow().is_empty())
}
}
impl Drop for Net {
fn drop(&mut self) {
if let Some(plans) = &self.plans {
assert!(plans.borrow().is_empty())
}
}
}
pub enum MatchRequest {
Method(http::Method),
Url(reqwest::Url),
Header { name: String, value: String },
Body(String),
}
pub struct WhenRequest<'net> {
net: &'net MockNet,
match_on: Vec<MatchRequest>,
}
impl<'net> WhenRequest<'net> {
pub fn url(mut self, url: impl Into<reqwest::Url>) -> Self {
self.match_on.push(MatchRequest::Url(url.into()));
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.match_on.push(MatchRequest::Header {
name: name.into(),
value: value.into(),
});
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.match_on.push(MatchRequest::Body(body.into()));
self
}
pub fn respond<T>(self, response: http::Response<T>)
where
T: Into<reqwest::Body>,
{
self.net._when(Plan {
match_request: self.match_on,
response: response.into(),
});
}
fn new(net: &'net MockNet, method: impl Into<http::Method>) -> Self {
Self {
net,
match_on: vec![MatchRequest::Method(method.into())],
}
}
}

View file

@ -1,795 +0,0 @@
#![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(())
}
}

View file

@ -1,36 +0,0 @@
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>;

View file

@ -1,66 +0,0 @@
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
}
}

View file

@ -1,462 +0,0 @@
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");
}
}

View file

@ -1,102 +0,0 @@
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");
}
}

View file

@ -1,38 +0,0 @@
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
}
}

View file

@ -1,174 +0,0 @@
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>;
}

View file

@ -1,56 +0,0 @@
#[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>>();
}
}

View file

@ -1,612 +0,0 @@
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());
}
}

View file

@ -1,86 +0,0 @@
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");
}
}

View file

@ -1,26 +0,0 @@
#[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"),
}
}
}

View file

@ -1,22 +0,0 @@
#[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
}
}

View file

@ -1,113 +0,0 @@
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);
}
}

14
src/result.rs Normal file
View file

@ -0,0 +1,14 @@
//
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>;

View file

@ -1,12 +0,0 @@
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn write_read_file_exists() -> TestResult {
let fs = crate::filesystem::temp()?;
let pathbuf = fs.write_file("foo", "content")?;
let c = fs.read_file("foo")?;
assert_eq!(c, "content");
assert!(fs.file_exists(&pathbuf));
Ok(())
}

View file

@ -1,294 +0,0 @@
use assert2::let_assert;
use crate::fs;
type TestResult = Result<(), fs::Error>;
mod path_of {
use super::*;
#[test]
fn validate_fails_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let_assert!(Err(fs::Error::PathTraversal { base, path: _path }) = fs.path_of("..".into()));
assert_eq!(base, fs.base());
Ok(())
}
}
mod file {
use super::*;
#[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()?;
let pathbuf = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&pathbuf, "content"));
let_assert!(
Ok(c) = fs.file_read_to_string(&pathbuf),
"file_read_to_string"
);
assert_eq!(c, "content");
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists);
let_assert!(Ok(is_file) = fs.path_is_file(&pathbuf));
assert!(is_file);
Ok(())
}
}
mod dir_create {
use super::*;
#[test]
fn should_create_a_dir() -> TestResult {
let fs = fs::temp()?;
let pathbuf = fs.base().join("subdir");
let_assert!(Ok(_) = fs.dir_create(&pathbuf));
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists);
let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf));
assert!(is_dir);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_create(&path)
);
Ok(())
}
}
mod dir_create_all {
use super::*;
#[test]
fn should_create_a_dir() -> TestResult {
let fs = fs::temp()?;
let pathbuf = fs.base().join("subdir").join("child");
let_assert!(Ok(_) = fs.dir_create_all(&pathbuf));
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists, "path exists");
let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf));
assert!(is_dir, "path is a directory");
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_create_all(&path)
);
Ok(())
}
}
mod dir_dir_read {
use crate::fs::DirItem;
use super::*;
#[test]
fn should_return_dir_items() -> TestResult {
let fs = fs::temp()?;
let file1 = fs.base().join("file-1");
let dir = fs.base().join("dir");
let file2 = dir.join("file-2");
fs.file_write(&file1, "file-1")?;
fs.dir_create(&dir)?;
fs.file_write(&file2, "file-2")?;
let items = fs
.dir_read(fs.base())?
.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()?;
let path = fs.base().join("file");
fs.file_write(&path, "contents")?;
let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir_read(&path));
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_read(&path)
);
Ok(())
}
}
mod path_exists {
use super::*;
#[test]
fn should_be_true_when_it_exists() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(exists) = fs.path_exists(&path));
assert!(exists);
Ok(())
}
#[test]
fn should_be_false_when_it_does_not_exist() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(exists) = fs.path_exists(&path));
assert!(!exists);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_exists(&path)
);
Ok(())
}
}
mod path_is_dir {
use super::*;
#[test]
fn should_be_true_when_is_a_dir() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.dir_create(&path));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(is_dir);
Ok(())
}
#[test]
fn should_be_false_when_is_a_file() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(!is_dir);
Ok(())
}
#[test]
#[ignore]
fn should_be_false_when_is_a_link() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
// TODO create a link
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(!is_dir);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_is_dir(&path)
);
Ok(())
}
}
mod path_is_file {
use super::*;
#[test]
fn should_be_true_when_is_a_file() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(is_file);
Ok(())
}
#[test]
fn should_be_false_when_is_a_dir() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.dir_create(&path));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(!is_file);
Ok(())
}
#[test]
#[ignore]
fn should_be_false_when_is_a_link() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
// TODO create a link
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(!is_file);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_is_file(&path)
);
Ok(())
}
}

View file

@ -1,8 +0,0 @@
#[cfg(feature = "fs")]
pub mod filesystem;
#[cfg(feature = "fs")]
pub mod fs;
// #[cfg(feature = "network")]
// pub mod network;

920
tests/fs.rs Normal file
View file

@ -0,0 +1,920 @@
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(())
}
}

269
tests/net.rs Normal file
View file

@ -0,0 +1,269 @@
use assert2::let_assert;
use http::Method;
//
use kxio::net::{Error, MockNet, Net};
use reqwest::Url;
#[tokio::test]
async fn test_get_url() {
//given
let mock_net = kxio::net::mock();
let client = mock_net.client();
let url = "https://www.example.com";
let my_response = mock_net
.response()
.status(200)
.body("Get OK")
.expect("body");
mock_net
.on(Method::GET)
.url(Url::parse(url).expect("parse url"))
.respond(my_response);
//when
let response = Net::from(mock_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 mock_net = kxio::net::mock();
let client = mock_net.client();
let url = "https://www.example.com";
let my_response = mock_net
.response()
.status(200)
.body("Get OK")
.expect("body");
mock_net
.on(Method::GET)
.url(Url::parse(url).expect("parse url"))
.respond(my_response);
let net = Net::from(mock_net);
//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
let mock_net = MockNet::try_from(net).expect("recover net");
mock_net.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 my_response = net.response().status(200).body("Post OK").expect("body");
net.on(Method::POST)
.url(Url::parse(url).expect("parse url"))
.respond(my_response);
//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 my_response = net.response().status(200).body("").expect("response body");
net.on(Method::POST)
// NOTE: No URL specified - so shou∂ match any URL
.respond(my_response);
//when
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"), "");
}
#[tokio::test]
async fn test_post_by_body() {
//given
let net = kxio::net::mock();
let client = net.client();
let my_response = net
.response()
.status(200)
.body("response body")
.expect("body");
net.on(Method::POST)
// No URL - so any POST with a matching body
.body("match on body")
.respond(my_response);
//when
let response = Net::from(net)
.send(client.post("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_header() {
//given
let net = kxio::net::mock();
let client = net.client();
let my_response = net
.response()
.status(200)
.body("response body")
.expect("body");
net.on(Method::POST)
.header("test", "match")
.respond(my_response);
//when
let response = Net::from(net)
.send(
client
.post("https://some.other.url")
.body("nay 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]
async fn test_post_by_header_wrong_value() {
//given
let mock_net = kxio::net::mock();
let client = mock_net.client();
let my_response = mock_net
.response()
.status(200)
.body("response body")
.expect("body");
mock_net
.on(Method::POST)
.header("test", "match")
.respond(my_response);
let net = Net::from(mock_net);
//when
let response = net
.send(
client
.post("https://some.other.url")
.body("nay body")
.header("test", "no match"),
)
.await;
//then
let_assert!(Err(kxio::net::Error::UnexpectedMockRequest(_)) = response);
MockNet::try_from(net).expect("recover mock").reset();
}
#[tokio::test]
#[should_panic]
async fn test_unused_post_as_net() {
//given
let mock_net = kxio::net::mock();
let url = "https://www.example.com";
let my_response = mock_net
.response()
.status(200)
.body("Post OK")
.expect("body");
mock_net
.on(Method::POST)
.url(Url::parse(url).expect("prase url"))
.respond(my_response);
let _net = Net::from(mock_net);
//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
}
#[tokio::test]
#[should_panic]
async fn test_unused_post_as_mocknet() {
//given
let mock_net = kxio::net::mock();
let url = "https://www.example.com";
let my_response = mock_net
.response()
.status(200)
.body("Post OK")
.expect("body");
mock_net
.on(Method::POST)
.url(Url::parse(url).expect("parse url"))
.respond(my_response);
//when
// don't send the planned request
// let _response = Net::from(net).send(client.post(url)).await.expect("send");
//then
// Drop implementation for mock_net should panic
}