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