From 2050a0718519526322918b9e8ccdf67d0e84c468 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Mon, 6 Jan 2025 12:11:36 +0100 Subject: [PATCH] Add `async` feature and rework `Dispatcher` --- Cargo.lock | 65 ----------------------- Cargo.toml | 6 ++- README.md | 4 +- examples/blocking.rs | 4 +- examples/client.rs | 4 +- justfile | 9 ++-- src/dispatcher/async.rs | 49 ++++++++++++++++++ src/dispatcher/blocking.rs | 44 ++++++++++++++++ src/dispatcher/builder.rs | 53 +++++++++++-------- src/dispatcher/mod.rs | 103 ++++++++++++------------------------- src/error.rs | 39 ++++++++------ src/lib.rs | 7 ++- 12 files changed, 201 insertions(+), 186 deletions(-) create mode 100644 src/dispatcher/async.rs create mode 100644 src/dispatcher/blocking.rs diff --git a/Cargo.lock b/Cargo.lock index 4339c3a..3894c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,12 +50,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - [[package]] name = "bumpalo" version = "3.14.0" @@ -326,16 +320,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.20" @@ -421,29 +405,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -571,15 +532,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags", -] - [[package]] name = "reqwest" version = "0.12.12" @@ -700,12 +652,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.193" @@ -749,15 +695,6 @@ dependencies = [ "serde", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.9" @@ -892,9 +829,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", diff --git a/Cargo.toml b/Cargo.toml index ab09a13..36670c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ readme = "README.md" keywords = ["ntfy", "notifications", "sdk"] [features] -default = ["dep:reqwest"] +default = ["async"] +async = ["dep:reqwest"] blocking = ["dep:ureq"] [dependencies] @@ -23,7 +24,7 @@ ureq = { version = "2.12", features = ["json", "socks-proxy"], optional = true } url = { version = "2", features = ["serde"] } [dev-dependencies] -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [[example]] name = "blocking" @@ -31,6 +32,7 @@ required-features = ["blocking"] [[example]] name = "client" +required-features = ["async"] [profile.release] lto = true diff --git a/README.md b/README.md index 2f6f9d2..be42af9 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ use ntfy::prelude::*; #[tokio::main] async fn main() -> Result<(), NtfyError> { - let dispatcher = Dispatcher::builder("https://ntfy.sh") + let dispatcher = DispatcherBuilder::new("https://ntfy.sh") .credentials(Auth::credentials("username", "password")) // Add optional credentials .proxy("socks5h://127.0.0.1:9050") // Add optional proxy - .build()?; // Build dispatcher + .build_async()?; // Build dispatcher let action = Action::new( ActionType::Http, diff --git a/examples/blocking.rs b/examples/blocking.rs index adc1f5f..a345aad 100644 --- a/examples/blocking.rs +++ b/examples/blocking.rs @@ -4,10 +4,10 @@ use ntfy::prelude::*; fn main() -> Result<(), NtfyError> { - let dispatcher = Dispatcher::builder("https://ntfy.sh") + let dispatcher = DispatcherBuilder::new("https://ntfy.sh") .credentials(Auth::credentials("username", "password")) // Add optional credentials .proxy("socks5://127.0.0.1:9050") // Add optional proxy - .build()?; // Build dispatcher + .build_blocking()?; // Build dispatcher let action = Action::new( ActionType::Http, diff --git a/examples/client.rs b/examples/client.rs index 82e17a7..78f2209 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -5,10 +5,10 @@ use ntfy::prelude::*; #[tokio::main] async fn main() -> Result<(), NtfyError> { - let dispatcher = Dispatcher::builder("https://ntfy.sh") + let dispatcher = DispatcherBuilder::new("https://ntfy.sh") .credentials(Auth::credentials("username", "password")) // Add optional credentials .proxy("socks5://127.0.0.1:9050") // Add optional proxy - .build()?; // Build dispatcher + .build_async()?; // Build dispatcher let action = Action::new( ActionType::Http, diff --git a/justfile b/justfile index f4470eb..8002780 100755 --- a/justfile +++ b/justfile @@ -2,6 +2,9 @@ check: cargo fmt --all -- --config format_code_in_doc_comments=true - cargo check - cargo test - cargo clippy -- -D warnings + cargo check --no-default-features --features async + cargo check --no-default-features --features blocking + cargo test --no-default-features --features async + cargo test --no-default-features --features blocking + cargo clippy --no-default-features --features async -- -D warnings + cargo clippy --no-default-features --features blocking -- -D warnings diff --git a/src/dispatcher/async.rs b/src/dispatcher/async.rs new file mode 100644 index 0000000..7b38e60 --- /dev/null +++ b/src/dispatcher/async.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Yuki Kishimoto +// Distributed under the MIT software license + +use reqwest::{Client, Response}; +use url::Url; + +use crate::{NtfyError, Payload}; + +/// Async dispatcher +#[derive(Debug, Clone)] +pub struct Async { + client: Client, +} + +impl Async { + #[inline] + pub(crate) fn new(client: Client) -> Self { + Self { client } + } + + /// Send payload to ntfy server + pub(crate) async fn send(&self, url: &Url, payload: &Payload) -> Result<(), NtfyError> { + // Build request + let mut builder = self.client.post(url.as_str()); + + // If markdown, set headers + if payload.markdown { + builder = builder.header("Markdown", "yes"); + } + + // Add payload + builder = builder.json(payload); + + // Send request + let res: Response = builder.send().await?; + let res: Response = res.error_for_status()?; + + // Get full response text + let text: String = res.text().await?; + + if text.is_empty() { + return Err(NtfyError::EmptyResponse); + } + + // TODO: check the text? + + Ok(()) + } +} diff --git a/src/dispatcher/blocking.rs b/src/dispatcher/blocking.rs new file mode 100644 index 0000000..1cd00fe --- /dev/null +++ b/src/dispatcher/blocking.rs @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Yuki Kishimoto +// Distributed under the MIT software license + +use ureq::{Agent, Response}; +use url::Url; + +use crate::{NtfyError, Payload}; + +/// Blocking dispatcher +#[derive(Debug, Clone)] +pub struct Blocking { + client: Agent, +} + +impl Blocking { + #[inline] + pub(crate) fn new(client: Agent) -> Self { + Self { client } + } + + pub(crate) fn send(&self, url: &Url, payload: &Payload) -> Result<(), NtfyError> { + // Build request + let mut builder = self.client.post(url.as_str()); + + // If markdown, set headers + if payload.markdown { + builder = builder.set("Markdown", "yes"); + } + + // Send request + let res: Response = builder.send_json(payload)?; + + // Get full response text + let text: String = res.into_string()?; + + if text.is_empty() { + return Err(NtfyError::EmptyResponse); + } + + // TODO: check the text? + + Ok(()) + } +} diff --git a/src/dispatcher/builder.rs b/src/dispatcher/builder.rs index cb7760b..df74530 100644 --- a/src/dispatcher/builder.rs +++ b/src/dispatcher/builder.rs @@ -1,19 +1,16 @@ // Copyright (c) 2022 Yuki Kishimoto // Distributed under the MIT software license -use std::str::FromStr; - -#[cfg(not(feature = "blocking"))] -use reqwest::header::{HeaderMap, HeaderValue}; -#[cfg(not(feature = "blocking"))] -use reqwest::ClientBuilder; -#[cfg(not(feature = "blocking"))] -use reqwest::Proxy; - +#[cfg(any(feature = "async", feature = "blocking"))] use url::Url; -use super::{Auth, Dispatcher}; -use crate::error::NtfyError; +#[cfg(feature = "async")] +use super::Async; +use super::Auth; +#[cfg(feature = "blocking")] +use super::Blocking; +#[cfg(any(feature = "async", feature = "blocking"))] +use super::{Dispatcher, NtfyError}; #[derive(Debug, Clone)] pub struct DispatcherBuilder { @@ -23,6 +20,7 @@ pub struct DispatcherBuilder { } impl DispatcherBuilder { + #[inline] pub fn new(url: S) -> Self where S: Into, @@ -40,6 +38,7 @@ impl DispatcherBuilder { self } + #[inline] pub fn proxy(mut self, proxy: S) -> Self where S: Into, @@ -48,8 +47,20 @@ impl DispatcherBuilder { self } - #[cfg(not(feature = "blocking"))] - pub fn build(self) -> Result { + #[cfg(feature = "async")] + #[deprecated( + since = "0.7.0", + note = "Please use `build_async` or `build_blocking` instead" + )] + pub fn build(self) -> Result, NtfyError> { + self.build_async() + } + + #[cfg(feature = "async")] + pub fn build_async(self) -> Result, NtfyError> { + use reqwest::header::{HeaderMap, HeaderValue}; + use reqwest::{ClientBuilder, Proxy}; + let mut client = ClientBuilder::new(); if let Some(auth) = self.auth { @@ -65,22 +76,22 @@ impl DispatcherBuilder { } Ok(Dispatcher { - url: Url::from_str(&self.url)?, - client: client.build()?, + url: Url::parse(&self.url)?, + inner: Async::new(client.build()?), }) } #[cfg(feature = "blocking")] - pub fn build(self) -> Result { + pub fn build_blocking(self) -> Result, NtfyError> { use ureq::{Error, MiddlewareNext, Request, Response}; - let mut agent = ureq::builder(); + let mut client = ureq::builder(); if let Some(auth) = self.auth { let heaver_value = auth.to_header_value(); // Set the authorization headers of every request using a middleware function - agent = agent.middleware( + client = client.middleware( move |req: Request, next: MiddlewareNext| -> Result { next.handle(req.set("Authorization", &heaver_value)) }, @@ -89,12 +100,12 @@ impl DispatcherBuilder { if let Some(proxy) = self.proxy { let proxy = ureq::Proxy::new(proxy)?; - agent = agent.proxy(proxy); + client = client.proxy(proxy); } Ok(Dispatcher { - url: Url::from_str(&self.url)?, - agent: agent.build(), + url: Url::parse(&self.url)?, + inner: Blocking::new(client.build()), }) } } diff --git a/src/dispatcher/mod.rs b/src/dispatcher/mod.rs index 499e113..d01dfa6 100644 --- a/src/dispatcher/mod.rs +++ b/src/dispatcher/mod.rs @@ -1,109 +1,70 @@ // Copyright (c) 2022 Yuki Kishimoto // Distributed under the MIT software license -#[cfg(not(feature = "blocking"))] -use reqwest::{Client, Response}; -#[cfg(feature = "blocking")] -use ureq::{Agent, Response}; use url::Url; +#[cfg(feature = "async")] +mod r#async; pub mod auth; +#[cfg(feature = "blocking")] +mod blocking; pub mod builder; pub use self::auth::Auth; +#[cfg(feature = "blocking")] +pub use self::blocking::Blocking; pub use self::builder::DispatcherBuilder; +#[cfg(feature = "async")] +pub use self::r#async::Async; use crate::error::NtfyError; +#[cfg(any(feature = "async", feature = "blocking"))] use crate::payload::Payload; #[derive(Debug, Clone)] -pub struct Dispatcher { +pub struct Dispatcher +where + T: Clone, +{ url: Url, - #[cfg(not(feature = "blocking"))] - client: Client, - #[cfg(feature = "blocking")] - agent: Agent, + inner: T, } -impl Dispatcher { +impl Dispatcher +where + T: Clone, +{ /// Create new dispatcher - pub fn new(url: S, auth: Option, proxy: Option) -> Result + #[deprecated(since = "0.7.0", note = "Please use `Dispatcher::builder` instead")] + pub fn new(_url: S, _auth: Option, _proxy: Option) -> Result where S: Into, { - let mut builder = DispatcherBuilder::new(url); - - if let Some(auth) = auth { - builder = builder.credentials(auth); - } - - if let Some(proxy) = proxy { - builder = builder.proxy(proxy); - } - - builder.build() + unimplemented!() } - #[inline] + #[deprecated(since = "0.7.0", note = "Please use `DispatcherBuilder::new` instead")] pub fn builder(url: S) -> DispatcherBuilder where S: Into, { DispatcherBuilder::new(url) } +} - #[cfg(not(feature = "blocking"))] +#[cfg(feature = "async")] +impl Dispatcher { /// Send payload to ntfy server + #[inline] pub async fn send(&self, payload: &Payload) -> Result<(), NtfyError> { - // Build request - let mut builder = self.client.post(self.url.as_str()); - - // If markdown, set headers - if payload.markdown { - builder = builder.header("Markdown", "yes"); - } - - // Add payload - builder = builder.json(payload); - - // Send request - let res: Response = builder.send().await?; - let res: Response = res.error_for_status()?; - - // Get full response text - let text: String = res.text().await?; - - if text.is_empty() { - return Err(NtfyError::EmptyResponse); - } - - // TODO: check the text? - - Ok(()) + self.inner.send(&self.url, payload).await } +} - #[cfg(feature = "blocking")] +#[cfg(feature = "blocking")] +impl Dispatcher { /// Send payload to ntfy server + #[inline] pub fn send(&self, payload: &Payload) -> Result<(), NtfyError> { - // Build request - let mut builder = self.agent.post(self.url.as_str()); - - // If markdown, set headers - if payload.markdown { - builder = builder.set("Markdown", "yes"); - } - - // Send request - let res: Response = builder.send_json(payload)?; - - // Get full response text - let text: String = res.into_string()?; - - if text.is_empty() { - return Err(NtfyError::EmptyResponse); - } - - // TODO: check the text? - - Ok(()) + self.inner.send(&self.url, payload) } } diff --git a/src/error.rs b/src/error.rs index 538e999..ccebe87 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,20 +1,23 @@ // Copyright (c) 2022 Yuki Kishimoto // Distributed under the MIT software license -use std::{fmt, io}; +use std::fmt; +#[cfg(feature = "blocking")] +use std::io; -#[cfg(not(feature = "blocking"))] +#[cfg(feature = "async")] use reqwest::header::InvalidHeaderValue; #[derive(Debug)] pub enum NtfyError { - #[cfg(not(feature = "blocking"))] - ReqwestError(reqwest::Error), + #[cfg(feature = "async")] + Reqwest(reqwest::Error), + #[cfg(feature = "blocking")] + Ureq(Box), #[cfg(feature = "blocking")] - UreqError(ureq::Error), - IoError(io::Error), + Io(io::Error), Url(url::ParseError), - #[cfg(not(feature = "blocking"))] + #[cfg(feature = "async")] InvalidHeaderValue(InvalidHeaderValue), EmptyResponse, } @@ -24,36 +27,38 @@ impl std::error::Error for NtfyError {} impl fmt::Display for NtfyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - #[cfg(not(feature = "blocking"))] - Self::ReqwestError(e) => write!(f, "{}", e), + #[cfg(feature = "async")] + Self::Reqwest(e) => write!(f, "{}", e), #[cfg(feature = "blocking")] - Self::UreqError(e) => write!(f, "{}", e), - Self::IoError(e) => write!(f, "{}", e), + Self::Ureq(e) => write!(f, "{}", e), + #[cfg(feature = "blocking")] + Self::Io(e) => write!(f, "{}", e), Self::Url(e) => write!(f, "{}", e), - #[cfg(not(feature = "blocking"))] + #[cfg(feature = "async")] Self::InvalidHeaderValue(e) => write!(f, "{}", e), Self::EmptyResponse => write!(f, "Empty Response"), } } } -#[cfg(not(feature = "blocking"))] +#[cfg(feature = "async")] impl From for NtfyError { fn from(e: reqwest::Error) -> Self { - Self::ReqwestError(e) + Self::Reqwest(e) } } #[cfg(feature = "blocking")] impl From for NtfyError { fn from(e: ureq::Error) -> Self { - Self::UreqError(e) + Self::Ureq(Box::new(e)) } } +#[cfg(feature = "blocking")] impl From for NtfyError { fn from(e: io::Error) -> Self { - Self::IoError(e) + Self::Io(e) } } @@ -63,7 +68,7 @@ impl From for NtfyError { } } -#[cfg(not(feature = "blocking"))] +#[cfg(feature = "async")] impl From for NtfyError { fn from(e: InvalidHeaderValue) -> Self { Self::InvalidHeaderValue(e) diff --git a/src/lib.rs b/src/lib.rs index 073c9f8..ad41fb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,12 @@ // Copyright (c) 2022 Yuki Kishimoto // Distributed under the MIT software license -#![doc = include_str!("../README.md")] +//! Ntfy + +#![cfg_attr(feature = "async", doc = include_str!("../README.md"))] + +#[cfg(not(any(feature = "async", feature = "blocking")))] +compile_error!("at least one of the `async` or `blocking` features must be enabled"); #[macro_use] extern crate serde;