1use serde::{Deserialize, Serialize};
27
28use super::{InternalVerb, VerbCategory};
29
30pub struct DnsRecordUpsert;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
38pub struct DnsRecordUpsertInput {
39 pub zone: String,
42 pub name: String,
45 #[serde(rename = "type")]
47 pub record_type: String,
48 pub content: String,
51 #[serde(default = "ttl_auto")]
54 pub ttl: u32,
55 #[serde(default)]
59 pub proxied: bool,
60}
61
62fn ttl_auto() -> u32 {
63 1
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
69pub struct DnsRecordUpsertOutput {
70 pub id: String,
74}
75
76impl InternalVerb for DnsRecordUpsert {
77 type Input = DnsRecordUpsertInput;
78 type Output = DnsRecordUpsertOutput;
79 const ID: &'static str = "dns.record.upsert";
80 const CATEGORY: VerbCategory = VerbCategory::Dns;
81}
82
83pub struct DnsRecordDelete;
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
91pub struct DnsRecordDeleteInput {
92 pub zone: String,
94 pub name: String,
96 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
100 pub record_type: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
106pub struct DnsRecordDeleteOutput {
107 pub deleted: u32,
110}
111
112impl InternalVerb for DnsRecordDelete {
113 type Input = DnsRecordDeleteInput;
114 type Output = DnsRecordDeleteOutput;
115 const ID: &'static str = "dns.record.delete";
116 const CATEGORY: VerbCategory = VerbCategory::Dns;
117}
118
119pub struct DnsZoneList;
123
124#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
128pub struct DnsZoneListInput {}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
133pub struct DnsZoneEntry {
134 pub id: String,
136 pub name: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
143pub struct DnsZoneListOutput {
144 pub zones: Vec<DnsZoneEntry>,
145}
146
147impl InternalVerb for DnsZoneList {
148 type Input = DnsZoneListInput;
149 type Output = DnsZoneListOutput;
150 const ID: &'static str = "dns.zone.list";
151 const CATEGORY: VerbCategory = VerbCategory::Dns;
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn verb_ids_match_canonical_namespace() {
160 assert_eq!(DnsRecordUpsert::ID, "dns.record.upsert");
161 assert_eq!(DnsRecordDelete::ID, "dns.record.delete");
162 assert_eq!(DnsZoneList::ID, "dns.zone.list");
163 for id in [DnsRecordUpsert::ID, DnsRecordDelete::ID, DnsZoneList::ID] {
164 assert!(id.starts_with("dns."), "{id}");
165 }
166 }
167
168 #[test]
169 fn verbs_are_under_dns_category() {
170 assert_eq!(DnsRecordUpsert::CATEGORY, VerbCategory::Dns);
171 assert_eq!(DnsRecordDelete::CATEGORY, VerbCategory::Dns);
172 assert_eq!(DnsZoneList::CATEGORY, VerbCategory::Dns);
173 }
174
175 #[test]
176 fn upsert_input_defaults_ttl_to_auto_and_proxied_false() {
177 let wire = r#"{"zone":"yah.dev","name":"yubaba.yah.dev","type":"CNAME","content":"t.cfargotunnel.com"}"#;
178 let parsed: DnsRecordUpsertInput = serde_json::from_str(wire).unwrap();
179 assert_eq!(parsed.ttl, 1, "default TTL should be 1 (automatic)");
180 assert!(!parsed.proxied, "default proxied should be false");
181 }
182
183 #[test]
184 fn upsert_input_type_renamed_in_wire() {
185 let wire = r#"{"zone":"yah.dev","name":"a.yah.dev","type":"A","content":"1.2.3.4","ttl":300,"proxied":true}"#;
186 let parsed: DnsRecordUpsertInput = serde_json::from_str(wire).unwrap();
187 assert_eq!(parsed.record_type, "A");
188 assert_eq!(parsed.ttl, 300);
189 assert!(parsed.proxied);
190 let back = serde_json::to_value(&parsed).unwrap();
192 assert!(back.get("type").is_some(), "should serialize as 'type'");
193 assert!(
194 back.get("record_type").is_none(),
195 "should not serialize as 'record_type'"
196 );
197 }
198
199 #[test]
200 fn delete_input_type_optional() {
201 let with_type = r#"{"zone":"yah.dev","name":"old.yah.dev","type":"CNAME"}"#;
202 let parsed: DnsRecordDeleteInput = serde_json::from_str(with_type).unwrap();
203 assert_eq!(parsed.record_type.as_deref(), Some("CNAME"));
204
205 let no_type = r#"{"zone":"yah.dev","name":"old.yah.dev"}"#;
206 let parsed: DnsRecordDeleteInput = serde_json::from_str(no_type).unwrap();
207 assert!(parsed.record_type.is_none());
208 }
209
210 #[test]
211 fn delete_input_omits_type_when_absent() {
212 let input = DnsRecordDeleteInput {
213 zone: "z".into(),
214 name: "n".into(),
215 record_type: None,
216 };
217 let wire = serde_json::to_value(&input).unwrap();
218 assert!(!wire.as_object().unwrap().contains_key("type"));
219 }
220
221 #[test]
222 fn delete_output_zero_is_not_an_error() {
223 let out = DnsRecordDeleteOutput { deleted: 0 };
224 let wire = serde_json::to_value(&out).unwrap();
225 assert_eq!(wire["deleted"], 0);
226 }
227
228 #[test]
229 fn zone_list_input_serializes_to_empty_object() {
230 let wire = serde_json::to_value(DnsZoneListInput::default()).unwrap();
231 assert_eq!(wire, serde_json::json!({}));
232 }
233
234 #[test]
235 fn zone_list_output_round_trips() {
236 let out = DnsZoneListOutput {
237 zones: vec![
238 DnsZoneEntry {
239 id: "z1".into(),
240 name: "yah.dev".into(),
241 },
242 DnsZoneEntry {
243 id: "z2".into(),
244 name: "noisetable.com".into(),
245 },
246 ],
247 };
248 let wire = serde_json::to_string(&out).unwrap();
249 let back: DnsZoneListOutput = serde_json::from_str(&wire).unwrap();
250 assert_eq!(back.zones.len(), 2);
251 assert_eq!(back.zones[0].name, "yah.dev");
252 }
253
254 #[cfg(feature = "json-schema")]
255 #[test]
256 fn verbs_emit_schemas_via_for_verb() {
257 use super::super::VerbDescriptor;
258
259 let upsert = VerbDescriptor::for_verb::<DnsRecordUpsert>();
260 assert_eq!(upsert.id, "dns.record.upsert");
261 assert!(upsert.input_schema.to_string().contains("content"));
262
263 let delete = VerbDescriptor::for_verb::<DnsRecordDelete>();
264 assert_eq!(delete.id, "dns.record.delete");
265 assert!(delete.output_schema.to_string().contains("deleted"));
266
267 let list = VerbDescriptor::for_verb::<DnsZoneList>();
268 assert_eq!(list.id, "dns.zone.list");
269 assert!(list.output_schema.to_string().contains("zones"));
270 }
271}