Skip to main content

cloudflare_dns/api/
models.rs

1/// Data models for Cloudflare DNS records and API responses.
2///
3/// This module contains all the types used for serializing and deserializing
4/// Cloudflare API requests and responses.
5use serde::{Deserialize, Serialize};
6
7/// Represents a DNS record in Cloudflare's system.
8///
9/// This is the primary data structure used throughout the application.
10/// The `id` field is `Option<String>` because it's not present when creating new records.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct DnsRecord {
13    /// The unique identifier for the record (not present when creating new records)
14    pub id: Option<String>,
15
16    /// The DNS record type (A, AAAA, CNAME, MX, TXT, etc.)
17    #[serde(rename = "type")]
18    pub record_type: String,
19
20    /// The DNS record name (e.g., "example.com" or "www.example.com")
21    pub name: String,
22
23    /// The record content (IP address, hostname, etc.)
24    pub content: String,
25
26    /// Time-to-live in seconds (use `1` for automatic)
27    pub ttl: Option<i64>,
28
29    /// Whether the record is proxied through Cloudflare (orange cloud)
30    pub proxied: Option<bool>,
31
32    /// Optional comment for the record
33    pub comment: Option<String>,
34}
35
36/// Generic API response wrapper from Cloudflare.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ApiResponse<T> {
39    /// Whether the request was successful
40    pub success: bool,
41
42    /// Error details if the request failed
43    pub errors: Vec<ApiError>,
44
45    /// Informational messages
46    pub messages: Vec<String>,
47
48    /// The response data
49    pub result: Option<T>,
50}
51
52/// Individual error from Cloudflare API.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ApiError {
55    /// Error code
56    pub code: i64,
57
58    /// Error message
59    pub message: String,
60}
61
62/// DNS record as returned by the Cloudflare API (always has an ID).
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DnsRecordResponse {
65    pub id: String,
66
67    #[serde(rename = "type")]
68    pub record_type: String,
69
70    pub name: String,
71
72    pub content: String,
73
74    pub ttl: i64,
75
76    pub proxied: bool,
77
78    pub comment: Option<String>,
79}
80
81/// Zone information from Cloudflare.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ZoneResponse {
84    pub id: String,
85
86    pub name: String,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_dns_record_serialize_minimal() {
95        let record = DnsRecord {
96            id: None,
97            record_type: "A".to_string(),
98            name: "example.com".to_string(),
99            content: "192.168.1.1".to_string(),
100            ttl: None,
101            proxied: None,
102            comment: None,
103        };
104        let json = serde_json::to_string(&record).unwrap();
105        assert!(json.contains("\"type\":\"A\""));
106        assert!(json.contains("\"name\":\"example.com\""));
107        assert!(json.contains("\"content\":\"192.168.1.1\""));
108    }
109
110    #[test]
111    fn test_dns_record_serialize_full() {
112        let record = DnsRecord {
113            id: Some("abc123".to_string()),
114            record_type: "AAAA".to_string(),
115            name: "test.example.com".to_string(),
116            content: "2001:db8::1".to_string(),
117            ttl: Some(300),
118            proxied: Some(true),
119            comment: Some("Test record".to_string()),
120        };
121        let json = serde_json::to_string(&record).unwrap();
122        assert!(json.contains("\"id\":\"abc123\""));
123        assert!(json.contains("\"type\":\"AAAA\""));
124        assert!(json.contains("\"proxied\":true"));
125        assert!(json.contains("\"comment\":\"Test record\""));
126    }
127
128    #[test]
129    fn test_dns_record_deserialize() {
130        let json = r#"{
131            "id": "def456",
132            "type": "CNAME",
133            "name": "www.example.com",
134            "content": "example.com",
135            "ttl": 3600,
136            "proxied": false
137        }"#;
138        let record: DnsRecord = serde_json::from_str(json).unwrap();
139        assert_eq!(record.id, Some("def456".to_string()));
140        assert_eq!(record.record_type, "CNAME");
141        assert_eq!(record.name, "www.example.com");
142        assert_eq!(record.content, "example.com");
143        assert_eq!(record.ttl, Some(3600));
144        assert_eq!(record.proxied, Some(false));
145    }
146
147    #[test]
148    fn test_dns_record_deserialize_with_comment() {
149        let json = r#"{
150            "id": "ghi789",
151            "type": "A",
152            "name": "api.example.com",
153            "content": "10.0.0.1",
154            "ttl": 1,
155            "proxied": true,
156            "comment": "API server"
157        }"#;
158        let record: DnsRecord = serde_json::from_str(json).unwrap();
159        assert_eq!(record.comment, Some("API server".to_string()));
160    }
161
162    #[test]
163    fn test_dns_record_deserialize_without_comment() {
164        let json = r#"{
165            "id": "jkl012",
166            "type": "MX",
167            "name": "example.com",
168            "content": "mail.example.com",
169            "ttl": 3600,
170            "proxied": false
171        }"#;
172        let record: DnsRecord = serde_json::from_str(json).unwrap();
173        assert_eq!(record.comment, None);
174    }
175
176    #[test]
177    fn test_dns_record_type_renamed_correctly() {
178        let json = r#"{"type": "TXT", "name": "example.com", "content": "v=spf1"}"#;
179        let record: DnsRecord = serde_json::from_str(json).unwrap();
180        assert_eq!(record.record_type, "TXT");
181    }
182
183    #[test]
184    fn test_api_response_structure() {
185        let json = r#"{
186            "success": true,
187            "errors": [],
188            "messages": [],
189            "result": {
190                "id": "zone123",
191                "name": "example.com"
192            }
193        }"#;
194        let response: ApiResponse<ZoneResponse> = serde_json::from_str(json).unwrap();
195        assert!(response.success);
196        assert!(response.errors.is_empty());
197        assert!(response.messages.is_empty());
198        assert!(response.result.is_some());
199        let zone = response.result.unwrap();
200        assert_eq!(zone.id, "zone123");
201        assert_eq!(zone.name, "example.com");
202    }
203
204    #[test]
205    fn test_api_response_with_errors() {
206        let json = r#"{
207            "success": false,
208            "errors": [{"code": 1000, "message": "Invalid API token"}],
209            "messages": [],
210            "result": null
211        }"#;
212        let response: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
213        assert!(!response.success);
214        assert_eq!(response.errors.len(), 1);
215        assert_eq!(response.errors[0].code, 1000);
216        assert_eq!(response.errors[0].message, "Invalid API token");
217    }
218
219    #[test]
220    fn test_dns_record_response_structure() {
221        let json = r#"{
222            "id": "rec123",
223            "type": "A",
224            "name": "example.com",
225            "content": "192.168.1.1",
226            "ttl": 300,
227            "proxied": false,
228            "comment": "Test"
229        }"#;
230        let record: DnsRecordResponse = serde_json::from_str(json).unwrap();
231        assert_eq!(record.id, "rec123");
232        assert_eq!(record.record_type, "A");
233        assert_eq!(record.content, "192.168.1.1");
234        assert_eq!(record.ttl, 300);
235        assert!(!record.proxied);
236        assert_eq!(record.comment, Some("Test".to_string()));
237    }
238
239    #[test]
240    fn test_zone_response_structure() {
241        let json = r#"{
242            "id": "zone456",
243            "name": "mydomain.org"
244        }"#;
245        let zone: ZoneResponse = serde_json::from_str(json).unwrap();
246        assert_eq!(zone.id, "zone456");
247        assert_eq!(zone.name, "mydomain.org");
248    }
249
250    #[test]
251    fn test_dns_record_roundtrip() {
252        let original = DnsRecord {
253            id: Some("rt001".to_string()),
254            record_type: "SRV".to_string(),
255            name: "_sip._tcp.example.com".to_string(),
256            content: "10 60 5060 sip.example.com".to_string(),
257            ttl: Some(3600),
258            proxied: Some(false),
259            comment: Some("SIP service".to_string()),
260        };
261        let json = serde_json::to_string(&original).unwrap();
262        let deserialized: DnsRecord = serde_json::from_str(&json).unwrap();
263        assert_eq!(deserialized.id, original.id);
264        assert_eq!(deserialized.record_type, original.record_type);
265        assert_eq!(deserialized.name, original.name);
266        assert_eq!(deserialized.content, original.content);
267        assert_eq!(deserialized.ttl, original.ttl);
268        assert_eq!(deserialized.proxied, original.proxied);
269        assert_eq!(deserialized.comment, original.comment);
270    }
271
272    #[test]
273    fn test_dns_record_clone() {
274        let record = DnsRecord {
275            id: Some("clone001".to_string()),
276            record_type: "A".to_string(),
277            name: "test.example.com".to_string(),
278            content: "10.0.0.1".to_string(),
279            ttl: Some(60),
280            proxied: Some(true),
281            comment: None,
282        };
283        let cloned = record.clone();
284        assert_eq!(cloned.id, record.id);
285        assert_eq!(cloned.record_type, record.record_type);
286        assert_eq!(cloned.content, record.content);
287    }
288
289    #[test]
290    fn test_dns_record_debug_format() {
291        let record = DnsRecord {
292            id: Some("debug001".to_string()),
293            record_type: "NS".to_string(),
294            name: "example.com".to_string(),
295            content: "ns1.example.com".to_string(),
296            ttl: Some(86400),
297            proxied: Some(false),
298            comment: None,
299        };
300        let debug_str = format!("{:?}", record);
301        assert!(debug_str.contains("debug001"));
302        assert!(debug_str.contains("NS"));
303        assert!(debug_str.contains("ns1.example.com"));
304    }
305}