diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 222d26a..aff2483 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,6 +35,7 @@ jobs: with: toolchain: ${{ matrix.rust }} - run: cargo test + - run: cargo test --features blocking fmt: name: Rustfmt ${{ matrix.os }}, ${{ matrix.rust }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e718aa..2f32cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [1.1.0] - 2025-08-25 + +### Added +- Added the `blocking` feature, which replaces the inner HTTP asynchronous client with a blocking one. This is useful when running a minimal setup that doesn't require a full async runtime. + +### Fixed +- Updated stale documentation. + ## [1.0.0] - 2025-07-02 ### Changed diff --git a/Cargo.lock b/Cargo.lock index b9dad01..247a96a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -19,7 +19,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aletheia" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "reqwest", @@ -81,9 +81,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bumpalo" @@ -99,18 +99,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -208,9 +208,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -222,6 +222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -230,6 +231,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.31" @@ -249,9 +256,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -285,9 +296,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -304,9 +315,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" @@ -356,19 +367,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -408,9 +421,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -544,9 +557,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -565,14 +578,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -607,9 +631,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" @@ -781,9 +805,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -814,9 +838,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -838,23 +862,25 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -901,28 +927,28 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "once_cell", "rustls-pki-types", @@ -942,9 +968,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -953,9 +979,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1023,9 +1049,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -1053,18 +1079,18 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1074,12 +1100,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1090,20 +1116,19 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] @@ -1115,9 +1140,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1167,31 +1192,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -1210,20 +1235,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1259,9 +1286,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1354,13 +1381,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1574,7 +1602,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -1595,10 +1623,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -1784,9 +1813,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index 05e27d2..fc91116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aletheia" -version = "1.0.0" +version = "1.1.0" edition = "2021" description = "A client library for the Guardian's content API" license = "MIT" @@ -9,6 +9,11 @@ repository = "https://github.com/marsavar/aletheia/" keywords = ["guardian", "api", "client", "library", "http"] categories = ["api-bindings", "parsing", "web-programming::http-client"] +[features] +default = [] +# Replaces the asynchronous client with a blocking one +blocking = ["reqwest/blocking"] + [dependencies] chrono = { version = "0.4.41", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/README.md b/README.md index 4a45529..bd2631a 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,22 @@ Aletheia is an HTTP client library for [the Guardian](https://www.theguardian.co [actions-url]: https://github.com/marsavar/aletheia/actions?query=workflow%3ACI+branch%3Amain ## How to use it +You need an API key to be able to make requests. +Keys can be requested [here](https://open-platform.theguardian.com/access/). + +Aletheia provides two versions of the client: [asynchronous](#async-example) (default behaviour) and [blocking](#blocking-example).\ +The default behaviour is asynchronous, which means the client can be used inside `async fn`. + +### Async example Aletheia requires Tokio as a dependency to execute asynchronous code.\ Simply add `aletheia` and `tokio` to the list of dependencies in your `Cargo.toml` file. ```toml [dependencies] -aletheia = "1.0.0" +aletheia = "1.1.0" tokio = { version = "1", features = ["full"] } ``` -You also need an API key to be able to make requests. -Keys can be requested [here](https://open-platform.theguardian.com/access/). - -## Example - Let's say you were interested in finding five film, play or album reviews with a rating of 5 stars containing the word "politics" published from January to December 2022. The code would look something like the example below, and would consist of three steps: @@ -37,11 +39,11 @@ The code would look something like the example below, and would consist of three ```rust use aletheia::enums::*; -use aletheia::error::Error; use aletheia::GuardianContentClient; +use std::error::Error; #[tokio::main] -async fn main() -> Result<(), Error> { +async fn main() -> Result<(), Box> { // The client is constructed by passing your API key // as the only parameter let client = GuardianContentClient::new("YOUR_API_KEY"); @@ -101,6 +103,58 @@ https://www.theguardian.com/tv-and-radio/2022/sep/27/make-me-prime-minister-revi https://www.theguardian.com/film/2022/sep/02/bones-and-all-review-luca-guadagnino-timothee-chalamet-venice-film-festival ``` +### Blocking example +Add `aletheia` to the list of dependencies in your `Cargo.toml` file with the `blocking` feature enabled. +```toml +[dependencies] +aletheia = { version = "1.1.0", features = ["blocking"] } +``` +This makes `GuardianContentClient` blocking, which can be useful in situations where a full async runtime is not +needed. Note that invoking the blocking client from an `async` block causes a panic. +It can still be used in `async fn`, however, if invoked from a blocking context, for example inside +`tokio::task::spawn_blocking`. + +The above example then becomes: + +```rust +use aletheia::enums::*; +use aletheia::GuardianContentClient; +use std::error::Error; + +fn main() -> Result<(), Box> { + let client = GuardianContentClient::new("YOUR_API_KEY"); + + let response = client + .build_request() + .search("politics") + .date_from(2022, 1, 1) + .date_to(2022, 12, 31) + .star_rating(5) + .page_size(5) + .show_fields(vec![Field::Byline]) + .order_by(OrderBy::Newest) + .send(); + + if let Some(results) = response.results { + for result in results { + let Some(pub_date) = result.web_publication_date else { continue }; + let Some(fields) = result.fields else { continue }; + let Some(byline) = fields.byline else { continue }; + + println!( + "[{}] {} ({})\n{}\n", + pub_date.format("%Y-%m-%d"), + result.web_title.trim(), + byline, + result.web_url, + ) + } + } + + Ok(()) +} +``` + #### Debug [*] You can pretty-print the whole output response with the format specifier `#?`: ```rust diff --git a/src/lib.rs b/src/lib.rs index 0957e40..90f2acb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,38 +2,70 @@ //! //! It is built on top of `reqwest` and provides a similar interface //! for building queries. Responses returned by the client are deserialized -//! into structs, mirroring the types used by the API. +//! into structs that mirror the types used by the API. //! -//! Keys to start querying the API can be obtained here: +//! Aletheia provides both an asynchronous client (default) and a blocking +//! client, which can be enabled with the `blocking` feature. +//! +//! Keys to query the API can be obtained at //! //! -//! More information on the API can be found here: +//! More information on the API can be found at //! //! -//! # Example -//! ```ignore -//! use aletheia::error::Error; -//! use aletheia::GuardianContentClient; -//! use aletheia::enums::{Field, OrderBy, OrderDate}; +//! # Async example +//! Executing the code below requires an asynchronous context. +//! For example, it can be executed from within an `async fn` block. +//! ```rust +//! # use aletheia::GuardianContentClient; +//! # use aletheia::enums::{Field, OrderBy, OrderDate}; +//! # use std::error::Error; +//! # #[cfg(not(feature = "blocking"))] +//! # async fn run() -> Result<(), Box> { +//! let client = GuardianContentClient::new("YOUR_API_KEY"); +//! +//! let response = client +//! .build_request() +//! .search("Elections") +//! .page_size(10) +//! .show_fields(vec![Field::Byline, Field::LastModified]) +//! .order_by(OrderBy::Newest) +//! .order_date(OrderDate::Published) +//! .send() +//! .await?; //! -//! #[tokio::main] -//! async fn main() -> Result<(), Error> { -//! let client = GuardianContentClient::new("YOUR_API_KEY"); +//! println!("{:#?}", response.results); +//! # Ok(()) +//! # } +//! ``` +//! # Blocking example +//! Executing the code below requires a blocking context and the `blocking` +//! feature enabled. +//! ### Warning +//! Using the blocking client in an async context will cause a panic. +//! If you need to use the blocking client in an `async` function, +//! you can do so in a blocking context, for example by using [`tokio::task::spawn_blocking`]. +//! ```rust +//! # use aletheia::GuardianContentClient; +//! # use aletheia::enums::{Field, OrderBy, OrderDate}; +//! # use std::error::Error; +//! # #[cfg(feature = "blocking")] +//! # fn run() -> Result<(), Box> { +//! let client = GuardianContentClient::new("YOUR_API_KEY"); //! -//! let response = client -//! .build_request() -//! .search("Elections") -//! .page_size(10) -//! .show_fields(vec![Field::Byline, Field::LastModified]) -//! .order_by(OrderBy::Newest) -//! .order_date(OrderDate::Published) -//! .send() -//! .await?; +//! let response = client +//! .build_request() +//! .search("Elections") +//! .page_size(10) +//! .show_fields(vec![Field::Byline, Field::LastModified]) +//! .order_by(OrderBy::Newest) +//! .order_date(OrderDate::Published) +//! .send()?; //! -//! let results = response.results; +//! println!("{:#?}", response.results); //! -//! Ok(()) -//! } +//! # Ok(()) +//! # } //! ``` pub mod enums; @@ -45,25 +77,46 @@ use crate::enums::*; use crate::error::Error; use crate::structs::*; use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::{Client, Url}; +use reqwest::Url; use std::collections::HashMap; use std::string::ToString; const GUARDIAN_CONTENT_API_URL: &str = "https://content.guardianapis.com"; -/// The main asynchronous client used to build requests to send to the Guardian's -/// content API. This client maintains a private internal asynchronous client -/// implemented by [`reqwest::Client`] +#[cfg(not(feature = "blocking"))] +type ReqwestClient = reqwest::Client; + +#[cfg(feature = "blocking")] +type ReqwestClient = reqwest::blocking::Client; + #[derive(Clone, Debug)] +#[cfg_attr( + not(feature = "blocking"), + doc = "The client used to send requests to the Guardian's content API. + This client maintains a private internal asynchronous client implemented by [`reqwest::Client`]. + Note that use of this client requires an asychronous context. If your application does not run + asynchronously (for example if the `tokio` runtime is not available), consider enabling the `blocking` + feature of this crate, which will transform this into a blocking client." +)] +#[cfg_attr( + feature = "blocking", + doc = "The client used to send requests to the Guardian's content API. + This client maintains a private internal blocking client implemented by [`reqwest::blocking::Client`]. + Note that use of this client requires a blocking context. If your application relies on an async runtime + such as `tokio`, consider using the asynchronous version of this client by removing the `blocking` feature + of this crate. + Alternatively, you can still use this client in an async runtime, but panics will occur + if it is not invoked in a blocking context (e.g. by using [`tokio::task::spawn_blocking`])" +)] pub struct GuardianContentClient { - http_client: reqwest::Client, + http_client: ReqwestClient, api_key: String, base_url: Url, } #[derive(Debug, Clone)] pub struct GuardianRequestBuilder { - http_client: Client, + http_client: ReqwestClient, base_url: Url, api_key: String, request: HashMap, @@ -85,7 +138,7 @@ impl GuardianRequestBuilder { /// Here the term 'item' refers to either a piece of content, a tag, or a section. /// The item endpoint matches the paths on theguardian.com. /// - /// # Example 1 + /// # Async example 1 /// ```ignore /// let response = client /// .build_request() @@ -95,7 +148,7 @@ impl GuardianRequestBuilder { /// .await?; /// ``` /// - /// # Example 2 + /// # Async example 2 /// ```ignore /// let response = client /// .build_request() @@ -127,7 +180,7 @@ impl GuardianRequestBuilder { /// Calling this method on [`Endpoint::SingleItem`] will /// have no effect. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -146,7 +199,7 @@ impl GuardianRequestBuilder { /// In order to page through the results, you can pass the page number /// as a parameter to this function. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -167,7 +220,7 @@ impl GuardianRequestBuilder { /// This function overrides the default. /// The page value must be between 0 and 200 for a successful response. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -189,7 +242,7 @@ impl GuardianRequestBuilder { /// - [`OrderBy::Oldest`] /// - [`OrderBy::Relevance`] /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -211,7 +264,7 @@ impl GuardianRequestBuilder { /// - [`OrderDate::NewspaperEdition`] /// - [`OrderDate::LastModified`] /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -240,7 +293,7 @@ impl GuardianRequestBuilder { /// for more information on all the possible fields, /// or check the [`crate::enums`] section of the documentation. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -270,7 +323,7 @@ impl GuardianRequestBuilder { /// for more information on all the possible tags, /// or check the [`crate::enums`] section of the documentation. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -297,7 +350,7 @@ impl GuardianRequestBuilder { /// for more information on all the possible fields, /// or check the [`crate::enums`] section of the documentation. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -315,7 +368,7 @@ impl GuardianRequestBuilder { /// Only return content published on or after the specified date. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -338,7 +391,7 @@ impl GuardianRequestBuilder { /// Note: Providing invalid YMD-HMS does not append query parameters /// to the API request. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -370,7 +423,7 @@ impl GuardianRequestBuilder { /// Only return content published on or before the specified date. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -394,7 +447,7 @@ impl GuardianRequestBuilder { /// Note: Providing invalid YMD-HMS does not append query parameters /// to the API request. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -434,7 +487,7 @@ impl GuardianRequestBuilder { /// - [`UseDate::NewspaperEdition`] /// - [`UseDate::LastModified`] /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -453,7 +506,7 @@ impl GuardianRequestBuilder { /// Add associated metadata section. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -470,7 +523,7 @@ impl GuardianRequestBuilder { /// Return only content in those sections. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -487,7 +540,7 @@ impl GuardianRequestBuilder { /// Return only content with those references. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -504,7 +557,7 @@ impl GuardianRequestBuilder { /// Return only content with references of those types. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -521,7 +574,7 @@ impl GuardianRequestBuilder { /// Return only content with those tags. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -537,7 +590,7 @@ impl GuardianRequestBuilder { /// Return only content with those IDs. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -553,7 +606,7 @@ impl GuardianRequestBuilder { /// Return only content from those production offices. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -573,7 +626,7 @@ impl GuardianRequestBuilder { /// Return only content in those languages. /// Accepts ISO language codes, e.g. en, fr. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -590,7 +643,7 @@ impl GuardianRequestBuilder { /// Return only content with a given star rating /// ranging from 1 to 5. /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -608,7 +661,7 @@ impl GuardianRequestBuilder { /// Only return tags of the specified type. /// Only valid if the endpoint is set to [`Endpoint::Tags`] /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -642,7 +695,7 @@ impl GuardianRequestBuilder { /// - [`Block::BodyKeyEvents`] /// - [`Block::BodyPublishedSince(i64)`] (only blocks since given timestamp) /// - /// # Example + /// # Async example /// ```ignore /// let response = client /// .build_request() @@ -662,6 +715,7 @@ impl GuardianRequestBuilder { /// Terminal operation that sends a GET request to the Guardian API. /// Once this function is called, all the query parameters constructed /// via the building methods are dropped. + #[cfg(not(feature = "blocking"))] pub async fn send(&mut self) -> Result { let mut headers = HeaderMap::new(); if !self.api_key.is_empty() { @@ -714,16 +768,69 @@ impl GuardianRequestBuilder { None => Ok(crate::helpers::mock_response()), } } + + /// Terminal operation that sends a GET request to the Guardian API. + /// Once this function is called, all the query parameters constructed + /// via the building methods are dropped. + #[cfg(feature = "blocking")] + pub fn send(&mut self) -> Result { + let mut headers = HeaderMap::new(); + if !self.api_key.is_empty() { + headers.insert("api-key", HeaderValue::from_str(&self.api_key).unwrap()); + } + + let endpoint = match self.endpoint { + Endpoint::Content => String::from("search"), + Endpoint::Tags => self.endpoint.to_string(), + Endpoint::Sections => self.endpoint.to_string(), + Endpoint::Editions => self.endpoint.to_string(), + Endpoint::SingleItem => self + .request + .get("q") + .ok_or(Error::MissingQueryParameter("q"))? + .to_owned(), + }; + + let queries = Vec::from_iter(self.request.iter()); + + let mut url = self.base_url.clone(); + url.path_segments_mut().unwrap().push(&endpoint); + + let search = self + .http_client + .get(url) + .headers(headers) + .query(&queries) + .send()? + .json::()?; + + if let Some(err) = search.message { + return Err(Error::ApiError(err)); + } + + if let Some(response_content) = &search.response { + if response_content.status.as_deref() == Some("error") { + if let Some(message) = &response_content.message { + return Err(Error::ApiError(message.to_owned())); + } + } + } + + self.request.clear(); + + match search.response { + Some(r) => Ok(r), + None => Ok(crate::helpers::mock_response()), + } + } } impl GuardianContentClient { /// Constructor for the client. /// The constructor takes an API key which is then stored internally - /// in the struct. The client then uses the builder pattern - /// to add query parameters to the request. The query-building - /// methods as well as the terminal operation take a mutable borrow - /// of self. This allows the client not to be consumed after a request. - /// As a result, the client should be initialised with the `mut` keyword. + /// in the struct. You can then build requests by using the + /// `build_request()` method on the client and then gradually + /// adding query parameters. /// /// API keys for the Guardian's content API can be requested at /// @@ -734,7 +841,7 @@ impl GuardianContentClient { /// ``` pub fn new(api_key: &str) -> GuardianContentClient { Self { - http_client: Client::new(), + http_client: ReqwestClient::new(), // Safety: it's ok to unwrap here since we are passing a valid URL string base_url: Url::parse(GUARDIAN_CONTENT_API_URL).unwrap(), api_key: String::from(api_key), @@ -742,10 +849,10 @@ impl GuardianContentClient { } /// Start building a new request. - /// This method returns `GuardianRequestBuilder`, which allows you to + /// This method returns [`GuardianRequestBuilder`], which allows you to /// build queries using the builder pattern. /// - /// # Example + /// # Async example /// ```ignore /// let client = aletheia::GuardianContentClient::new("YOUR_API_KEY"); /// let response = client @@ -755,6 +862,17 @@ impl GuardianContentClient { /// .send() /// .await?; /// ``` + /// # Blocking example + /// This requires the `blocking` feature to be enabled, which + /// replaces the async client with a blocking one. + /// ```ignore + /// let client = aletheia::GuardianContentClient::new("YOUR_API_KEY"); + /// let response = client + /// .build_request() + /// .search("Elections") + /// .datetime_from(2020, 1, 1, 12, 0, 0, 2) + /// .send()?; + /// ``` pub fn build_request(&self) -> GuardianRequestBuilder { GuardianRequestBuilder { http_client: self.http_client.clone(), diff --git a/src/tests.rs b/src/tests.rs index 9d22af6..b7ec604 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,5 +1,5 @@ -#[cfg(test)] -mod tests { +#[cfg(all(test, not(feature = "blocking")))] +mod async_client { use reqwest::Url; use crate::{ @@ -340,3 +340,32 @@ mod tests { assert!(matches!(err, Error::MissingQueryParameter("q"))); } } + +#[cfg(all(test, feature = "blocking"))] +mod blocking_client { + + use crate::{enums::Endpoint, error::Error, GuardianContentClient}; + + fn client_setup() -> GuardianContentClient { + GuardianContentClient::new("test-api-key") + } + + #[test] + fn test_api_key() { + let client = client_setup(); + assert_eq!(client.api_key, "test-api-key"); + } + + #[test] + fn test_error_missing_parameter() { + let result = client_setup() + .build_request() + .endpoint(Endpoint::SingleItem) + .send(); + + assert!(result.is_err()); + + let err = result.err().unwrap(); + assert!(matches!(err, Error::MissingQueryParameter("q"))); + } +}