From db9059e0819c9477f561424fb7f22a2b35219d76 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:14:58 +0700 Subject: [PATCH] feat: add duration-based retry helper --- CHANGELOG.md | 4 ++++ README.md | 5 +++-- src/response.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ab996..f626d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [Unreleased] +### Added +- `ErrorResponse::with_retry_after_duration` helper for specifying retry advice via `Duration`. + ## [0.3.3] - 2025-09-11 ### Added - `ErrorResponse::status_code()` exposing validated `StatusCode`. diff --git a/README.md b/README.md index 23e8a57..5e40ba1 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,11 @@ fn do_work(flag: bool) -> AppResult<()> { ~~~rust use masterror::{AppError, AppErrorKind, AppCode, ErrorResponse}; +use std::time::Duration; let app_err = AppError::new(AppErrorKind::Unauthorized, "Token expired"); let resp: ErrorResponse = (&app_err).into() - .with_retry_after_secs(30) + .with_retry_after_duration(Duration::from_secs(30)) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#); assert_eq!(resp.status, 401); @@ -244,7 +245,7 @@ assert_eq!(app.kind, AppErrorKind::RateLimited); Migration 0.2 → 0.3 - Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy -- New helpers: `.with_retry_after_secs`, `.with_www_authenticate` +- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate` - `ErrorResponse::new_legacy` is temporary shim diff --git a/src/response.rs b/src/response.rs index 4847465..f2003b2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -16,7 +16,9 @@ //! (`serde_json::Value` if the `serde_json` feature is enabled, otherwise //! plain text) //! - [`retry`](ErrorResponse::retry): optional retry advice, rendered as the -//! `Retry-After` header in HTTP adapters +//! `Retry-After` header in HTTP adapters; set via +//! [`with_retry_after_secs`](ErrorResponse::with_retry_after_secs) or +//! [`with_retry_after_duration`](ErrorResponse::with_retry_after_duration) //! - [`www_authenticate`](ErrorResponse::www_authenticate): optional //! authentication challenge string, rendered as the `WWW-Authenticate` header //! @@ -27,11 +29,13 @@ //! # Example //! //! ```rust +//! use std::time::Duration; +//! //! use masterror::{AppCode, ErrorResponse}; //! //! let resp = ErrorResponse::new(404, AppCode::NotFound, "User not found") //! .expect("status") -//! .with_retry_after_secs(30); +//! .with_retry_after_duration(Duration::from_secs(30)); //! ``` //! //! With `serde_json` enabled: @@ -55,7 +59,10 @@ //! stable machine-readable code. A temporary [`ErrorResponse::new_legacy`] is //! provided as a deprecated shim. -use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + time::Duration +}; use http::StatusCode; use serde::{Deserialize, Serialize}; @@ -155,7 +162,9 @@ impl ErrorResponse { /// Attach retry advice (number of seconds). /// - /// When present, integrations set the `Retry-After` header automatically. + /// See [`with_retry_after_duration`](Self::with_retry_after_duration) for + /// using a [`Duration`]. When present, integrations set the `Retry-After` + /// header automatically. #[must_use] pub fn with_retry_after_secs(mut self, secs: u64) -> Self { self.retry = Some(RetryAdvice { @@ -164,6 +173,28 @@ impl ErrorResponse { self } + /// Attach retry advice as a [`Duration`]. + /// + /// Equivalent to [`with_retry_after_secs`](Self::with_retry_after_secs). + /// When present, integrations set the `Retry-After` header automatically. + /// + /// # Examples + /// + /// ```rust + /// use std::time::Duration; + /// + /// use masterror::{AppCode, ErrorResponse}; + /// + /// let resp = ErrorResponse::new(503, AppCode::Internal, "retry later") + /// .expect("status") + /// .with_retry_after_duration(Duration::from_secs(60)); + /// assert_eq!(resp.retry.expect("retry").after_seconds, 60); + /// ``` + #[must_use] + pub fn with_retry_after_duration(self, dur: Duration) -> Self { + self.with_retry_after_secs(dur.as_secs()) + } + /// Attach an authentication challenge string. /// /// When present, integrations set the `WWW-Authenticate` header @@ -368,6 +399,16 @@ mod tests { assert_eq!(e.www_authenticate.as_deref(), Some(r#"Bearer realm="api""#)); } + #[test] + fn with_retry_after_duration_attaches_advice() { + use std::time::Duration; + + let e = ErrorResponse::new(429, AppCode::RateLimited, "slow down") + .expect("status") + .with_retry_after_duration(Duration::from_secs(42)); + assert_eq!(e.retry.unwrap().after_seconds, 42); + } + #[test] fn status_code_maps_invalid_to_internal_server_error() { use http::StatusCode;