use serde::{Deserialize, Serialize};
use super::{InternalVerb, VerbCategory};
pub struct DnsRecordUpsert;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsRecordUpsertInput {
pub zone: String,
pub name: String,
#[serde(rename = "type")]
pub record_type: String,
pub content: String,
#[serde(default = "ttl_auto")]
pub ttl: u32,
#[serde(default)]
pub proxied: bool,
}
fn ttl_auto() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsRecordUpsertOutput {
pub id: String,
}
impl InternalVerb for DnsRecordUpsert {
type Input = DnsRecordUpsertInput;
type Output = DnsRecordUpsertOutput;
const ID: &'static str = "dns.record.upsert";
const CATEGORY: VerbCategory = VerbCategory::Dns;
}
pub struct DnsRecordDelete;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsRecordDeleteInput {
pub zone: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
pub record_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsRecordDeleteOutput {
pub deleted: u32,
}
impl InternalVerb for DnsRecordDelete {
type Input = DnsRecordDeleteInput;
type Output = DnsRecordDeleteOutput;
const ID: &'static str = "dns.record.delete";
const CATEGORY: VerbCategory = VerbCategory::Dns;
}
pub struct DnsZoneList;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsZoneListInput {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsZoneEntry {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct DnsZoneListOutput {
pub zones: Vec<DnsZoneEntry>,
}
impl InternalVerb for DnsZoneList {
type Input = DnsZoneListInput;
type Output = DnsZoneListOutput;
const ID: &'static str = "dns.zone.list";
const CATEGORY: VerbCategory = VerbCategory::Dns;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verb_ids_match_canonical_namespace() {
assert_eq!(DnsRecordUpsert::ID, "dns.record.upsert");
assert_eq!(DnsRecordDelete::ID, "dns.record.delete");
assert_eq!(DnsZoneList::ID, "dns.zone.list");
for id in [DnsRecordUpsert::ID, DnsRecordDelete::ID, DnsZoneList::ID] {
assert!(id.starts_with("dns."), "{id}");
}
}
#[test]
fn verbs_are_under_dns_category() {
assert_eq!(DnsRecordUpsert::CATEGORY, VerbCategory::Dns);
assert_eq!(DnsRecordDelete::CATEGORY, VerbCategory::Dns);
assert_eq!(DnsZoneList::CATEGORY, VerbCategory::Dns);
}
#[test]
fn upsert_input_defaults_ttl_to_auto_and_proxied_false() {
let wire = r#"{"zone":"yah.dev","name":"yubaba.yah.dev","type":"CNAME","content":"t.cfargotunnel.com"}"#;
let parsed: DnsRecordUpsertInput = serde_json::from_str(wire).unwrap();
assert_eq!(parsed.ttl, 1, "default TTL should be 1 (automatic)");
assert!(!parsed.proxied, "default proxied should be false");
}
#[test]
fn upsert_input_type_renamed_in_wire() {
let wire = r#"{"zone":"yah.dev","name":"a.yah.dev","type":"A","content":"1.2.3.4","ttl":300,"proxied":true}"#;
let parsed: DnsRecordUpsertInput = serde_json::from_str(wire).unwrap();
assert_eq!(parsed.record_type, "A");
assert_eq!(parsed.ttl, 300);
assert!(parsed.proxied);
let back = serde_json::to_value(&parsed).unwrap();
assert!(back.get("type").is_some(), "should serialize as 'type'");
assert!(
back.get("record_type").is_none(),
"should not serialize as 'record_type'"
);
}
#[test]
fn delete_input_type_optional() {
let with_type = r#"{"zone":"yah.dev","name":"old.yah.dev","type":"CNAME"}"#;
let parsed: DnsRecordDeleteInput = serde_json::from_str(with_type).unwrap();
assert_eq!(parsed.record_type.as_deref(), Some("CNAME"));
let no_type = r#"{"zone":"yah.dev","name":"old.yah.dev"}"#;
let parsed: DnsRecordDeleteInput = serde_json::from_str(no_type).unwrap();
assert!(parsed.record_type.is_none());
}
#[test]
fn delete_input_omits_type_when_absent() {
let input = DnsRecordDeleteInput {
zone: "z".into(),
name: "n".into(),
record_type: None,
};
let wire = serde_json::to_value(&input).unwrap();
assert!(!wire.as_object().unwrap().contains_key("type"));
}
#[test]
fn delete_output_zero_is_not_an_error() {
let out = DnsRecordDeleteOutput { deleted: 0 };
let wire = serde_json::to_value(&out).unwrap();
assert_eq!(wire["deleted"], 0);
}
#[test]
fn zone_list_input_serializes_to_empty_object() {
let wire = serde_json::to_value(DnsZoneListInput::default()).unwrap();
assert_eq!(wire, serde_json::json!({}));
}
#[test]
fn zone_list_output_round_trips() {
let out = DnsZoneListOutput {
zones: vec![
DnsZoneEntry {
id: "z1".into(),
name: "yah.dev".into(),
},
DnsZoneEntry {
id: "z2".into(),
name: "noisetable.com".into(),
},
],
};
let wire = serde_json::to_string(&out).unwrap();
let back: DnsZoneListOutput = serde_json::from_str(&wire).unwrap();
assert_eq!(back.zones.len(), 2);
assert_eq!(back.zones[0].name, "yah.dev");
}
#[cfg(feature = "json-schema")]
#[test]
fn verbs_emit_schemas_via_for_verb() {
use super::super::VerbDescriptor;
let upsert = VerbDescriptor::for_verb::<DnsRecordUpsert>();
assert_eq!(upsert.id, "dns.record.upsert");
assert!(upsert.input_schema.to_string().contains("content"));
let delete = VerbDescriptor::for_verb::<DnsRecordDelete>();
assert_eq!(delete.id, "dns.record.delete");
assert!(delete.output_schema.to_string().contains("deleted"));
let list = VerbDescriptor::for_verb::<DnsZoneList>();
assert_eq!(list.id, "dns.zone.list");
assert!(list.output_schema.to_string().contains("zones"));
}
}