guests/toy-dns/src/lib.rs
//! **A toy DNS guest** — what proves the DNS writer ABI end to end without a provider: every
//! door is ONE request to the block's `api` (a test's fake, plain http on loopback) over the
//! host's `http` door. The guest sets no `Authorization` header and reads no secret: the
//! host presents the token (`http_auth`, the fed secret by name), so a fake that sees the
//! right bearer proves the host found the secret where the feed wrote it.
//!
//! | door | request |
//! |---|---|
//! | `write` | `POST {api}/records` with the records as JSON |
//! | `remove` | `POST {api}/remove` with the names as JSON |
//! | `listed` | `GET {api}/records` (a JSON array of records) |
//! | `settled` | always true (an API writer is settled at once) |
//! | `zone` | the block's `zone` (none: it cannot say) |
use monetize_dns_guest::product_guest::wire::HttpRequest;
use monetize_dns_guest::{DnsGuest, DnsRecord, Host, Manifest};
use serde_json::Value;
/// The name the block is declared under (`[dns_writers.toydns]`).
pub const NAME: &str = "toydns";
fn api(settings: &Value) -> Result<String, String> {
settings.get("api").and_then(Value::as_str).map(|a| a.trim_end_matches('/').to_string()).filter(|a| !a.is_empty()).ok_or_else(|| "toydns: the block names no api".to_string())
}
fn call(host: &dyn Host, method: &str, url: String, body: Vec<u8>) -> Result<Vec<u8>, String> {
let headers = if body.is_empty() { Vec::new() } else { vec![("content-type".to_string(), "application/json".to_string())] };
let r = host.http(&HttpRequest { method: method.into(), url, headers, body })?;
if !(200..300).contains(&r.status) {
return Err(format!("toydns {method}: HTTP {}: {}", r.status, String::from_utf8_lossy(&r.body)));
}
Ok(r.body)
}
#[derive(Default)]
pub struct ToyDns;
impl DnsGuest for ToyDns {
fn manifest(&self) -> Manifest {
Manifest::of_kind(monetize_dns_guest::kinds::DNS, NAME, &[monetize_dns_guest::grants::HTTP])
}
fn check(&self, _: &dyn Host, settings: &Value) -> Result<(), String> {
api(settings).map(|_| ())
}
fn write(&self, host: &dyn Host, settings: &Value, records: &[DnsRecord]) -> Result<String, String> {
let body = serde_json::to_vec(records).map_err(|e| e.to_string())?;
call(host, "POST", format!("{}/records", api(settings)?), body)?;
Ok(format!("toydns wrote {} record(s)", records.len()))
}
fn remove(&self, host: &dyn Host, settings: &Value, names: &[String]) -> Result<String, String> {
let body = serde_json::to_vec(names).map_err(|e| e.to_string())?;
call(host, "POST", format!("{}/remove", api(settings)?), body)?;
Ok(format!("toydns removed {} name(s)", names.len()))
}
fn listed(&self, host: &dyn Host, settings: &Value) -> Result<Vec<DnsRecord>, String> {
let body = call(host, "GET", format!("{}/records", api(settings)?), Vec::new())?;
serde_json::from_slice(&body).map_err(|e| format!("toydns listed: {e}"))
}
fn zone(&self, _: &dyn Host, settings: &Value) -> Result<Option<String>, String> {
Ok(settings.get("zone").and_then(Value::as_str).map(str::to_string))
}
}
monetize_dns_guest::export_dns!(ToyDns, grants = [http]);