use nostr_sdk::hashes::{Hash, sha256};
fn suffix(preimage: &str) -> String {
sha256::Hash::hash(preimage.as_bytes())
.to_string()
.chars()
.take(8)
.collect()
}
pub fn normalize(input: &str, fallback: &str) -> String {
let slug: String = input
.trim()
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_whitespace() { '-' } else { c })
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-')
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-");
if slug.is_empty() {
fallback.to_string()
} else {
slug
}
}
pub fn header_dtag(name_singular: &str, pubkey_hex: &str) -> String {
let slug = normalize(name_singular, "list");
let sfx = suffix(&format!("header|{pubkey_hex}|{slug}"));
format!("{slug}--{sfx}")
}
pub fn item_dtag(parent_z: &str, anchor_value: &str) -> String {
let slug = normalize(anchor_value, "item");
let sfx = suffix(&format!("item|{parent_z}|{anchor_value}"));
format!("{slug}--{sfx}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_simple_lowercase() {
assert_eq!(normalize("Hello World", "x"), "hello-world");
}
#[test]
fn normalize_trims_whitespace() {
assert_eq!(normalize(" spaced ", "x"), "spaced");
}
#[test]
fn normalize_collapses_whitespace_runs() {
assert_eq!(normalize("a b c", "x"), "a-b-c");
}
#[test]
fn normalize_strips_special_chars() {
assert_eq!(
normalize("AI Agents! On @Nostr?", "x"),
"ai-agents-on-nostr"
);
}
#[test]
fn normalize_preserves_digits() {
assert_eq!(normalize("Web3 Tools 42", "x"), "web3-tools-42");
}
#[test]
fn normalize_strips_unicode() {
assert_eq!(normalize("café résumé", "x"), "caf-rsum");
}
#[test]
fn normalize_empty_input_returns_fallback() {
assert_eq!(normalize("", "item"), "item");
}
#[test]
fn normalize_all_special_chars_returns_fallback() {
assert_eq!(normalize("!@#$%^&*()", "list"), "list");
}
#[test]
fn normalize_only_whitespace_returns_fallback() {
assert_eq!(normalize(" ", "list"), "list");
}
#[test]
fn normalize_leading_trailing_hyphens_trimmed() {
assert_eq!(normalize("--hello--", "x"), "hello");
}
#[test]
fn normalize_repeated_hyphens_collapsed() {
assert_eq!(normalize("a---b", "x"), "a-b");
}
#[test]
fn normalize_numeric_only() {
assert_eq!(normalize("12345", "x"), "12345");
}
#[test]
fn header_dtag_format() {
let result = header_dtag("AI Agents on Nostr", "aabbccdd");
assert!(result.starts_with("ai-agents-on-nostr--"));
assert_eq!(result.len(), "ai-agents-on-nostr--".len() + 8);
}
#[test]
fn header_dtag_deterministic() {
let a = header_dtag("test", "pubkey1");
let b = header_dtag("test", "pubkey1");
assert_eq!(a, b);
}
#[test]
fn header_dtag_different_names_differ() {
let a = header_dtag("alpha", "pubkey1");
let b = header_dtag("beta", "pubkey1");
assert_ne!(a, b);
}
#[test]
fn header_dtag_different_pubkeys_differ() {
let a = header_dtag("test", "pubkey1");
let b = header_dtag("test", "pubkey2");
assert_ne!(a, b);
}
#[test]
fn header_dtag_empty_name_uses_fallback() {
let result = header_dtag("", "pubkey1");
assert!(result.starts_with("list--"));
}
#[test]
fn item_dtag_format() {
let result = item_dtag("39998:pk:my-list", "https://example.com/resource");
assert!(result.contains("--"));
let parts: Vec<&str> = result.rsplitn(2, "--").collect();
assert_eq!(parts[0].len(), 8); }
#[test]
fn item_dtag_deterministic() {
let a = item_dtag("parent-z", "https://example.com");
let b = item_dtag("parent-z", "https://example.com");
assert_eq!(a, b);
}
#[test]
fn item_dtag_different_parents_differ() {
let a = item_dtag("parent-a", "https://example.com");
let b = item_dtag("parent-b", "https://example.com");
assert_ne!(a, b);
}
#[test]
fn item_dtag_different_anchors_differ() {
let a = item_dtag("parent", "https://a.com");
let b = item_dtag("parent", "https://b.com");
assert_ne!(a, b);
}
#[test]
fn item_dtag_empty_anchor_uses_fallback() {
let result = item_dtag("parent-z", "");
assert!(result.starts_with("item--"));
}
#[test]
fn suffix_length_is_8() {
assert_eq!(suffix("anything").len(), 8);
}
#[test]
fn suffix_is_hex() {
let s = suffix("test-input");
assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn suffix_deterministic() {
assert_eq!(suffix("same"), suffix("same"));
}
#[test]
fn suffix_different_inputs_differ() {
assert_ne!(suffix("alpha"), suffix("beta"));
}
}