1#[cfg(all(not(feature = "std"), feature = "alloc"))]
19use alloc::string::String;
20use core::fmt;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
36#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
37pub struct Deprecated {
38 pub sunset: String,
42
43 #[cfg_attr(
45 feature = "serde",
46 serde(default, skip_serializing_if = "Option::is_none")
47 )]
48 pub link: Option<String>,
49}
50
51impl Deprecated {
52 #[must_use]
64 pub fn new(sunset: impl Into<String>) -> Self {
65 Self {
66 sunset: sunset.into(),
67 link: None,
68 }
69 }
70
71 #[must_use]
83 pub fn with_link(mut self, link: impl Into<String>) -> Self {
84 self.link = Some(link.into());
85 self
86 }
87
88 #[must_use]
92 pub fn deprecation_header_value(&self) -> &'static str {
93 "true"
94 }
95
96 #[must_use]
107 pub fn sunset_header_value(&self) -> &str {
108 &self.sunset
109 }
110
111 #[must_use]
128 #[cfg(any(feature = "std", feature = "alloc"))]
129 pub fn link_header_value(&self) -> Option<String> {
130 #[cfg(all(not(feature = "std"), feature = "alloc"))]
131 use alloc::format;
132 self.link
133 .as_deref()
134 .map(|url| format!("<{url}>; rel=\"successor-version\""))
135 }
136
137 #[cfg(feature = "http")]
144 pub fn inject_headers(
145 &self,
146 headers: &mut http::HeaderMap,
147 ) -> Result<(), http::header::InvalidHeaderValue> {
148 use http::header::{HeaderName, HeaderValue};
149
150 headers.insert(
151 HeaderName::from_static("deprecation"),
152 HeaderValue::from_static("true"),
153 );
154 headers.insert(
155 HeaderName::from_static("sunset"),
156 HeaderValue::from_str(&self.sunset)?,
157 );
158 if let Some(link_val) = self.link_header_value() {
159 headers.insert(http::header::LINK, HeaderValue::from_str(&link_val)?);
160 }
161 Ok(())
162 }
163}
164
165impl fmt::Display for Deprecated {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 write!(f, "Deprecated(sunset={})", self.sunset)
172 }
173}
174
175#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn new_sets_sunset() {
185 let d = Deprecated::new("2025-12-31");
186 assert_eq!(d.sunset, "2025-12-31");
187 assert!(d.link.is_none());
188 }
189
190 #[test]
191 fn with_link() {
192 let d = Deprecated::new("2025-12-31").with_link("https://example.com/v2");
193 assert_eq!(d.link.as_deref(), Some("https://example.com/v2"));
194 }
195
196 #[test]
197 fn header_values() {
198 let d = Deprecated::new("2025-12-31");
199 assert_eq!(d.deprecation_header_value(), "true");
200 assert_eq!(d.sunset_header_value(), "2025-12-31");
201 }
202
203 #[cfg(any(feature = "std", feature = "alloc"))]
204 #[test]
205 fn link_header_value_format() {
206 let d = Deprecated::new("2025-12-31").with_link("https://example.com/v2");
207 assert_eq!(
208 d.link_header_value().as_deref(),
209 Some("<https://example.com/v2>; rel=\"successor-version\"")
210 );
211 }
212
213 #[cfg(any(feature = "std", feature = "alloc"))]
214 #[test]
215 fn link_header_value_none() {
216 let d = Deprecated::new("2025-12-31");
217 assert!(d.link_header_value().is_none());
218 }
219
220 #[cfg(feature = "serde")]
221 #[test]
222 fn serde_round_trip() {
223 let d = Deprecated::new("2025-12-31").with_link("https://example.com/v2");
224 let json = serde_json::to_value(&d).unwrap();
225 assert_eq!(json["sunset"], "2025-12-31");
226 assert_eq!(json["link"], "https://example.com/v2");
227 let back: Deprecated = serde_json::from_value(json).unwrap();
228 assert_eq!(back, d);
229 }
230
231 #[cfg(feature = "serde")]
232 #[test]
233 fn serde_omits_null_link() {
234 let d = Deprecated::new("2025-12-31");
235 let json = serde_json::to_value(&d).unwrap();
236 assert!(json.get("link").is_none());
237 }
238
239 #[test]
240 fn display_format() {
241 let d = Deprecated::new("2025-12-31");
242 assert_eq!(d.to_string(), "Deprecated(sunset=2025-12-31)");
243 }
244
245 #[cfg(feature = "http")]
246 #[test]
247 fn inject_headers_sets_deprecation_and_sunset() {
248 let d = Deprecated::new("Sat, 31 Dec 2025 00:00:00 GMT");
249 let mut headers = http::HeaderMap::new();
250 d.inject_headers(&mut headers).unwrap();
251 assert_eq!(headers["deprecation"], "true");
252 assert_eq!(headers["sunset"], "Sat, 31 Dec 2025 00:00:00 GMT");
253 assert!(headers.get(http::header::LINK).is_none());
254 }
255
256 #[cfg(feature = "http")]
257 #[test]
258 fn inject_headers_with_link() {
259 let d = Deprecated::new("2025-12-31").with_link("https://example.com/v2");
260 let mut headers = http::HeaderMap::new();
261 d.inject_headers(&mut headers).unwrap();
262 assert_eq!(headers["deprecation"], "true");
263 let link = headers[http::header::LINK].to_str().unwrap();
264 assert!(link.contains("https://example.com/v2"));
265 assert!(link.contains("successor-version"));
266 }
267}