use crate::{Error, Result, SafeUrl};
use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub(crate) type PublicName = String;
#[derive(Debug, PartialEq, Eq, Default, Serialize, Deserialize, Clone)]
pub struct NrsMap {
pub map: BTreeMap<PublicName, SafeUrl>,
}
impl NrsMap {
pub fn get(&self, public_name: &str) -> Result<Option<SafeUrl>> {
match self.map.get(public_name) {
Some(link) => {
debug!(
"NRS: public name resolution is: {} => {}",
public_name, link
);
Ok(Some(link.clone()))
}
None => {
debug!("NRS: No link found for public name: {}", public_name);
if self.public_name_contains_subname(public_name) {
return Err(Error::ContentError(format!(
"Link not found in NRS Map Container for public name: \"{public_name}\""
)));
}
Ok(None)
}
}
}
pub fn get_map_summary(&self) -> Vec<(String, String)> {
let mut v = self
.map
.iter()
.map(|x| (x.0.clone(), x.1.to_string()))
.collect::<Vec<(String, String)>>();
v.sort_by(|a, b| a.0.len().cmp(&b.0.len()));
v
}
fn public_name_contains_subname(&self, public_name: &str) -> bool {
let mut parts = public_name.split('.');
parts.next_back();
let subnames = parts.collect::<Vec<&str>>().join(".");
!subnames.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SafeUrl;
use anyhow::{anyhow, Result};
use assert_matches::assert_matches;
#[test]
fn get_should_return_link_for_subname() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
nrs_map
.map
.insert("example".to_string(), SafeUrl::from_url("safe://example")?);
let subname_url = SafeUrl::from_url("safe://a.example")?;
nrs_map
.map
.insert("a.example".to_string(), subname_url.clone());
let url = nrs_map.get("a.example")?;
assert_eq!(
url.ok_or_else(|| anyhow!("url should not be None"))?,
subname_url
);
Ok(())
}
#[test]
fn get_should_return_link_for_multi_subname() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
nrs_map
.map
.insert("example".to_string(), SafeUrl::from_url("safe://example")?);
nrs_map.map.insert(
"a.example".to_string(),
SafeUrl::from_url("safe://a.example")?,
);
let subname_url = SafeUrl::from_url("safe://a.b.example")?;
nrs_map
.map
.insert("a.b.example".to_string(), subname_url.clone());
let url = nrs_map.get("a.b.example")?;
assert_eq!(
url.ok_or_else(|| anyhow!("url should not be None"))?,
subname_url
);
Ok(())
}
#[test]
fn get_should_return_link_for_topname() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
let topname_url = SafeUrl::from_url("safe://example")?;
nrs_map
.map
.insert("example".to_string(), topname_url.clone());
nrs_map.map.insert(
"a.example".to_string(),
SafeUrl::from_url("safe://a.example")?,
);
let url = nrs_map.get("example")?;
assert_eq!(
url.ok_or_else(|| anyhow!("url should not be None"))?,
topname_url
);
Ok(())
}
#[test]
fn get_should_return_error_for_non_existent_subname() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
nrs_map
.map
.insert("example".to_string(), SafeUrl::from_url("safe://example")?);
nrs_map.map.insert(
"a.example".to_string(),
SafeUrl::from_url("safe://a.example")?,
);
nrs_map.map.insert(
"a.b.example".to_string(),
SafeUrl::from_url("safe://a.b.example")?,
);
assert_matches!(
nrs_map.get("a.b.c.example"), Err(Error::ContentError(err))
if err.as_str() == "Link not found in NRS Map Container for public name: \"a.b.c.example\""
);
Ok(())
}
#[test]
fn get_should_return_none_for_container_xorurl() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
let topname_url = SafeUrl::from_url("safe://example")?;
nrs_map
.map
.insert("example".to_string(), topname_url.clone());
nrs_map.map.insert(
"a.example".to_string(),
SafeUrl::from_url("safe://a.example")?,
);
nrs_map.map.insert(
"a.b.example".to_string(),
SafeUrl::from_url("safe://a.b.example")?,
);
let container_xorurl = SafeUrl::from_url(&topname_url.to_xorurl_string())?;
let url = nrs_map.get(container_xorurl.public_name())?;
assert!(url.is_none());
Ok(())
}
#[test]
fn get_should_return_none_for_topname_when_topname_has_no_link() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
nrs_map.map.insert(
"a.example".to_string(),
SafeUrl::from_url("safe://a.example")?,
);
nrs_map.map.insert(
"a.b.example".to_string(),
SafeUrl::from_url("safe://a.b.example")?,
);
let url = nrs_map.get("example")?;
assert!(url.is_none());
Ok(())
}
#[test]
fn get_map_summary_should_return_map_entries() -> Result<()> {
let mut nrs_map = NrsMap {
map: BTreeMap::new(),
};
let topname_url = SafeUrl::from_url("safe://example")?;
let a_url = SafeUrl::from_url("safe://a.example")?;
let a_b_url = SafeUrl::from_url("safe://a.b.example")?;
nrs_map
.map
.insert("example".to_string(), topname_url.clone());
nrs_map.map.insert("a.example".to_string(), a_url.clone());
nrs_map
.map
.insert("a.b.example".to_string(), a_b_url.clone());
let summary = nrs_map.get_map_summary();
assert_eq!(summary.len(), 3);
assert_eq!(summary[0].0, "example");
assert_eq!(summary[0].1, topname_url.to_string());
assert_eq!(summary[1].0, "a.example");
assert_eq!(summary[1].1, a_url.to_string());
assert_eq!(summary[2].0, "a.b.example");
assert_eq!(summary[2].1, a_b_url.to_string());
Ok(())
}
}