use std::{collections::HashSet, ops::DerefMut};
use serde::{Deserialize, Serialize};
use crate::prelude::ContentExtensions;
use super::lenient::{Stringish, VectorStringish};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Extension(pub String);
impl From<&str> for Extension {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl std::ops::Deref for Extension {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub type RdapConformance = Vec<Extension>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum HrefLang {
Langs(Vec<String>),
Lang(String),
}
pub type Links = Vec<Link>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Link {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hreflang: Option<HrefLang>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<String>,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
#[buildstructor::buildstructor]
impl Link {
pub fn is_relation(&self, rel: &str) -> bool {
let Some(link_rel) = &self.rel else {
return false;
};
link_rel == rel
}
#[builder(visibility = "pub")]
fn new(
value: String,
href: String,
rel: String,
hreflang: Option<String>,
title: Option<String>,
media: Option<String>,
media_type: Option<String>,
) -> Self {
let hreflang = hreflang.map(HrefLang::Lang);
Self {
value: Some(value),
rel: Some(rel),
href: Some(href),
hreflang,
title,
media,
media_type,
}
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(dead_code)]
fn new_illegal(
value: Option<String>,
href: Option<String>,
rel: Option<String>,
hreflang: Option<String>,
title: Option<String>,
media: Option<String>,
media_type: Option<String>,
) -> Self {
let hreflang = hreflang.map(HrefLang::Lang);
Self {
value,
rel,
href,
hreflang,
title,
media,
media_type,
}
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn rel(&self) -> Option<&str> {
self.rel.as_deref()
}
pub fn href(&self) -> Option<&str> {
self.href.as_deref()
}
pub fn hreflang(&self) -> Option<&HrefLang> {
self.hreflang.as_ref()
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn media(&self) -> Option<&str> {
self.media.as_deref()
}
pub fn media_type(&self) -> Option<&str> {
self.media_type.as_deref()
}
}
pub type Notices = Vec<Notice>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Notice(pub NoticeOrRemark);
#[buildstructor::buildstructor]
impl Notice {
#[builder(visibility = "pub")]
fn new(
title: Option<String>,
description: Vec<String>,
links: Vec<Link>,
nr_type: Option<String>,
simple_redaction_keys: Option<Vec<String>>,
) -> Self {
let nr = NoticeOrRemark::builder()
.description(description)
.and_title(title)
.links(links)
.and_nr_type(nr_type)
.and_simple_redaction_keys(simple_redaction_keys)
.build();
Self(nr)
}
}
impl std::ops::Deref for Notice {
type Target = NoticeOrRemark;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ContentExtensions for Notice {
fn content_extensions(&self) -> HashSet<super::ExtensionId> {
self.0.content_extensions()
}
}
pub type Remarks = Vec<Remark>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Remark(pub NoticeOrRemark);
impl DerefMut for Remark {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[buildstructor::buildstructor]
impl Remark {
#[builder(visibility = "pub")]
fn new(
title: Option<String>,
description: Vec<String>,
links: Vec<Link>,
nr_type: Option<String>,
simple_redaction_keys: Option<Vec<String>>,
) -> Self {
let nr = NoticeOrRemark::builder()
.description(description)
.and_title(title)
.links(links)
.and_nr_type(nr_type)
.and_simple_redaction_keys(simple_redaction_keys)
.build();
Self(nr)
}
}
impl std::ops::Deref for Remark {
type Target = NoticeOrRemark;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ContentExtensions for Remark {
fn content_extensions(&self) -> HashSet<super::ExtensionId> {
self.0.content_extensions()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NoticeOrRemark {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<VectorStringish>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub nr_type: Option<String>,
#[serde(rename = "simpleRedaction_keys")]
#[serde(skip_serializing_if = "Option::is_none")]
pub simple_redaction_keys: Option<Vec<String>>,
}
#[buildstructor::buildstructor]
impl NoticeOrRemark {
#[builder(visibility = "pub")]
fn new(
title: Option<String>,
description: Vec<String>,
links: Vec<Link>,
nr_type: Option<String>,
simple_redaction_keys: Option<Vec<String>>,
) -> Self {
Self {
title,
description: Some(VectorStringish::from(description)),
links: (!links.is_empty()).then_some(links),
nr_type,
simple_redaction_keys,
}
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(dead_code)]
fn new_illegal(
title: Option<String>,
description: Option<Vec<String>>,
links: Option<Vec<Link>>,
nr_type: Option<String>,
simple_redaction_keys: Option<Vec<String>>,
) -> Self {
let d = description
.is_some()
.then_some(VectorStringish::from(description.unwrap()));
Self {
title,
description: d,
links,
nr_type,
simple_redaction_keys,
}
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn description(&self) -> &[String] {
self.description
.as_ref()
.map(|v| v.vec().as_ref())
.unwrap_or_default()
}
pub fn description_as_pgs(&self) -> Vec<String> {
let mut pgs = vec![];
let mut acc_line = String::new();
for line in self.description() {
acc_line.push_str(line.trim());
if acc_line.ends_with('.') || acc_line.to_ascii_uppercase().eq(&acc_line) {
pgs.push(acc_line);
acc_line = String::new();
} else {
acc_line.push(' ');
}
}
if !acc_line.is_empty() {
pgs.push(acc_line);
}
pgs
}
pub fn links(&self) -> &[Link] {
self.links.as_deref().unwrap_or_default()
}
pub fn nr_type(&self) -> Option<&str> {
self.nr_type.as_deref()
}
pub fn simple_redaction_keys(&self) -> &[String] {
self.simple_redaction_keys.as_deref().unwrap_or_default()
}
pub fn has_simple_redaction_key(&self, key: &str) -> bool {
self.simple_redaction_keys().iter().any(|k| k.eq(key))
}
}
pub trait ToNotices {
fn to_notices(self) -> Vec<Notice>;
fn to_opt_notices(self) -> Option<Vec<Notice>>;
}
impl ToNotices for &[NoticeOrRemark] {
fn to_notices(self) -> Vec<Notice> {
self.iter().map(|n| Notice(n.clone())).collect::<Notices>()
}
fn to_opt_notices(self) -> Option<Vec<Notice>> {
let notices = self.to_notices();
(!notices.is_empty()).then_some(notices)
}
}
impl ToNotices for Vec<NoticeOrRemark> {
fn to_notices(self) -> Vec<Notice> {
self.into_iter().map(Notice).collect::<Notices>()
}
fn to_opt_notices(self) -> Option<Vec<Notice>> {
let notices = self.to_notices();
(!notices.is_empty()).then_some(notices)
}
}
pub trait ToRemarks {
fn to_remarks(self) -> Vec<Remark>;
fn to_opt_remarks(self) -> Option<Vec<Remark>>;
}
impl ToRemarks for &[NoticeOrRemark] {
fn to_remarks(self) -> Vec<Remark> {
self.iter().map(|n| Remark(n.clone())).collect::<Remarks>()
}
fn to_opt_remarks(self) -> Option<Vec<Remark>> {
let remarks = self.to_remarks();
(!remarks.is_empty()).then_some(remarks)
}
}
impl ToRemarks for Vec<NoticeOrRemark> {
fn to_remarks(self) -> Vec<Remark> {
self.into_iter().map(Remark).collect::<Remarks>()
}
fn to_opt_remarks(self) -> Option<Vec<Remark>> {
let remarks = self.to_remarks();
(!remarks.is_empty()).then_some(remarks)
}
}
impl ContentExtensions for NoticeOrRemark {
fn content_extensions(&self) -> std::collections::HashSet<super::ExtensionId> {
if self.simple_redaction_keys.is_some() {
HashSet::from([super::ExtensionId::SimpleRedaction])
} else {
HashSet::new()
}
}
}
pub type Events = Vec<Event>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Event {
#[serde(rename = "eventAction")]
#[serde(skip_serializing_if = "Option::is_none")]
pub event_action: Option<String>,
#[serde(rename = "eventActor")]
#[serde(skip_serializing_if = "Option::is_none")]
pub event_actor: Option<String>,
#[serde(rename = "eventDate")]
#[serde(skip_serializing_if = "Option::is_none")]
pub event_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
}
#[buildstructor::buildstructor]
impl Event {
#[builder(visibility = "pub")]
fn new(
event_action: String,
event_date: String,
event_actor: Option<String>,
links: Option<Links>,
) -> Self {
Self {
event_action: Some(event_action),
event_actor,
event_date: Some(event_date),
links,
}
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(dead_code)]
fn new_illegal(
event_action: Option<String>,
event_date: Option<String>,
event_actor: Option<String>,
links: Option<Links>,
) -> Self {
Self {
event_action,
event_actor,
event_date,
links,
}
}
pub fn event_action(&self) -> Option<&str> {
self.event_action.as_deref()
}
pub fn event_actor(&self) -> Option<&str> {
self.event_actor.as_deref()
}
pub fn event_date(&self) -> Option<&str> {
self.event_date.as_deref()
}
pub fn links(&self) -> &[Link] {
self.links.as_deref().unwrap_or_default()
}
}
pub type Port43 = String;
pub type PublicIds = Vec<PublicId>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct PublicId {
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id_type: Option<Stringish>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<Stringish>,
}
#[buildstructor::buildstructor]
impl PublicId {
#[builder(visibility = "pub")]
fn new(id_type: String, identifier: String) -> Self {
Self {
id_type: Some(id_type.into()),
identifier: Some(identifier.into()),
}
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(dead_code)]
fn new_illegal(id_type: Option<String>, identifier: Option<String>) -> Self {
Self {
id_type: id_type.map(|s| s.into()),
identifier: identifier.map(|s| s.into()),
}
}
pub fn id_type(&self) -> Option<&str> {
self.id_type.as_deref()
}
pub fn identifier(&self) -> Option<&str> {
self.identifier.as_deref()
}
}
#[cfg(test)]
mod tests {
use crate::{
prelude::ObjectCommon,
response::types::{Extension, Notice, Notices, RdapConformance, Remark, Remarks},
};
use super::{Event, Link, Links, NoticeOrRemark, PublicId};
#[test]
fn test_rdap_conformance_serialize() {
let rdap_conformance: RdapConformance =
vec![Extension("foo".to_string()), Extension("bar".to_string())];
let actual = serde_json::to_string(&rdap_conformance).unwrap();
let expected = r#"["foo","bar"]"#;
assert_eq!(actual, expected);
}
#[test]
fn test_an_array_of_links_deserialize() {
let expected = r#"
[
{
"value" : "https://1.example.com/context_uri",
"rel" : "self",
"href" : "https://1.example.com/target_uri",
"hreflang" : [ "en", "ch" ],
"title" : "title1",
"media" : "screen",
"type" : "application/json"
},
{
"value" : "https://2.example.com/context_uri",
"rel" : "self",
"href" : "https://2.example.com/target_uri",
"hreflang" : [ "en", "ch" ],
"title" : "title2",
"media" : "screen",
"type" : "application/json"
}
]
"#;
let links = serde_json::from_str::<Links>(expected);
let actual = links.unwrap();
assert_eq!(actual.len(), 2);
let actual_1 = actual.first().unwrap();
let actual_2 = actual.last().unwrap();
assert_eq!(
actual_1.value.as_ref().unwrap(),
"https://1.example.com/context_uri"
);
assert_eq!(
actual_2.value.as_ref().unwrap(),
"https://2.example.com/context_uri"
);
assert_eq!(
actual_1.href.as_ref().unwrap(),
"https://1.example.com/target_uri"
);
assert_eq!(
actual_2.href.as_ref().unwrap(),
"https://2.example.com/target_uri"
);
assert_eq!(actual_1.title.as_ref().unwrap(), "title1");
assert_eq!(actual_2.title.as_ref().unwrap(), "title2");
assert_eq!(actual_1.media_type.as_ref().unwrap(), "application/json");
assert_eq!(actual_2.media_type.as_ref().unwrap(), "application/json");
}
#[test]
fn test_an_array_of_links_with_one_lang() {
let expected = r#"
[
{
"value" : "https://1.example.com/context_uri",
"rel" : "self",
"href" : "https://1.example.com/target_uri",
"hreflang" : "en",
"title" : "title1",
"media" : "screen",
"type" : "application/json"
},
{
"value" : "https://2.example.com/context_uri",
"rel" : "self",
"href" : "https://2.example.com/target_uri",
"hreflang" : "ch",
"title" : "title2",
"media" : "screen",
"type" : "application/json"
}
]
"#;
let links = serde_json::from_str::<Links>(expected);
let actual = links.unwrap();
assert_eq!(actual.len(), 2);
let actual_1 = actual.first().unwrap();
let actual_2 = actual.last().unwrap();
assert_eq!(
actual_1.value.as_ref().unwrap(),
"https://1.example.com/context_uri"
);
assert_eq!(
actual_2.value.as_ref().unwrap(),
"https://2.example.com/context_uri"
);
assert_eq!(
actual_1.href.as_ref().unwrap(),
"https://1.example.com/target_uri"
);
assert_eq!(
actual_2.href.as_ref().unwrap(),
"https://2.example.com/target_uri"
);
assert_eq!(actual_1.title.as_ref().unwrap(), "title1");
assert_eq!(actual_2.title.as_ref().unwrap(), "title2");
assert_eq!(actual_1.media_type.as_ref().unwrap(), "application/json");
assert_eq!(actual_2.media_type.as_ref().unwrap(), "application/json");
}
#[test]
fn test_a_notice_or_remark_deserialize() {
let expected = r#"
{
"title" : "Terms of Use",
"description" :
[
"Service subject to The Registry of the Moon's TOS.",
"Copyright (c) 2020 LunarNIC"
],
"links" :
[
{
"value" : "https://example.net/entity/XXXX",
"rel" : "alternate",
"type" : "text/html",
"href" : "https://www.example.com/terms_of_use.html"
}
]
}
"#;
let actual = serde_json::from_str::<NoticeOrRemark>(expected);
let actual = actual.unwrap();
actual.title.as_ref().unwrap();
let description: Vec<String> = actual.description.expect("must have description").into();
assert_eq!(description.len(), 2);
actual.links.unwrap();
}
#[test]
fn test_notices_serialize() {
let notices: Notices = vec![
Notice(
NoticeOrRemark::builder()
.description(vec!["foo".to_string()])
.build(),
),
Notice(
NoticeOrRemark::builder()
.description(vec!["bar".to_string()])
.build(),
),
];
let actual = serde_json::to_string(¬ices).unwrap();
let expected = r#"[{"description":["foo"]},{"description":["bar"]}]"#;
assert_eq!(actual, expected);
}
#[test]
fn test_remarks_serialize() {
let remarks: Remarks = vec![
Remark(
NoticeOrRemark::builder()
.description(vec!["foo".to_string()])
.build(),
),
Remark(
NoticeOrRemark::builder()
.description(vec!["bar".to_string()])
.build(),
),
];
let actual = serde_json::to_string(&remarks).unwrap();
let expected = r#"[{"description":["foo"]},{"description":["bar"]}]"#;
assert_eq!(actual, expected);
}
#[test]
fn test_an_event_deserialize() {
let expected = r#"
{
"eventAction" : "last changed",
"eventActor" : "OTHERID-LUNARNIC",
"eventDate" : "1991-12-31T23:59:59Z"
}
"#;
let actual = serde_json::from_str::<Event>(expected);
let actual = actual.unwrap();
actual.event_actor.as_ref().unwrap();
}
#[test]
fn test_a_public_id_deserialize() {
let expected = r#"
{
"type":"IANA Registrar ID",
"identifier":"1"
}
"#;
let actual = serde_json::from_str::<PublicId>(expected);
let _actual = actual.unwrap();
}
#[test]
fn test_set_self_link() {
let mut oc = ObjectCommon::domain()
.links(vec![Link::builder()
.href("http://bar.example")
.value("http://bar.example")
.rel("unknown")
.build()])
.build();
oc = oc.with_self_link(
Link::builder()
.href("http://foo.example")
.value("http://foo.example")
.rel("unknown")
.build(),
);
assert_eq!(
oc.links
.expect("links are empty")
.iter()
.filter(|link| link.is_relation("self"))
.count(),
1
);
}
#[test]
fn test_set_self_link_on_no_links() {
let mut oc = ObjectCommon::domain().build();
oc = oc.with_self_link(
Link::builder()
.href("http://foo.example")
.value("http://foo.example")
.rel("unknown")
.build(),
);
assert_eq!(
oc.links
.expect("links are empty")
.iter()
.filter(|link| link.is_relation("self"))
.count(),
1
);
}
#[test]
fn test_set_self_link_when_one_exists() {
let mut oc = ObjectCommon::domain()
.links(vec![Link::builder()
.href("http://bar.example")
.value("http://bar.example")
.rel("self")
.build()])
.build();
oc = oc.with_self_link(
Link::builder()
.href("http://foo.example")
.value("http://foo.example")
.rel("unknown")
.build(),
);
assert_eq!(
oc.links
.as_ref()
.expect("links are empty")
.iter()
.filter(|link| link.is_relation("self")
&& link.href.as_ref().unwrap() == "http://foo.example")
.count(),
1
);
assert_eq!(
oc.links
.as_ref()
.expect("links are empty")
.iter()
.filter(|link| link.is_relation("self"))
.count(),
1
);
}
#[test]
fn test_description_as_pgs() {
let notice = Notice::builder()
.description_entry("This is a test")
.description_entry("that should be consolidated.")
.description_entry("SEPARATE LINE")
.description_entry("Another line.")
.build();
let actual = notice.description_as_pgs();
assert_eq!(
actual.first().unwrap(),
"This is a test that should be consolidated."
);
assert_eq!(actual.get(1).unwrap(), "SEPARATE LINE");
assert_eq!(actual.get(2).unwrap(), "Another line.");
}
}