1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::error::{Error, Result};
8
9pub const REDIRECT_CODES: [u16; 5] = [301, 302, 303, 307, 308];
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Redirect {
20 pub name: String,
23 pub zone_id: Uuid,
25 pub code: u16,
27 pub location: String,
29 pub tls: String,
32 pub created_at: i64,
34 pub updated_at: i64,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub certificate: Option<RedirectCertificate>,
39}
40
41impl Redirect {
42 pub fn tls_mode(&self) -> Option<RedirectTls> {
44 self.tls.parse().ok()
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum RedirectTls {
51 Auto,
53 Manual,
56 None,
58}
59
60impl RedirectTls {
61 pub fn as_wire(self) -> &'static str {
63 match self {
64 Self::Auto => "Auto",
65 Self::Manual => "Manual",
66 Self::None => "None",
67 }
68 }
69}
70
71impl fmt::Display for RedirectTls {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(self.as_wire())
74 }
75}
76
77impl FromStr for RedirectTls {
78 type Err = Error;
79
80 fn from_str(text: &str) -> Result<Self> {
82 match text.trim().to_ascii_lowercase().as_str() {
83 "auto" => Ok(Self::Auto),
84 "manual" => Ok(Self::Manual),
85 "none" => Ok(Self::None),
86 _ => Err(Error::InvalidInput(format!(
87 "unknown redirect tls {text:?} (Auto, Manual or None)"
88 ))),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RedirectCertificate {
96 pub status: String,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub certificate_id: Option<Uuid>,
102 pub renew_at: i64,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub last_error: Option<String>,
107}
108
109impl RedirectCertificate {
110 pub fn kind(&self) -> Option<AcmeStatus> {
112 self.status.parse().ok()
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub enum AcmeStatus {
119 Pending,
121 Valid,
123 Failed,
125}
126
127impl FromStr for AcmeStatus {
128 type Err = Error;
129
130 fn from_str(text: &str) -> Result<Self> {
132 match text.trim().to_ascii_lowercase().as_str() {
133 "pending" => Ok(Self::Pending),
134 "valid" => Ok(Self::Valid),
135 "failed" => Ok(Self::Failed),
136 _ => Err(Error::InvalidInput(format!(
137 "unknown certificate status {text:?} (Pending, Valid or Failed)"
138 ))),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize)]
145pub struct NewRedirect {
146 pub zone_id: Uuid,
149 pub name: String,
152 pub code: u16,
155 pub location: String,
157 #[serde(skip_serializing_if = "Option::is_none")]
160 pub tls: Option<RedirectTls>,
161}
162
163#[derive(Debug, Clone, Default, Serialize)]
166pub struct RedirectPatch {
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub code: Option<u16>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub location: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub tls: Option<RedirectTls>,
176}
177
178impl RedirectPatch {
179 pub fn is_empty(&self) -> bool {
181 self.code.is_none() && self.location.is_none() && self.tls.is_none()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn redirect_with_certificate_deserializes() {
191 let redirect: Redirect = serde_json::from_value(serde_json::json!({
192 "name": "go.example.com", "zone_id": "00000000-0000-0000-0000-000000000001",
193 "code": 302, "location": "https://example.com/", "tls": "Auto",
194 "created_at": 1, "updated_at": 2,
195 "certificate": { "status": "Pending", "renew_at": 3 }
196 }))
197 .unwrap();
198 assert_eq!(redirect.tls_mode(), Some(RedirectTls::Auto));
199 let cert = redirect.certificate.unwrap();
200 assert_eq!(cert.kind(), Some(AcmeStatus::Pending));
201 assert_eq!(cert.certificate_id, None);
202 }
203
204 #[test]
205 fn unknown_tls_is_kept() {
206 let redirect: Redirect = serde_json::from_value(serde_json::json!({
207 "name": "go.example.com", "zone_id": "00000000-0000-0000-0000-000000000001",
208 "code": 302, "location": "https://example.com/", "tls": "Something",
209 "created_at": 1, "updated_at": 2
210 }))
211 .unwrap();
212 assert_eq!(redirect.tls, "Something");
213 assert_eq!(redirect.tls_mode(), None);
214 }
215
216 #[test]
217 fn new_redirect_wire_shape() {
218 let new = NewRedirect {
219 zone_id: Uuid::nil(),
220 name: "go.example.com".into(),
221 code: 302,
222 location: "https://example.com/".into(),
223 tls: Some(RedirectTls::None),
224 };
225 assert_eq!(
226 serde_json::to_value(&new).unwrap(),
227 serde_json::json!({
228 "zone_id": "00000000-0000-0000-0000-000000000000",
229 "name": "go.example.com", "code": 302,
230 "location": "https://example.com/", "tls": "None",
231 })
232 );
233 }
234}