pax_global_header00006660000000000000000000000064146303721550014520gustar00rootroot0000000000000052 comment=ad6b4aa01635c92f307c088b4540fb9c64d05f67 rust-hypothesis-0.11.5/000077500000000000000000000000001463037215500147765ustar00rootroot00000000000000rust-hypothesis-0.11.5/.github/000077500000000000000000000000001463037215500163365ustar00rootroot00000000000000rust-hypothesis-0.11.5/.github/workflows/000077500000000000000000000000001463037215500203735ustar00rootroot00000000000000rust-hypothesis-0.11.5/.github/workflows/cd.yml000066400000000000000000000043611463037215500215100ustar00rootroot00000000000000name: Continuous Deployment on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' jobs: publish: name: Publishing for ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ macos-latest, ubuntu-latest, windows-latest ] rust: [ stable ] include: - os: macos-latest artifact_prefix: macos target: x86_64-apple-darwin binary_postfix: "" - os: ubuntu-latest artifact_prefix: linux target: x86_64-unknown-linux-gnu binary_postfix: "" - os: windows-latest artifact_prefix: windows target: x86_64-pc-windows-msvc binary_postfix: ".exe" steps: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} targets: "x86_64-apple-darwin, x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc" - uses: actions/checkout@v2 - run: cargo build --release --target ${{ matrix.target }} - name: Packaging final binary shell: bash run: | cd target/${{ matrix.target }}/release strip hypothesis${{ matrix.binary_postfix }} tar czvf hypothesis-${{ matrix.artifact_prefix }}.tar.gz hypothesis${{ matrix.binary_postfix }} if [[ ${{ runner.os }} == 'Windows' ]]; then certutil -hashfile hypothesis-${{ matrix.artifact_prefix }}.tar.gz sha256 | grep -E [A-Fa-f0-9]{64} > hypothesis-${{ matrix.artifact_prefix }}.sha256 else shasum -a 256 hypothesis-${{ matrix.artifact_prefix }}.tar.gz > hypothesis-${{ matrix.artifact_prefix }}.sha256 fi - name: Releasing assets uses: softprops/action-gh-release@v1 with: files: | target/${{ matrix.target }}/release/hypothesis-${{ matrix.artifact_prefix }}.tar.gz target/${{ matrix.target }}/release/hypothesis-${{ matrix.artifact_prefix }}.sha256 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-cargo: name: Publishing to Cargo runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: dtolnay/rust-toolchain@stable - run: cargo publish --token ${{ secrets.CARGO_API_KEY }} --allow-dirty rust-hypothesis-0.11.5/.github/workflows/ci.yml000066400000000000000000000012671463037215500215170ustar00rootroot00000000000000on: [ push, pull_request ] name: Continuous Integration jobs: # test: # name: Test Suite # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v2 # - uses: dtolnay/rust-toolchain@stable # - run: cargo test rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@stable with: components: clippy - run: cargo clippy rust-hypothesis-0.11.5/CHANGELOG.md000066400000000000000000000053471463037215500166200ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 0.11.4 - 2024-06-06 TextPositionSelector is sometimes empty, went back to hashmaps ## 0.11.3 - 2024-06-04 Ignore empty fields in search query ## 0.11.2 - 2024-05-27 Fix [#10](https://github.com/out-of-cheese-error/rust-hypothesis/issues/10) and add new selectors ## 0.11.1 - 2023-12-31 Fix import for no cli feature ## 0.11.0 - 2023-12-31 - Allow specifying multiple groups in search - Updated dependencies ## 0.10.4 - 2022-08-25 Updated dependencies ## 0.10.3 - 2022-04-30 Updated rust edition, dependencies, lints ## 0.10.2 - 2021-04-13 ### Changed Added serde error and raw text to `APIError` for easier debugging ## 0.10.0 - 2021-04-13 ### Fixed Added all [w3 selectors](https://www.w3.org/TR/annotation-model/#selectors) to `Selector` enum ## 0.9.1 - 2021-03-26 Updated dependencies ## 0.9.0 - 2021-03-26 Added `Document` to `Annotation` output struct ## 0.8.0 - 2021-01-16 Updated dependencies ## 0.7.2 - 2020-11-28 Fixed typo: wildcard-uri -> wildcard_uri ## 0.7.1 - 2020-09-03 Added `builder` methods to generate Builders (https://matklad.github.io/2020/08/12/who-builds-the-builder.html) ## 0.7.0 - 2020-09-03 Added `search_annotations_return_all` which uses a loop to bypass the limit for number of annotations returned ## 0.6.0 - 2020-06-23 ### Changed * Library exposes `HypothesisError` instead of using `eyre` ### Fixed * File creation bug (expected file to exist) * `display_name` can be null now ## 0.5.0 - 2020-06-06 ### Changed * Update takes Annotation as input instead of InputAnnotation * SearchQuery takes String as input, i.e. input needs to already be formatted as acct:{username}@hypothes.is ### Added * Happy path tests for `annotations` and `groups` CLI ## 0.4.0 - 2020-06-02 ### Changed * Switched `AnnotationID` and `GroupID` back to Strings and &str * Renamed `AnnotationMaker` to `InputAnnotation` ### Added * made Builders for `InputAnnotation`, `Target, Document`, and `SearchQuery` using `derive_builder` * better docs ## 0.3.0 - 2020-05-31 * everything is asynchronous. * added a bulk API for modifying many things at once. * `AnnotationMaker` tags are optional to allow for removing a tag during update ## 0.2.0 - 2020-05-28 Works both as a crate and a binary now! ### Added * a CLI version under a feature flag "cli" * Better documentation ### Changed License to BSD-2-Clause (consistent with hypothesis/h) ## 0.1.0 - 2020-05-27 First version! Complete API for annotations, groups and profile. Some missing docs and weird edges, listed in Caveats/Todos in the README.rust-hypothesis-0.11.5/Cargo.lock000066400000000000000000001266531463037215500167200ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "assert_cmd" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "bytes" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", "windows-targets 0.48.5", ] [[package]] name = "clap" version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_complete" version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a51919c5608a32e34ea1d6be321ad070065e17613e168c5b6977024290f2630b" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "color-eyre" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" dependencies = [ "backtrace", "color-spantrace", "eyre", "indenter", "once_cell", "owo-colors", "tracing-error", ] [[package]] name = "color-spantrace" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" dependencies = [ "once_cell", "owo-colors", "tracing-core", "tracing-error", ] [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "darling" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn 1.0.92", ] [[package]] name = "darling_macro" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" dependencies = [ "darling_core", "quote", "syn 1.0.92", ] [[package]] name = "derive_builder" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling", "proc-macro2", "quote", "syn 1.0.92", ] [[package]] name = "derive_builder_macro" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ "derive_builder_core", "syn 1.0.92", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" dependencies = [ "cfg-if 0.1.10", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "eyre" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", ] [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if 1.0.0", "libc", "wasi", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes 1.0.1", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" dependencies = [ "libc", ] [[package]] name = "http" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" dependencies = [ "bytes 0.5.6", "fnv", "itoa 0.4.6", ] [[package]] name = "http-body" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" dependencies = [ "bytes 1.0.1", "http", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes 1.0.1", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper-rustls" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", "hyper", "rustls", "tokio", "tokio-rustls", ] [[package]] name = "hypothesis" version = "0.11.5" dependencies = [ "assert_cmd", "chrono", "clap", "clap_complete", "color-eyre", "derive_builder", "dotenv", "eyre", "futures", "predicates", "reqwest", "serde", "serde_json", "thiserror", "tokio", "url", ] [[package]] name = "iana-time-zone" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" dependencies = [ "android_system_properties", "core-foundation-sys", "js-sys", "wasm-bindgen", "winapi", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indenter" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bd112d44d9d870a6819eb505d04dd92b5e4d94bb8c304924a0872ae7016fb5" [[package]] name = "indexmap" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "ipnet" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itoa" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "itoa" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "js-sys" version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "log" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ "cfg-if 0.1.10", ] [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "owo-colors" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "predicates" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", "float-cmp", "itertools", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a293318316cf6478ec1ad2a21c49390a8d5b5eae9fab736467d93fbc0edc29c5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" dependencies = [ "aho-corasick", "memchr", "regex-syntax", "thread_local", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" [[package]] name = "regex-syntax" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "reqwest" version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", "bytes 1.0.1", "encoding_rs", "futures-core", "futures-util", "h2", "http", "http-body", "hyper", "hyper-rustls", "ipnet", "js-sys", "log", "mime", "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", "tokio", "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "winreg", ] [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin 0.5.2", "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "ring" version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", "windows-sys 0.48.0", ] [[package]] name = "rustc-demangle" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustls" version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.7", "rustls-webpki", "sct", ] [[package]] name = "rustls-pemfile" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64 0.13.0", ] [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring 0.17.7", "untrusted 0.9.0", ] [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "sct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring 0.16.20", "untrusted 0.7.1", ] [[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "serde_json" version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "sharded-slab" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" dependencies = [ "lazy_static", ] [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "socket2" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "syn" version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "system-configuration" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "thread_local" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ "once_cell", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes 1.0.1", "libc", "mio", "num_cpus", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" dependencies = [ "bytes 1.0.1", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "tower-service" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-error" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ "tracing", "tracing-subscriber", ] [[package]] name = "tracing-subscriber" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ "sharded-slab", "thread_local", "tracing-core", ] [[package]] name = "try-lock" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "unicode-bidi" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ "log", "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 1.0.92", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" dependencies = [ "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", "syn 1.0.92", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be2398f326b7ba09815d0b403095f34dd708579220d099caae89be0b32137b2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.0", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if 1.0.0", "windows-sys 0.48.0", ] rust-hypothesis-0.11.5/Cargo.toml000066400000000000000000000024511463037215500167300ustar00rootroot00000000000000[package] name = "hypothesis" version = "0.11.5" authors = ["Ninjani"] edition = "2021" description = "a Rust wrapper and CLI for the Hypothesis API" repository = "https://github.com/out-of-cheese-error/rust-hypothesis" readme = "README.md" license = "MIT" keywords = ["hypothesis", "annotation", "api", "cli"] categories = ["api-bindings", "command-line-utilities"] [features] default = ["cli"] # Feature required for hypothesis the CLI application. # Disable (set default-features=false) if using as a Rust crate. cli = [ "clap", "clap_complete", "eyre", "color-eyre" ] [dependencies] # For CLI eyre = { version = "0.6.11", optional = true } color-eyre = { version = "0.6.2", optional = true } clap = { version = "4.4.12", features = ["derive", "env"], optional = true } clap_complete = { version = "4.4.5", optional = true } # API calls reqwest = { version = "0.11.23", features = ["json", "rustls-tls"], default-features = false } tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } futures = "0.3.30" thiserror = "1.0.53" chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" derive_builder = "0.12.0" url = "2.5.0" [dev-dependencies] assert_cmd = "2.0.12" predicates = "3.0.4" dotenv = "0.15.0" rust-hypothesis-0.11.5/LICENSE000066400000000000000000000024561463037215500160120ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2020, OutOfCheeseError All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. rust-hypothesis-0.11.5/README.md000066400000000000000000000101551463037215500162570ustar00rootroot00000000000000 [![Crates.io](https://img.shields.io/crates/v/hypothesis.svg)](https://crates.io/crates/hypothesis) [![Docs.rs](https://docs.rs/hypothesis/badge.svg)](https://docs.rs/hypothesis) [![CI](https://github.com/out-of-cheese-error/rust-hypothesis/workflows/Continuous%20Integration/badge.svg)](https://github.com/out-of-cheese-error/rust-hypothesis/actions) [![GitHub release](https://img.shields.io/github/release/out-of-cheese-error/rust-hypothesis.svg)](https://GitHub.com/out-of-cheese-error/rust-hypothesis/releases/) [![dependency status](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis/status.svg)](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis) # A Rust API for [Hypothesis](https://web.hypothes.is/) ## Description A lightweight wrapper and CLI for the [Hypothesis Web API v1.0.0](https://h.readthedocs.io/en/latest/api-reference/v1/). It includes all APIKey authorized endpoints related to * annotations (create / update / delete / search / fetch / flag), * groups (create / update / list / fetch / leave / members) * profile (user information / groups) ## Installation and Usage ### Authorization You'll need a [Hypothesis](https://hypothes.is) account, and a personal API token obtained as described [here](https://h.readthedocs.io/en/latest/api/authorization/). Set the environment variables `$HYPOTHESIS_NAME` and `$HYPOTHESIS_KEY` to your username and the developer API key respectively. ### As a command-line utility: ```bash cargo install hypothesis ``` Run `hypothesis --help` to see subcommands and options. NOTE: the CLI doesn't currently have all the capabilities of the Rust crate, specifically bulk actions and updating dates are not supported. Generate shell completions: ```bash hypothesis complete zsh > .oh-my-zsh/completions/_hypothesis exec zsh ``` ### As a Rust crate Add to your Cargo.toml: ```toml [dependencies] hypothesis = {version = "0.4.0", default-features = false} tokio = { version = "0.2", features = ["macros"] } ``` #### Examples ```rust no_run use hypothesis::Hypothesis; use hypothesis::annotations::{InputAnnotation, Target, Selector}; #[tokio::main] async fn main() -> Result<(), hypothesis::errors::HypothesisError> { let api = Hypothesis::from_env()?; let new_annotation = InputAnnotation::builder() .uri("https://www.example.com") .text("this is a comment") .target(Target::builder() .source("https://www.example.com") .selector(vec![Selector::new_quote("exact text in website to highlight", "prefix of text", "suffix of text")]) .build()?) .tags(vec!["tag1".to_string(), "tag2".to_string()]) .build()?; api.create_annotation(&new_annotation).await?; Ok(()) } ``` See the documentation of the API struct ([`Hypothesis`](https://docs.rs/crate/hypothesis/struct.Hypothesis.html)) for a list of possible queries. Use bulk functions to perform multiple actions - e.g. `api.fetch_annotations` instead of a loop around `api.fetch_annotation`. Check the [documentation](https://docs.rs/crate/hypothesis) for more usage examples. ### Changelog See the [CHANGELOG](CHANGELOG.md) ### Contributing Make sure you have a .env file (added to .gitignore) in the repo root with HYPOTHESIS_NAME, HYPOTHESIS_KEY, and TEST_GROUP_ID ### Caveats / Todo: - Only supports APIKey authorization and hypothes.is authority (i.e. single users). - `Target.selector.RangeSelector` doesn't seem to follow [W3C standards](https://www.w3.org/TR/annotation-model/#range-selector). It's just a hashmap for now. - `Annotation` hypermedia links are stored as a hashmap, b/c I don't know all the possible values. - Need to figure out how `Document` works to properly document it (hah). - Can't delete a group after making it, can leave it though (maybe it's the same thing?) - No idea what `UserProfile.preferences` and `UserProfile.features` mean. - CLI just dumps output as JSON, this is fine right? Fancier CLIs can build on top of this (or use the crate directly) rust-hypothesis-0.11.5/src/000077500000000000000000000000001463037215500155655ustar00rootroot00000000000000rust-hypothesis-0.11.5/src/annotations.rs000066400000000000000000000434571463037215500205050ustar00rootroot00000000000000//! Objects related to the "annotations" endpoint use std::collections::HashMap; use chrono::{DateTime, Utc}; #[cfg(feature = "cli")] use clap::Parser; #[cfg(feature = "cli")] use clap::ValueEnum; use serde::{Deserialize, Serialize}; use crate::{errors, is_default, UserAccountID}; #[cfg_attr(feature = "cli", derive(Parser))] #[cfg_attr( feature = "cli", clap( about = "Create an annotation", long_about = "Create and upload an annotation to your Hypothesis" ) )] /// Struct to create annotations /// /// All fields except uri are optional, i.e. leave as default. /// /// # Example /// ``` /// use hypothesis::annotations::{InputAnnotation, Target, Selector}; /// # #[tokio::main] /// # async fn main() -> Result<(), hypothesis::errors::HypothesisError> { /// // A simple annotation /// let annotation_simple = InputAnnotation::builder() /// .uri("https://www.example.com") /// .text("My new annotation").build()?; /// /// // A complex annotation /// let annotation_complex = InputAnnotation::builder() /// .uri("https://www.example.com") /// .text("this is a comment") /// .target(Target::builder().source("https://www.example.com") /// .selector(vec![Selector::new_quote("exact text in website to highlight", /// "prefix of text", /// "suffix of text")]).build()?) /// .tags(vec!["tag1".into(), "tag2".into()]) /// .build()?; /// # Ok(()) /// # } /// ``` #[derive(Serialize, Debug, Default, Clone, Builder, PartialEq)] #[builder(default, build_fn(name = "builder"))] pub struct InputAnnotation { /// URI that this annotation is attached to. /// /// Can be a URL (a web page address) or a URN representing another kind of resource such as /// DOI (Digital Object Identifier) or a PDF fingerprint. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = ""))] #[builder(setter(into))] pub uri: String, /// Annotation text / comment given by user /// /// This is NOT the selected text on the web-page #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub text: String, /// Tags attached to the annotation #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(long))] #[builder(setter(strip_option), default)] pub tags: Option>, /// Further metadata about the target document #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(skip))] #[builder(setter(strip_option), default)] pub document: Option, #[serde(skip_serializing_if = "is_default")] /// The unique identifier for the annotation's group. /// /// If an annotation is a reply to another /// annotation (see `references`), this field will be ignored — /// replies belong to the same group as their parent annotations. #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub group: String, /// Which part of the document does the annotation target? /// /// If left as default then the annotation is linked to the whole page. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(skip))] pub target: Target, /// Annotation IDs for any annotations this annotation references (e.g. is a reply to) #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(long))] pub references: Vec, } impl InputAnnotation { pub fn builder() -> InputAnnotationBuilder { InputAnnotationBuilder::default() } } impl InputAnnotationBuilder { /// Builds a new `InputAnnotation`. pub fn build(&self) -> Result { self.builder() .map_err(|e| errors::HypothesisError::BuilderError(e.to_string())) } } impl Annotation { pub fn update(&mut self, annotation: InputAnnotation) { if !annotation.uri.is_empty() { self.uri = annotation.uri; } if !annotation.text.is_empty() { self.text = annotation.text; } if let Some(tags) = annotation.tags { self.tags = tags; } if !annotation.group.is_empty() { self.group = annotation.group; } if annotation.references.is_empty() { self.references = annotation.references; } } } #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Builder)] #[builder(build_fn(name = "builder"))] pub struct Document { #[serde(skip_serializing_if = "is_default", default)] pub title: Vec, #[serde(skip_serializing_if = "is_default", default)] #[builder(setter(strip_option), default)] pub dc: Option, #[serde(skip_serializing_if = "is_default", default)] #[builder(setter(strip_option), default)] pub highwire: Option, #[serde(skip_serializing_if = "is_default", default)] pub link: Vec, } impl Document { pub fn builder() -> DocumentBuilder { DocumentBuilder::default() } } impl DocumentBuilder { /// Builds a new `Document`. pub fn build(&self) -> Result { self.builder() .map_err(|e| errors::HypothesisError::BuilderError(e.to_string())) } } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] pub struct HighWire { #[serde(skip_serializing_if = "is_default", default)] pub doi: Vec, #[serde(skip_serializing_if = "is_default", default)] pub pdf_url: Vec, } #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub struct Link { pub href: String, #[serde(skip_serializing_if = "is_default", rename = "type", default)] pub link_type: String, } #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub struct Dc { #[serde(skip_serializing_if = "is_default", default)] pub identifier: Vec, } /// Full representation of an Annotation resource and applicable relationships. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Annotation { /// Annotation ID pub id: String, /// Date of creation pub created: DateTime, /// Date of last update pub updated: DateTime, /// User account ID in the format "acct:@" pub user: UserAccountID, /// URL of document this annotation is attached to pub uri: String, /// The text content of the annotation body (NOT the selected text in the document) pub text: String, /// Tags attached to annotation pub tags: Vec, /// The unique identifier for the annotation's group pub group: String, pub permissions: Permissions, /// Which part of the document does the annotation target. pub target: Vec, /// An object containing hypermedia links for this annotation pub links: HashMap, /// Whether this annotation is hidden from public view pub hidden: bool, /// Whether this annotation has one or more flags for moderation pub flagged: bool, /// Document information #[serde(default)] pub document: Option, /// Annotation IDs for any annotations this annotation references (e.g. is a reply to) #[serde(default)] pub references: Vec, #[serde(default)] pub user_info: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct UserInfo { /// The annotation creator's display name pub display_name: Option, } /// > While the API accepts arbitrary Annotation selectors in the target.selector property, /// > the Hypothesis client currently supports TextQuoteSelector, RangeSelector and TextPositionSelector selector. /// [Hypothesis API v1.0.0](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/annotations/paths/~1annotations/post) #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Builder)] #[builder(build_fn(name = "builder"))] pub struct Target { /// The target URI for the annotation /// Leave empty when creating an annotation #[serde(skip_serializing_if = "is_default")] #[builder(setter(into))] pub source: String, /// An array of selectors that refine this annotation's target #[serde(default, skip_serializing_if = "is_default")] pub selector: Vec, } impl Target { pub fn builder() -> TargetBuilder { TargetBuilder::default() } } impl TargetBuilder { /// Builds a new `Target`. pub fn build(&self) -> Result { self.builder() .map_err(|e| errors::HypothesisError::BuilderError(e.to_string())) } } /// > Many Annotations refer to part of a resource, rather than all of it, as the Target. /// > We call that part of the resource a Segment (of Interest). A Selector is used to describe how /// > to determine the Segment from within the Source resource. /// [Web Annotation Data Model - Selectors](https://www.w3.org/TR/annotation-model/#selectors) #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "type")] pub enum Selector { TextQuoteSelector(TextQuoteSelector), /// > Selections made by users may be extensive and/or cross over internal boundaries in the /// > representation, making it difficult to construct a single selector that robustly describes /// > the correct content. A Range Selector can be used to identify the beginning and the end of /// > the selection by using other Selectors. In this way, two points can be accurately identified /// > using the most appropriate selection mechanisms, and then linked together to form the selection. /// > The selection consists of everything from the beginning of the starting selector through to the /// > beginning of the ending selector, but not including it. /// [Web Annotation Data Model - Range Selector](https://www.w3.org/TR/annotation-model/#range-selector) /// NOTE - the Hypothesis API doesn't seem to follow this standard for RangeSelector so this just returns a HashMap for now /// TODO: make Selectors into structs TextPositionSelector(HashMap), RangeSelector(HashMap), FragmentSelector(HashMap), CssSelector(HashMap), XPathSelector(HashMap), DataPositionSelector(HashMap), SvgSelector(HashMap), // See https://github.com/hypothesis/h/issues/7803: PageSelector(HashMap), EPUBContentSelector(HashMap), } impl Selector { pub fn new_quote(exact: &str, prefix: &str, suffix: &str) -> Self { Self::TextQuoteSelector(TextQuoteSelector { exact: exact.to_string(), prefix: prefix.to_string(), suffix: suffix.to_string(), }) } } /// > This Selector describes a range of text by copying it, and including some of the text /// > immediately before (a prefix) and after (a suffix) it to distinguish between multiple /// > copies of the same sequence of characters. /// /// > For example, if the document were again "abcdefghijklmnopqrstuvwxyz", one could select /// > "efg" by a prefix of "abcd", the match of "efg" and a suffix of "hijk". /// [Web Annotation Data Model - Text Quote Selector](https://www.w3.org/TR/annotation-model/#text-quote-selector) #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TextQuoteSelector { /// A copy of the text which is being selected, after normalization. pub exact: String, /// A snippet of text that occurs immediately before the text which is being selected. pub prefix: String, /// The snippet of text that occurs immediately after the text which is being selected. pub suffix: String, } #[cfg_attr(feature = "cli", derive(ValueEnum))] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Sort { Created, Updated, Id, Group, User, } impl Default for Sort { fn default() -> Self { Self::Updated } } #[cfg_attr(feature = "cli", derive(ValueEnum))] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Order { Asc, Desc, } impl Default for Order { fn default() -> Self { Self::Desc } } /// Options to filter and sort search results. See [the Hypothesis API docs](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/annotations/paths/~1search/get) for more details on using these fields #[cfg_attr(feature = "cli", derive(Parser))] #[derive(Serialize, Debug, Clone, PartialEq, Builder, Default)] #[builder(build_fn(name = "builder"), default)] pub struct SearchQuery { /// The maximum number of annotations to return. /// /// Default: 20. Range: [ 0 .. 200 ] #[builder(default = "20")] #[cfg_attr(feature = "cli", clap(default_value = "20", long))] pub limit: u8, /// The field by which annotations should be sorted /// One of created, updated, id, group, user /// /// Default: updated #[cfg_attr(feature = "cli", clap(default_value = "updated", long, value_parser = clap::builder::EnumValueParser::::new()))] pub sort: Sort, /// Example: "2019-01-03T19:46:09.334Z" /// /// Define a start point for a subset (page) of annotation search results. /// NOTE: make sure to set sort to `Sort::Asc` if using `search_after` #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub search_after: String, /// The number of initial annotations to skip in the result set. /// /// Default: 0. Range: <= 9800. /// search_after is more efficient. #[cfg_attr(feature = "cli", clap(default_value = "0", long))] pub offset: usize, /// The order in which the results should be sorted. /// One of asc, desc /// /// Default: desc #[cfg_attr(feature = "cli", clap(default_value = "desc", long, value_parser = clap::builder::EnumValueParser::::new()))] pub order: Order, /// Limit the results to annotations matching the specific URI or equivalent URIs. /// /// URI can be a URL (a web page address) or a URN representing another kind of resource such /// as DOI (Digital Object Identifier) or a PDF fingerprint. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub uri: String, /// Limit the results to annotations containing the given keyword (tokenized chunk) in the URI. /// The value must exactly match an individual URI keyword. /// #[serde(rename = "uri.parts", skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub uri_parts: String, /// Limit the results to annotations whose URIs match the wildcard pattern. #[serde(rename = "wildcard_uri", skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub wildcard_uri: String, /// Limit the results to annotations made by the specified user. (in the format `acct:@`) #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub user: String, /// Limit the results to annotations made in the specified group (by group ID). /// This can be specified multiple times to retrieve multiple groups. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(long))] #[builder(setter(into))] pub group: Vec, /// Limit the results to annotations tagged with the specified value. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub tag: String, /// Similar to tag but allows a list of multiple tags. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(long))] pub tags: Vec, /// Limit the results to annotations who contain the indicated keyword in any of the following fields: /// `quote`, `tags`, `text`, `url` #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub any: String, /// Limit the results to annotations that contain this text inside the text that was annotated. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub quote: String, /// Returns annotations that are replies to this parent annotation ID. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub references: String, /// Limit the results to annotations that contain this text in their textual body. #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] #[builder(setter(into))] pub text: String, } impl SearchQuery { pub fn builder() -> SearchQueryBuilder { SearchQueryBuilder::default() } } impl SearchQueryBuilder { /// Builds a new `SearchQuery`. pub fn build(&self) -> Result { self.builder() .map_err(|e| errors::HypothesisError::BuilderError(e.to_string())) } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Permissions { pub read: Vec, pub delete: Vec, pub admin: Vec, pub update: Vec, } rust-hypothesis-0.11.5/src/cli.rs000066400000000000000000000374441463037215500167160ustar00rootroot00000000000000//! Objects related to the command-line tool use crate::annotations::InputAnnotation; use crate::annotations::{Order, SearchQuery, Sort}; use crate::errors::CLIError; use crate::groups::{Expand, GroupFilters}; use crate::Hypothesis; use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use std::io::Write; use std::path::PathBuf; use std::str::FromStr; use std::{fs, io}; #[derive(Debug, Parser)] #[clap( name = "hypothesis", about = "Call the Hypothesis API from the comfort of your terminal" )] pub enum HypothesisCLI { /// Manage annotations Annotations { #[clap(subcommand)] cmd: AnnotationsCommand, }, /// Manage groups Groups { #[clap(subcommand)] cmd: GroupsCommand, }, /// Manage user profile Profile { #[clap(subcommand)] cmd: ProfileCommand, }, /// Generate shell completions Complete { #[clap(value_enum)] shell: Shell, }, } #[derive(Parser, Debug)] pub enum AnnotationsCommand { /// Create a new annotation (TODO: add Target somehow) Create { #[clap(flatten)] annotation: InputAnnotation, /// write created annotation to this file in JSON format #[clap(short = 'o', long)] file: Option, }, /// Update an existing annotation Update { /// unique ID of the annotation to update id: String, #[clap(flatten)] annotation: InputAnnotation, /// write updated annotation to this file in JSON format #[clap(short = 'o', long)] file: Option, }, /// Search for annotations with optional filters Search { #[clap(flatten)] query: SearchQuery, /// json file to write search results to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, /// Fetch annotation by ID Fetch { /// unique ID of the annotation to fetch id: String, /// json file to write annotation to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, /// Delete annotation by ID Delete { /// unique ID of the annotation to delete id: String, }, /// Flag an annotation /// /// Flag an annotation for review (moderation). The moderator of the group containing the /// annotation will be notified of the flag and can decide whether or not to hide the /// annotation. Note that flags persist and cannot be removed once they are set. Flag { /// unique ID of the annotation to flag id: String, }, /// Hide an annotation /// /// Hide an annotation. The authenticated user needs to have the moderate permission for the /// group that contains the annotation — this permission is granted to the user who created the group. Hide { /// unique ID of the annotation to hide id: String, }, /// Show an annotation /// /// Show/"un-hide" an annotation. The authenticated user needs to have the moderate permission /// for the group that contains the annotation—this permission is granted to the user who created the group. Show { /// unique ID of the annotation to show id: String, }, } #[derive(Parser, Debug)] pub enum GroupsCommand { /// Retrieve a list of applicable Groups, filtered by authority and target document (document_uri). /// Also retrieve user's private Groups. List { #[clap(flatten)] filters: GroupFilters, /// json file to write filtered groups to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, /// Create a new, private group for the currently-authenticated user. Create { /// group name name: String, /// group description description: Option, /// write created group to this file in JSON format #[clap(short = 'o', long)] file: Option, }, /// Fetch a single Group resource. Fetch { /// unique Group ID id: String, /// Expand the organization, scope, or both #[clap(long, short)] expand: Vec, /// write group to this file in JSON format #[clap(short = 'o', long)] file: Option, }, /// Update a Group resource. Update { /// unique Group ID id: String, /// new group name #[clap(long, short)] name: Option, /// new group description #[clap(long, short)] description: Option, /// write updated group to this file in JSON format #[clap(short = 'o', long)] file: Option, }, /// Fetch a list of all members (users) in a group. /// /// Returned user resource only contains public-facing user data. /// Authenticated user must have read access to the group. Does not require authentication for reading members of /// public groups. Returned members are unsorted. Members { /// unique Group ID id: String, /// json file to write groups members to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, /// Remove yourself from a group. Leave { id: String }, } #[derive(Parser, Debug)] pub enum ProfileCommand { /// Fetch profile information for the currently-authenticated user. User { /// json file to write user profile to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, /// Fetch the groups for which the currently-authenticated user is a member. Groups { /// json file to write groups to, writes to stdout if not given #[clap(short = 'o', long)] file: Option, }, } impl HypothesisCLI { pub async fn run(self, client: Hypothesis) -> color_eyre::Result<()> { match self { Self::Annotations { cmd } => match cmd { AnnotationsCommand::Create { annotation, file } => { let annotation = client.create_annotation(&annotation).await?; println!("Created annotation {}", annotation.id); if let Some(file) = file { let writer: Box = Box::new(fs::File::create(file)?); let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&annotation)?)?; } } AnnotationsCommand::Update { id, annotation, file, } => { let mut old_annotation = client.fetch_annotation(&id).await?; old_annotation.update(annotation); let annotation = client.update_annotation(&old_annotation).await?; println!("Updated annotation {}", annotation.id); if let Some(file) = file { let writer: Box = Box::new(fs::File::create(file)?); let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&annotation)?)?; } } AnnotationsCommand::Search { query, file } => { let annotations = client.search_annotations(&query).await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); for annotation in annotations { writeln!(buffered, "{}", serde_json::to_string(&annotation)?)?; } } AnnotationsCommand::Fetch { id, file } => { let annotation = client.fetch_annotation(&id).await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&annotation)?)?; } AnnotationsCommand::Delete { id } => { let deleted = client.delete_annotation(&id).await?; if deleted { println!("Deleted annotation {}", id); } else { println!("Couldn't delete annotation {}", id); } } AnnotationsCommand::Flag { id } => { client.flag_annotation(&id).await?; println!("Flagged annotation {}", id); } AnnotationsCommand::Hide { id } => { client.hide_annotation(&id).await?; println!("Hid annotation {}", id); } AnnotationsCommand::Show { id } => { client.show_annotation(&id).await?; println!("Unhid annotation {}", id); } }, Self::Groups { cmd } => match cmd { GroupsCommand::List { filters, file } => { let groups = client.get_groups(&filters).await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); for group in groups { writeln!(buffered, "{}", serde_json::to_string(&group)?)?; } } GroupsCommand::Create { name, description, file, } => { let group = client.create_group(&name, description.as_deref()).await?; println!("Created group {}", group.id); if let Some(file) = file { let writer: Box = Box::new(fs::File::create(file)?); let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&group)?)?; } } GroupsCommand::Fetch { id, expand, file } => { let group = client.fetch_group(&id, expand).await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&group)?)?; } GroupsCommand::Update { id, name, description, file, } => { let group = client .update_group(&id, name.as_deref(), description.as_deref()) .await?; println!("Updated group {}", group.id); if let Some(file) = file { let writer: Box = Box::new(fs::File::create(file)?); let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&group)?)?; } } GroupsCommand::Members { id, file } => { let members = client.get_group_members(&id).await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); for member in members { writeln!(buffered, "{}", serde_json::to_string(&member)?)?; } } GroupsCommand::Leave { id } => { client.leave_group(&id).await?; println!("Left group {}", id); } }, Self::Profile { cmd } => match cmd { ProfileCommand::User { file } => { let profile = client.fetch_user_profile().await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); writeln!(buffered, "{}", serde_json::to_string(&profile)?)?; } ProfileCommand::Groups { file } => { let groups = client.fetch_user_groups().await?; let writer: Box = match file { Some(file) => Box::new(fs::File::create(file)?), None => Box::new(io::stdout()), }; let mut buffered = io::BufWriter::new(writer); for group in groups { writeln!(buffered, "{}", serde_json::to_string(&group)?)?; } } }, Self::Complete { shell } => { // Generates shell completions let mut cmd = HypothesisCLI::command(); clap_complete::generate(shell, &mut cmd, "hypothesis", &mut io::stdout()); } } Ok(()) } } impl FromStr for Sort { type Err = CLIError; fn from_str(s: &str) -> Result { match s { "created" => Ok(Self::Created), "updated" => Ok(Self::Updated), "id" => Ok(Self::Id), "group" => Ok(Self::Group), "user" => Ok(Self::User), _ => Err(CLIError::ParseError { name: "sort".into(), types: vec![ "created".into(), "updated".into(), "id".into(), "group".into(), "user".into(), ], }), } } } impl Sort { /// A list of possible variants in `&'static str` form pub const fn variants() -> [&'static str; 5] { ["created", "updated", "id", "group", "user"] } } impl FromStr for Order { type Err = CLIError; fn from_str(s: &str) -> Result { match s { "desc" => Ok(Self::Desc), "asc" => Ok(Self::Asc), _ => Err(CLIError::ParseError { name: "order".into(), types: vec!["asc".into(), "desc".into()], }), } } } impl Order { /// A list of possible variants in `&'static str` form pub const fn variants() -> [&'static str; 2] { ["asc", "desc"] } } impl FromStr for Expand { type Err = CLIError; fn from_str(s: &str) -> Result { match s { "organization" => Ok(Self::Organization), "scopes" => Ok(Self::Scopes), _ => Err(CLIError::ParseError { name: "expand".into(), types: vec!["organization".into(), "scopes".into()], }), } } } impl Expand { /// A list of possible variants in `&'static str` form pub const fn variants() -> [&'static str; 2] { ["organization", "scopes"] } } rust-hypothesis-0.11.5/src/errors.rs000066400000000000000000000033541463037215500174540ustar00rootroot00000000000000//! API and CLI specific errors use std::fmt; use reqwest::header::InvalidHeaderValue; use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Error, Debug)] pub enum HypothesisError { #[error("Make sure input fields are valid")] APIError { #[source] source: APIError, serde_error: Option, raw_text: String, }, #[error("Invalid header value")] HeaderError(#[from] InvalidHeaderValue), #[error("Reqwest error")] ReqwestError(#[from] reqwest::Error), #[error("{suggestion:?}")] EnvironmentError { #[source] source: std::env::VarError, suggestion: String, }, #[error("JSON format error")] SerdeError(#[from] serde_json::Error), #[error("Couldn't parse URL")] URLError(#[from] url::ParseError), #[error("Builder error: {0}")] BuilderError(String), } /// Errors returned from the Hypothesis API #[derive(Error, Serialize, Deserialize, Debug, Default, Clone)] pub struct APIError { /// API returned status pub status: String, /// Cause of failure pub reason: String, } impl fmt::Display for APIError { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Status: {}\nReason: {}", self.status, self.reason) } } #[cfg(feature = "cli")] /// Errors returned from the Hypothesis CLI #[derive(Error, Serialize, Deserialize, Debug, Clone)] pub enum CLIError { /// Thrown when Hypothesis client creation fails #[error("Could not authorize")] AuthorizationError, /// Failed to parse a command line argument into its corresponding type #[error("ParseError: {name:?} must be one of {types:?}")] ParseError { name: String, types: Vec }, } rust-hypothesis-0.11.5/src/groups.rs000066400000000000000000000107731463037215500174620ustar00rootroot00000000000000//! Objects related to the "groups" endpoint #[cfg(feature = "cli")] use clap::Parser; #[cfg(feature = "cli")] use clap::ValueEnum; use serde::{Deserialize, Serialize}; use crate::is_default; /// Which field to expand #[cfg_attr(feature = "cli", derive(ValueEnum))] #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Expand { /// Expand `organization` field to `Org` Organization, /// Expand `scopes` field to `Scope` Scopes, } /// Filter groups by authority and target document #[cfg_attr(feature = "cli", derive(Parser))] #[derive(Serialize, Debug, Default, Clone, PartialEq)] pub struct GroupFilters { /// Filter returned groups to this authority. /// For authenticated requests, the user's associated authority will supersede any provided value. /// /// Default: "hypothes.is" #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "hypothes.is", long))] pub authority: String, /// Only retrieve public (i.e. non-private) groups that apply to a given document URI (i.e. the target document being annotated). #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(default_value = "", long))] pub document_uri: String, /// One or more relations to expand for a group resource. /// Possible values: organization, scopes #[serde(skip_serializing_if = "is_default")] #[cfg_attr(feature = "cli", clap(long, value_parser = clap::builder::EnumValueParser::::new()))] pub expand: Vec, } /// URL to the group's main (activity) page #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Links { /// URL to the group's main (activity) page #[serde(default)] pub html: Option, } /// See [the Hypothesis API docs](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/groups/paths/~1groups/get) for more information. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Scope { pub enforced: bool, pub uri_patterns: Vec, } /// Group type #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum Type { /// Only creator can view and edit Private, /// Anyone can view and edit Open, /// More than one user can view and edit Restricted, } /// Information about an organization /// Can be just the organization ID, /// an `Org` struct, /// or None if user is not authorized to access this organization #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum Organization { /// Unexpanded = Unique organization ID String(String), /// Expanded (None if not authorized) Organization(Option), } /// Information about an organization #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Org { /// Organization ID pub id: String, /// true if this organization is the default organization for the current authority pub default: bool, /// URI to logo image; may be null if no logo exists pub logo: Option, /// Organization name pub name: String, } /// Information returned about a Group resource #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Group { /// Group ID pub id: String, /// Authority-unique identifier that may be set for groups that are owned by a third-party authority. /// This field is currently present but unused for first-party-authority groups. pub groupid: Option, /// Group name pub name: String, /// URL to the group's main (activity) page pub links: Links, /// The organization to which this group belongs. pub organization: Organization, #[serde(default)] /// Information about the URL restrictions for annotations within this group. pub scopes: Option, /// Whether or not this group has URL restrictions for documents that may be annotated within it. /// Non-scoped groups allow annotation to documents at any URL pub scoped: bool, /// Is the groyp private, open, or restricted #[serde(rename = "type")] pub group_type: Type, } /// Information about another user #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Member { /// "hypothes.is" pub authority: String, /// string [ 3 .. 30 ] characters ^[A-Za-z0-9._]+$ pub username: String, /// string^acct:.+$ pub userid: String, /// string <= 30 characters #[serde(default)] pub display_name: Option, } rust-hypothesis-0.11.5/src/lib.rs000066400000000000000000001113161463037215500167040ustar00rootroot00000000000000//! [![Crates.io](https://img.shields.io/crates/v/hypothesis.svg)](https://crates.io/crates/hypothesis) //! [![Docs.rs](https://docs.rs/hypothesis/badge.svg)](https://docs.rs/hypothesis) //! [![CI](https://github.com/out-of-cheese-error/rust-hypothesis/workflows/Continuous%20Integration/badge.svg)](https://github.com/out-of-cheese-error/rust-hypothesis/actions) //! [![GitHub release](https://img.shields.io/github/release/out-of-cheese-error/rust-hypothesis.svg)](https://GitHub.com/out-of-cheese-error/rust-hypothesis/releases/) //! [![dependency status](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis/status.svg)](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis) //! //! # A Rust API for [Hypothesis](https://web.hypothes.is/) //! //! ## Description //! A lightweight wrapper and CLI for the [Hypothesis Web API v1.0.0](https://h.readthedocs.io/en/latest/api-reference/v1/). //! It includes all APIKey authorized endpoints related to //! * annotations (create / update / delete / search / fetch / flag), //! * groups (create / update / list / fetch / leave / members) //! * profile (user information / groups) //! //! ## Installation and Usage //! ### Authorization //! You'll need a [Hypothesis](https://hypothes.is) account, and a personal API token obtained as described [here](https://h.readthedocs.io/en/latest/api/authorization/). //! Set the environment variables `$HYPOTHESIS_NAME` and `$HYPOTHESIS_KEY` to your username and the developer API key respectively. //! //! ### As a command-line utility: //! ```bash //! cargo install hypothesis //! ``` //! Run `hypothesis --help` to see subcommands and options. //! NOTE: the CLI doesn't currently have all the capabilities of the Rust crate, specifically bulk actions and updating dates are not supported. //! //! Generate shell completions: //! ```bash //! hypothesis complete zsh > .oh-my-zsh/completions/_hypothesis //! exec zsh //! ``` //! //! ### As a Rust crate //! Add to your Cargo.toml: //! ```toml //! [dependencies] //! hypothesis = {version = "0.4.0", default-features = false} //! # For a tokio runtime: //! tokio = { version = "0.2", features = ["macros"] } //! ``` //! //! #### Examples //! ```rust no_run //! use hypothesis::Hypothesis; //! use hypothesis::annotations::{InputAnnotation, Target, Selector}; //! //! #[tokio::main] //! async fn main() -> Result<(), hypothesis::errors::HypothesisError> { //! let api = Hypothesis::from_env()?; //! let new_annotation = InputAnnotation::builder() //! .uri("https://www.example.com") //! .text("this is a comment") //! .target(Target::builder() //! .source("https://www.example.com") //! .selector(vec![Selector::new_quote("exact text in website to highlight", //! "prefix of text", //! "suffix of text")]) //! .build()?) //! .tags(vec!["tag1".to_string(), "tag2".to_string()]) //! .build()?; //! api.create_annotation(&new_annotation).await?; //! Ok(()) //! } //! ``` //! See the documentation of the API struct ([`Hypothesis`](https://docs.rs/crate/hypothesis/struct.Hypothesis.html)) for a list of possible queries. //! Use bulk functions to perform multiple actions - e.g. `api.fetch_annotations` instead of a loop around `api.fetch_annotation`. //! //! Check the [documentation](https://docs.rs/crate/hypothesis) for more usage examples. //! //! ### Changelog //! See the [CHANGELOG](CHANGELOG.md) //! //! ### Contributing //! Make sure you have a .env file (added to .gitignore) in the repo root with HYPOTHESIS_NAME, HYPOTHESIS_KEY, and TEST_GROUP_ID //! //! ### Caveats / Todo: //! - Only supports APIKey authorization and hypothes.is authority (i.e. single users). //! - `Target.selector.RangeSelector` doesn't seem to follow [W3C standards](https://www.w3.org/TR/annotation-model/#range-selector). It's just a hashmap for now. //! - `Annotation` hypermedia links are stored as a hashmap, b/c I don't know all the possible values. //! - Need to figure out how `Document` works to properly document it (hah). //! - Can't delete a group after making it, can leave it though (maybe it's the same thing?) //! - No idea what `UserProfile.preferences` and `UserProfile.features` mean. //! - CLI just dumps output as JSON, this is fine right? Fancier CLIs can build on top of this (or use the crate directly) #[macro_use] extern crate derive_builder; use std::collections::HashMap; use std::str::FromStr; use std::string::ParseError; use std::{env, fmt}; use futures::future::try_join_all; use reqwest::{header, Url}; use serde::{Deserialize, Serialize}; use crate::annotations::{Annotation, InputAnnotation, SearchQuery}; use crate::errors::HypothesisError; use crate::groups::{Expand, Group, GroupFilters, Member}; use crate::profile::UserProfile; pub mod annotations; #[cfg(feature = "cli")] pub mod cli; pub mod errors; pub mod groups; pub mod profile; /// Hypothesis API URL pub const API_URL: &str = "https://api.hypothes.is/api"; /// checks if a variable is the default value of its type fn is_default(t: &T) -> bool { t == &T::default() } pub fn serde_parse<'a, T: Deserialize<'a>>(text: &'a str) -> Result { serde_json::from_str::(text).map_err(|e| errors::HypothesisError::APIError { source: serde_json::from_str::(text).unwrap_or_default(), serde_error: Some(e), raw_text: text.to_owned(), }) } /// Hypothesis API client pub struct Hypothesis { /// Authenticated user pub username: String, /// "acct:{username}@hypothes.is" pub user: UserAccountID, /// authorized reqwest async client client: reqwest::Client, } impl Hypothesis { /// Make a new Hypothesis client with your username and developer key /// (see [here](https://h.readthedocs.io/en/latest/api/authorization/) on how to get one) /// # Example /// ``` /// # fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// let api = Hypothesis::new(&username, &developer_key)?; /// # Ok(()) /// # } /// ``` pub fn new(username: &str, developer_key: &str) -> Result { let user = UserAccountID::from_str(username).expect("This should never error"); let mut headers = header::HeaderMap::new(); headers.insert( header::AUTHORIZATION, header::HeaderValue::from_str(&format!("Bearer {}", developer_key)) .map_err(HypothesisError::HeaderError)?, ); headers.insert( header::ACCEPT, header::HeaderValue::from_str("application/vnd.hypothesis.v1+json") .map_err(HypothesisError::HeaderError)?, ); let client = reqwest::Client::builder() .default_headers(headers) .build() .map_err(HypothesisError::ReqwestError)?; Ok(Self { username: username.into(), user, client, }) } /// Make a new Hypothesis client from environment variables. /// Username from `$HYPOTHESIS_NAME`, /// Developer key from `$HYPOTHESIS_KEY` /// (see [here](https://h.readthedocs.io/en/latest/api/authorization/) on how to get one) /// # Example /// ``` /// # fn main() -> Result<(), Box> { /// # use std::env; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # env::set_var("HYPOTHESIS_NAME", username); /// # env::set_var("HYPOTHESIS_KEY", developer_key); /// use hypothesis::Hypothesis; /// let api = Hypothesis::from_env()?; /// # Ok(()) /// # } /// ``` pub fn from_env() -> Result { let username = env::var("HYPOTHESIS_NAME").map_err(|e| HypothesisError::EnvironmentError { source: e, suggestion: "Set the environment variable HYPOTHESIS_NAME to your username".into(), })?; let developer_key = env::var("HYPOTHESIS_KEY").map_err(|e| HypothesisError::EnvironmentError { source: e, suggestion: "Set the environment variable HYPOTHESIS_KEY to your personal API key" .into(), })?; Self::new(&username, &developer_key) } /// Create a new annotation /// /// Posts a new annotation object to Hypothesis. /// Returns an [`Annotation`](annotations/struct.Annotation.html) as output. /// See [`InputAnnotation`](annotations/struct.InputAnnotation.html) for examples on what you can add to an annotation. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// use hypothesis::annotations::InputAnnotation; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into()); /// /// let api = Hypothesis::new(&username, &developer_key)?; /// let annotation = api.create_annotation(&InputAnnotation::builder() /// .text("string") /// .uri("http://example.com") /// .group(&group_id) /// .build()?).await?; /// assert_eq!(&annotation.text, "string"); /// # api.delete_annotation(&annotation.id).await?; /// # Ok(()) /// # } /// ``` pub async fn create_annotation( &self, annotation: &InputAnnotation, ) -> Result { let text = self .client .post(&format!("{}/annotations", API_URL)) .json(annotation) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Create many new annotations /// /// Posts multiple new annotation objects asynchronously to Hypothesis. /// Returns [`Annotation`](annotations/struct.Annotation.html)s as output. /// See [`InputAnnotation`'s](annotations/struct.InputAnnotation.html) docs for examples on what /// you can add to an annotation. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use hypothesis::Hypothesis; /// # use hypothesis::annotations::InputAnnotation; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into()); /// let api = Hypothesis::new(&username, &developer_key)?; /// let input_annotations = vec![ /// InputAnnotation::builder() /// .text("first") /// .uri("http://example.com") /// .group(&group_id) /// .build()?, /// InputAnnotation::builder() /// .text("second") /// .uri("http://example.com") /// .group(&group_id) /// .build()? /// ]; /// let annotations = api.create_annotations(&input_annotations).await?; /// assert_eq!(&annotations[0].text, "first"); /// assert_eq!(&annotations[1].text, "second"); /// # api.delete_annotations(&annotations.into_iter().map(|a| a.id).collect::>()).await?; /// # Ok(()) /// # } /// ``` pub async fn create_annotations( &self, annotations: &[InputAnnotation], ) -> Result, HypothesisError> { let futures: Vec<_> = annotations .iter() .map(|a| self.create_annotation(a)) .collect(); async { try_join_all(futures).await }.await } /// Update an existing annotation /// /// Change any field in an existing annotation. Returns the modified [`Annotation`](annotations/struct.Annotation.html) /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// use hypothesis::annotations::InputAnnotation; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into()); /// let api = Hypothesis::new(&username, &developer_key)?; /// let mut annotation = api.create_annotation(&InputAnnotation::builder() /// .text("string") /// .uri("http://example.com") /// .tags(vec!["tag1".to_string(), "tag2".to_string()]) /// .group(&group_id) /// .build()?).await?; /// annotation.text = String::from("New String"); /// let updated_annotation = api.update_annotation(&annotation).await?; /// assert_eq!(updated_annotation.id, annotation.id); /// assert_eq!(&updated_annotation.text, "New String"); /// # api.delete_annotation(&updated_annotation.id).await?; /// # Ok(()) /// # } /// ``` pub async fn update_annotation( &self, annotation: &Annotation, ) -> Result { let text = self .client .patch(&format!("{}/annotations/{}", API_URL, annotation.id)) .json(&annotation) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Update many annotations at once pub async fn update_annotations( &self, annotations: &[Annotation], ) -> Result, HypothesisError> { let futures: Vec<_> = annotations .iter() .map(|a| self.update_annotation(a)) .collect(); async { try_join_all(futures).await }.await } /// Search for annotations with optional filters /// /// Returns a list of annotations matching the search query. /// See [`SearchQuery`](annotations/struct.SearchQuery.html) for more filtering options /// /// This returns a max of 50 annotations at once, use `search_annotations_return_all` if you expect more /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::{Hypothesis, UserAccountID}; /// use hypothesis::annotations::SearchQuery; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// let api = Hypothesis::new(&username, &developer_key)?; /// /// Search for your own annotations: /// let search_query = SearchQuery::builder().user(&api.user.0).build()?; /// let search_results = api.search_annotations(&search_query).await?; /// # assert!(!search_results.is_empty()); /// # Ok(()) /// # } /// ``` pub async fn search_annotations( &self, query: &SearchQuery, ) -> Result, HypothesisError> { let query: HashMap = serde_json::from_str( &serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?, ) .map_err(HypothesisError::SerdeError)?; let url = Url::parse_with_params( &format!("{}/search", API_URL), query .into_iter() .flat_map(|(k, v)| { if v.is_array() { v.as_array() .unwrap() .iter() .map(|v| (k.clone(), v.to_string().replace('"', ""))) .collect::>() } else { vec![(k, v.to_string().replace('"', ""))] } }) .collect::>(), ) .map_err(HypothesisError::URLError)?; let text = self .client .get(url) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; #[derive(Deserialize, Debug, Clone, PartialEq)] struct SearchResult { rows: Vec, total: usize, } Ok(serde_parse::(&text)?.rows) } /// Retrieve all annotations matching query /// See [`SearchQuery`](annotations/struct.SearchQuery.html) for filtering options pub async fn search_annotations_return_all( &self, query: &mut SearchQuery, ) -> Result, HypothesisError> { let mut annotations = Vec::new(); loop { let next = self.search_annotations(query).await?; if next.is_empty() { break; } query.search_after = next[next.len() - 1].updated.to_rfc3339(); annotations.extend_from_slice(&next); } Ok(annotations) } /// Fetch annotation by ID /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # use hypothesis::annotations::InputAnnotation; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into()); /// let api = Hypothesis::new(&username, &developer_key)?; /// # let annotation = api.create_annotation(&InputAnnotation::builder() /// # .text("string") /// # .uri("http://example.com") /// # .group(group_id).build()?).await?; /// # let annotation_id = annotation.id.to_owned(); /// let annotation = api.fetch_annotation(&annotation_id).await?; /// assert_eq!(annotation.id, annotation_id); /// # api.delete_annotation(&annotation.id).await?; /// # Ok(()) /// # } /// ``` pub async fn fetch_annotation(&self, id: &str) -> Result { let text = self .client .get(&format!("{}/annotations/{}", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Fetch multiple annotations by ID pub async fn fetch_annotations( &self, ids: &[String], ) -> Result, HypothesisError> { let futures: Vec<_> = ids.iter().map(|id| self.fetch_annotation(id)).collect(); async { try_join_all(futures).await }.await } /// Delete annotation by ID /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # use hypothesis::annotations::InputAnnotation; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into()); /// let api = Hypothesis::new(&username, &developer_key)?; /// # let annotation = api.create_annotation(&InputAnnotation::builder() /// # .text("string") /// # .uri("http://example.com") /// # .group(group_id).build()?).await?; /// # let annotation_id = annotation.id.to_owned(); /// let deleted = api.delete_annotation(&annotation_id).await?; /// assert!(deleted); /// assert!(api.fetch_annotation(&annotation_id).await.is_err()); /// # Ok(()) /// # } /// ``` pub async fn delete_annotation(&self, id: &str) -> Result { let text = self .client .delete(&format!("{}/annotations/{}", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; #[derive(Deserialize, Debug, Clone, PartialEq)] struct DeletionResult { id: String, deleted: bool, } Ok(serde_parse::(&text)?.deleted) } /// Delete multiple annotations by ID pub async fn delete_annotations(&self, ids: &[String]) -> Result, HypothesisError> { let futures: Vec<_> = ids.iter().map(|id| self.delete_annotation(id)).collect(); async { try_join_all(futures).await }.await } /// Flag an annotation /// /// Flag an annotation for review (moderation). The moderator of the group containing the /// annotation will be notified of the flag and can decide whether or not to hide the /// annotation. Note that flags persist and cannot be removed once they are set. pub async fn flag_annotation(&self, id: &str) -> Result<(), HypothesisError> { let text = self .client .put(&format!("{}/annotations/{}/flag", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; let error = serde_json::from_str::(&text); if let Ok(error) = error { Err(HypothesisError::APIError { source: error, raw_text: text, serde_error: None, }) } else { Ok(()) } } /// Hide an annotation /// /// Hide an annotation. The authenticated user needs to have the moderate permission for the /// group that contains the annotation — this permission is granted to the user who created the group. pub async fn hide_annotation(&self, id: &str) -> Result<(), HypothesisError> { let text = self .client .put(&format!("{}/annotations/{}/hide", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; let error = serde_json::from_str::(&text); if let Ok(error) = error { Err(HypothesisError::APIError { source: error, raw_text: text, serde_error: None, }) } else { Ok(()) } } /// Show an annotation /// /// Show/"un-hide" an annotation. The authenticated user needs to have the moderate permission /// for the group that contains the annotation—this permission is granted to the user who created the group. pub async fn show_annotation(&self, id: &str) -> Result<(), HypothesisError> { let text = self .client .delete(&format!("{}/annotations/{}/hide", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; let error = serde_json::from_str::(&text); if let Ok(error) = error { Err(HypothesisError::APIError { source: error, raw_text: text, serde_error: None, }) } else { Ok(()) } } /// Retrieve a list of applicable Groups, filtered by authority and target document (`document_uri`). /// Also retrieve user's private Groups. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// use hypothesis::groups::GroupFilters; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// /// let api = Hypothesis::new(&username, &developer_key)?; /// /// Get all Groups belonging to user /// let groups = api.get_groups(&GroupFilters::default()).await?; /// # assert!(!groups.is_empty()); /// # Ok(()) /// # } /// ``` pub async fn get_groups(&self, query: &GroupFilters) -> Result, HypothesisError> { let query: HashMap = serde_json::from_str( &serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?, ) .map_err(HypothesisError::SerdeError)?; let url = Url::parse_with_params( &format!("{}/groups", API_URL), query .into_iter() .map(|(k, v)| (k, v.to_string().replace('"', ""))) .collect::>(), ) .map_err(HypothesisError::URLError)?; let text = self .client .get(url) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::>(&text) } /// Create a new, private group for the currently-authenticated user. /// /// # Example /// ```no_run /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// /// let api = Hypothesis::new(&username, &developer_key)?; /// let group = api.create_group("my_group", Some("a test group")).await?; /// # Ok(()) /// # } /// ``` pub async fn create_group( &self, name: &str, description: Option<&str>, ) -> Result { let mut params = HashMap::new(); params.insert("name", name); if let Some(description) = description { params.insert("description", description); } let text = self .client .post(&format!("{}/groups", API_URL)) .json(¶ms) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Create multiple groups pub async fn create_groups( &self, names: &[String], descriptions: &[Option], ) -> Result, HypothesisError> { let futures: Vec<_> = names .iter() .zip(descriptions.iter()) .map(|(name, description)| self.create_group(name, description.as_deref())) .collect(); async { try_join_all(futures).await }.await } /// Fetch a single Group resource. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// use hypothesis::groups::Expand; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID")?; /// /// let api = Hypothesis::new(&username, &developer_key)?; /// /// Expands organization into a struct /// let group = api.fetch_group(&group_id, vec![Expand::Organization]).await?; /// # Ok(()) /// # } /// ``` pub async fn fetch_group( &self, id: &str, expand: Vec, ) -> Result { let params: HashMap<&str, Vec> = if !expand.is_empty() { vec![( "expand", expand .into_iter() .map(|e| serde_json::to_string(&e)) .collect::>() .map_err(HypothesisError::SerdeError)?, )] .into_iter() .collect() } else { HashMap::new() }; let text = self .client .get(&format!("{}/groups/{}", API_URL, id)) .json(¶ms) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Fetch multiple groups by ID pub async fn fetch_groups( &self, ids: &[String], expands: Vec>, ) -> Result, HypothesisError> { let futures: Vec<_> = ids .iter() .zip(expands.into_iter()) .map(|(id, expand)| self.fetch_group(id, expand)) .collect(); async { try_join_all(futures).await }.await } /// Update a Group resource. /// /// # Example /// ```no_run /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID")?; /// /// let api = Hypothesis::new(&username, &developer_key)?; /// let group = api.update_group(&group_id, Some("new_group_name"), None).await?; /// assert_eq!(&group.name, "new_group_name"); /// assert_eq!(group.id, group_id); /// # Ok(()) /// # } /// ``` pub async fn update_group( &self, id: &str, name: Option<&str>, description: Option<&str>, ) -> Result { let mut params = HashMap::new(); if let Some(name) = name { params.insert("name", name); } if let Some(description) = description { params.insert("description", description); } let text = self .client .patch(&format!("{}/groups/{}", API_URL, id)) .json(¶ms) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Update multiple groups pub async fn update_groups( &self, ids: &[String], names: &[Option], descriptions: &[Option], ) -> Result, HypothesisError> { let futures: Vec<_> = ids .iter() .zip(names.iter()) .zip(descriptions.iter()) .map(|((id, name), description)| { self.update_group(id, name.as_deref(), description.as_deref()) }) .collect(); async { try_join_all(futures).await }.await } /// Fetch a list of all members (users) in a group. Returned user resource only contains public-facing user data. /// Authenticated user must have read access to the group. Does not require authentication for reading members of /// public groups. Returned members are unsorted. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// # let group_id = dotenv::var("TEST_GROUP_ID")?; /// /// let api = Hypothesis::new(&username, &developer_key)?; /// let members = api.get_group_members(&group_id).await?; /// # Ok(()) /// # } /// ``` pub async fn get_group_members(&self, id: &str) -> Result, HypothesisError> { let text = self .client .get(&format!("{}/groups/{}/members", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::>(&text) } /// Remove yourself from a group. pub async fn leave_group(&self, id: &str) -> Result<(), HypothesisError> { let text = self .client .delete(&format!("{}/groups/{}/members/me", API_URL, id)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; let error = serde_json::from_str::(&text); if let Ok(error) = error { Err(HypothesisError::APIError { source: error, raw_text: text, serde_error: None, }) } else { Ok(()) } } /// Fetch profile information for the currently-authenticated user. /// /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// let api = Hypothesis::new(&username, &developer_key)?; /// let profile = api.fetch_user_profile().await?; /// assert!(profile.userid.is_some()); /// assert_eq!(profile.userid.unwrap(), api.user); /// # Ok(()) /// # } /// ``` pub async fn fetch_user_profile(&self) -> Result { let text = self .client .get(&format!("{}/profile", API_URL)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::(&text) } /// Fetch the groups for which the currently-authenticated user is a member. /// # Example /// ``` /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// use hypothesis::Hypothesis; /// # dotenv::dotenv()?; /// # let username = dotenv::var("HYPOTHESIS_NAME")?; /// # let developer_key = dotenv::var("HYPOTHESIS_KEY")?; /// let api = Hypothesis::new(&username, &developer_key)?; /// let groups = api.fetch_user_groups().await?; /// # Ok(()) /// # } /// ``` pub async fn fetch_user_groups(&self) -> Result, HypothesisError> { let text = self .client .get(&format!("{}/profile/groups", API_URL)) .send() .await .map_err(HypothesisError::ReqwestError)? .text() .await .map_err(HypothesisError::ReqwestError)?; serde_parse::>(&text) } } /// Stores user account ID in the form "acct:{username}@hypothes.is" /// /// Create from username: /// ``` /// # use hypothesis::UserAccountID; /// let user_id = "my_username".parse::().unwrap(); /// ``` #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct UserAccountID(pub String); impl UserAccountID { pub fn to_username(&self) -> String { if self.0.len() < 5 { String::new() } else { self.0 .to_owned() .split_off(5) .split('@') .next() .unwrap_or("") .to_owned() } } pub fn to_user_id(&self) -> String { self.0.to_owned() } } impl FromStr for UserAccountID { type Err = ParseError; fn from_str(s: &str) -> Result { Ok(Self(format!("acct:{}@hypothes.is", s))) } } impl fmt::Display for UserAccountID { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl From<&UserAccountID> for UserAccountID { #[inline] fn from(a: &UserAccountID) -> UserAccountID { UserAccountID(a.0.to_owned()) } } rust-hypothesis-0.11.5/src/main.rs000066400000000000000000000011771463037215500170650ustar00rootroot00000000000000#[cfg(not(feature = "cli"))] fn main() {} #[cfg(feature = "cli")] #[tokio::main] async fn main() -> color_eyre::Result<()> { use clap::Parser; use color_eyre::Help; use eyre::WrapErr; use hypothesis::cli::HypothesisCLI; use hypothesis::errors::CLIError; use hypothesis::Hypothesis; color_eyre::install()?; let cli: HypothesisCLI = HypothesisCLI::parse(); let api = Hypothesis::from_env() .wrap_err(CLIError::AuthorizationError) .suggestion("Make sure $HYPOTHESIS_NAME is set to your username and $HYPOTHESIS_KEY is set to your personal API key")?; cli.run(api).await?; Ok(()) } rust-hypothesis-0.11.5/src/profile.rs000066400000000000000000000011611463037215500175720ustar00rootroot00000000000000//! Objects related to the "profile" endpoint use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::UserAccountID; /// User profile information #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct UserProfile { /// "hypothes.is" pub authority: String, pub features: HashMap, pub preferences: HashMap, /// This property will be a string of the format "acct:username@authority" if the request is authenticated. /// This property will be null if the request is not authenticated. pub userid: Option, } rust-hypothesis-0.11.5/tests/000077500000000000000000000000001463037215500161405ustar00rootroot00000000000000rust-hypothesis-0.11.5/tests/cli.rs000066400000000000000000000175421463037215500172660ustar00rootroot00000000000000#![cfg(feature = "cli")] use assert_cmd::Command; use predicates::prelude::*; use std::{thread, time}; use hypothesis::annotations::Annotation; fn create_annotation( text: &str, username: &str, key: &str, group_id: &str, ) -> color_eyre::Result { let mut cmd = Command::cargo_bin("hypothesis")?; let output = cmd .env("HYPOTHESIS_NAME", username) .env("HYPOTHESIS_KEY", key) .arg("annotations") .arg("create") .arg(&format!("--text={}", text)) .arg(&format!("--group={}", group_id)) .arg("www.example.com") .assert(); let stdout = String::from_utf8(output.get_output().stdout.clone())?; let stdout = stdout.split("annotation ").last(); assert!(stdout.is_some()); Ok(stdout.unwrap().trim().to_string()) } #[test] fn add_and_delete_annotation() -> color_eyre::Result<()> { dotenv::dotenv()?; let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or_else(|_| "__world__".into()); let username = dotenv::var("HYPOTHESIS_NAME")?; let key = dotenv::var("HYPOTHESIS_KEY")?; // Create a new annotation let id = create_annotation("test annotation comment", &username, &key, &group_id); assert!(id.is_ok()); let id = id.unwrap(); // Fetch created annotation let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("fetch") .arg(&id) .assert() .stdout( predicate::str::contains(&id).and(predicate::str::contains("test annotation comment")), ); // Delete annotation let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("delete") .arg(&id) .assert() .stdout(predicate::str::starts_with("Deleted").and(predicate::str::contains(&id))); Ok(()) } #[test] fn update_annotation() -> color_eyre::Result<()> { dotenv::dotenv()?; let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or_else(|_| "__world__".into()); let username = dotenv::var("HYPOTHESIS_NAME")?; let key = dotenv::var("HYPOTHESIS_KEY")?; // Create a new annotation let id = create_annotation("test annotation comment", &username, &key, &group_id); assert!(id.is_ok()); let id = id.unwrap(); // Update annotation let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("update") .arg("--text=\"test text 2\"") .arg(&id) .assert() .stdout(predicate::str::starts_with("Updated").and(predicate::str::contains(&id))); // Fetch updated annotation let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("fetch") .arg(&id) .assert() .stdout(predicate::str::contains(&id).and(predicate::str::contains("test text 2"))); // Delete annotation let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("delete") .arg(&id) .assert() .success(); Ok(()) } #[test] fn search_annotations() -> color_eyre::Result<()> { dotenv::dotenv()?; let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or_else(|_| "__world__".into()); let username = dotenv::var("HYPOTHESIS_NAME")?; let key = dotenv::var("HYPOTHESIS_KEY")?; let ids = (0..4) .map(|i| create_annotation(&format!("test text {}", i), &username, &key, &group_id)) .collect::, _>>()?; let duration = time::Duration::from_millis(500); thread::sleep(duration); let mut cmd = Command::cargo_bin("hypothesis")?; let output = cmd .env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("search") .arg("--limit=200") .arg(&format!("--group={}", group_id)) .assert(); let stdout = String::from_utf8(output.get_output().stdout.clone())?; let mut count = 0; for annotation in serde_json::Deserializer::from_str(&stdout).into_iter::() { if ids.contains(&annotation?.id) { count += 1; } } assert_eq!(count, 4); for id in ids { let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("annotations") .arg("delete") .arg(id) .assert() .success(); } Ok(()) } fn create_group( name: &str, description: &str, username: &str, key: &str, ) -> color_eyre::Result { let mut cmd = Command::cargo_bin("hypothesis")?; let output = cmd .env("HYPOTHESIS_NAME", username) .env("HYPOTHESIS_KEY", key) .arg("groups") .arg("create") .arg(name) .arg(description) .assert(); let stdout = String::from_utf8(output.get_output().stdout.clone())?; let stdout = stdout.split("group ").last(); assert!(stdout.is_some()); Ok(stdout.unwrap().trim().to_string()) } #[test] fn create_and_leave_group() -> color_eyre::Result<()> { dotenv::dotenv()?; let username = dotenv::var("HYPOTHESIS_NAME")?; let key = dotenv::var("HYPOTHESIS_KEY")?; // Create a new group let group_id = create_group("test_name", "test description with spaces", &username, &key); assert!(group_id.is_ok()); let group_id = group_id.unwrap(); // Fetch created group let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("groups") .arg("fetch") .arg(&group_id) .assert() .stdout(predicate::str::contains(&group_id).and(predicate::str::contains("test_name"))); // Leave group let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("groups") .arg("leave") .arg(&group_id) .assert() .stdout(predicate::str::starts_with(format!( "Left group {}", &group_id ))); Ok(()) } #[test] fn update_group() -> color_eyre::Result<()> { let username = dotenv::var("HYPOTHESIS_NAME")?; let key = dotenv::var("HYPOTHESIS_KEY")?; // Create a new group let group_id = create_group("test_name", "test description with spaces", &username, &key); assert!(group_id.is_ok()); let group_id = group_id.unwrap(); // Update group let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("groups") .arg("update") .arg(&group_id) .arg("--name=test_group_2") .arg("--description=\"new description\"") .assert() .stdout(predicate::str::starts_with(format!( "Updated group {}", &group_id ))); // Check that update worked let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("groups") .arg("fetch") .arg(&group_id) .assert() .stdout(predicate::str::contains(&group_id).and(predicate::str::contains("test_group_2"))); // Leave group let mut cmd = Command::cargo_bin("hypothesis")?; cmd.env("HYPOTHESIS_NAME", &username) .env("HYPOTHESIS_KEY", &key) .arg("groups") .arg("leave") .arg(&group_id) .assert() .stdout(predicate::str::starts_with(format!( "Left group {}", group_id ))); Ok(()) }