Skip to main content

tatara_lisp_script/stdlib/
hash.rs

1//! Hash functions.
2//!
3//!   (sha256 STR)   → hex digest string
4//!   (slugify NAME TYPE) → slug matching Pangea::Architectures::
5//!                         CloudflareDnsRecords.derive_slug. Useful when a
6//!                         script is emitting tofu import commands whose
7//!                         resource addresses come from the Ruby
8//!                         architecture's naming convention.
9
10use std::sync::Arc;
11
12use sha2::{Digest, Sha256};
13use tatara_lisp_eval::{Arity, Interpreter, Value};
14
15use crate::script_ctx::ScriptCtx;
16use crate::stdlib::env::str_arg;
17
18pub fn install(interp: &mut Interpreter<ScriptCtx>) {
19    interp.register_fn(
20        "sha256",
21        Arity::Exact(1),
22        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
23            let s = str_arg(&args[0], "sha256", sp)?;
24            let digest = Sha256::digest(s.as_bytes());
25            Ok(Value::Str(Arc::from(hex::encode(digest))))
26        },
27    );
28
29    interp.register_fn(
30        "slugify",
31        Arity::Exact(2),
32        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
33            let name = str_arg(&args[0], "slugify", sp)?;
34            let kind = str_arg(&args[1], "slugify", sp)?;
35            Ok(Value::Str(Arc::from(derive_slug(&name, &kind))))
36        },
37    );
38}
39
40/// Mirror of Pangea::Architectures::CloudflareDnsRecords.derive_slug so
41/// tlisp scripts emit the same Terraform resource addresses as the Ruby
42/// architecture. Any drift here is a bug — keep this and the Ruby method
43/// in lockstep.
44pub fn derive_slug(name: &str, kind: &str) -> String {
45    let normalized = if name == "@" || name.is_empty() {
46        "root".to_string()
47    } else {
48        let mut s = name.replace('.', "_");
49        // "*_foo" → "wildcard_foo"
50        if let Some(stripped) = s.strip_prefix("*_") {
51            s = format!("wildcard_{stripped}");
52        } else if let Some(stripped) = s.strip_prefix("*") {
53            s = format!("wildcard{stripped}");
54        }
55        // Collapse runs of underscores so "resend__domainkey" → "resend_domainkey".
56        while s.contains("__") {
57            s = s.replace("__", "_");
58        }
59        s
60    };
61    format!("{}_{}", normalized, kind.to_lowercase())
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn apex_normalizes_to_root() {
70        assert_eq!(derive_slug("@", "CNAME"), "root_cname");
71    }
72
73    #[test]
74    fn dots_become_underscores() {
75        assert_eq!(derive_slug("api.staging", "CNAME"), "api_staging_cname");
76    }
77
78    #[test]
79    fn wildcard_expands() {
80        assert_eq!(derive_slug("*.staging", "CNAME"), "wildcard_staging_cname");
81    }
82
83    #[test]
84    fn underscore_prefixed_dkim_preserved() {
85        assert_eq!(
86            derive_slug("resend._domainkey", "TXT"),
87            "resend_domainkey_txt"
88        );
89    }
90
91    #[test]
92    fn plain_name_lowercases_type() {
93        assert_eq!(derive_slug("www", "CNAME"), "www_cname");
94        assert_eq!(derive_slug("send", "MX"), "send_mx");
95    }
96}