1use std::{
4 collections::HashMap,
5 fmt::{self, Debug},
6 str::{self, FromStr},
7};
8
9use paste::paste;
10use serde_json::Value;
11
12use super::{error::VOTableError, HasContent, HasContentElem, VOTableElement};
13
14#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum ContentRole {
17 Query,
18 Hints,
19 Doc,
20 Location,
21}
22
23impl FromStr for ContentRole {
24 type Err = String;
25
26 fn from_str(s: &str) -> Result<Self, Self::Err> {
27 match s {
28 "query" => Ok(ContentRole::Query),
29 "hints" => Ok(ContentRole::Hints),
30 "doc" => Ok(ContentRole::Doc),
31 "location" => Ok(ContentRole::Location),
32 _ => Err(format!("Unknown content-role variant. Actual: '{}'. Expected: 'query', 'hints', 'doc' or 'location'.", s))
33 }
34 }
35}
36
37impl From<&ContentRole> for &'static str {
38 fn from(content_role: &ContentRole) -> Self {
39 match content_role {
40 ContentRole::Query => "query",
41 ContentRole::Hints => "hints",
42 ContentRole::Doc => "doc",
43 ContentRole::Location => "location",
44 }
45 }
46}
47
48impl fmt::Display for ContentRole {
49 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50 Debug::fmt(self, f)
51 }
52}
53
54#[derive(Default, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
56pub struct Link {
57 #[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
58 pub id: Option<String>,
59 #[serde(rename = "content-role", skip_serializing_if = "Option::is_none")]
60 pub content_role: Option<ContentRole>,
61 #[serde(rename = "content-type", skip_serializing_if = "Option::is_none")]
62 pub content_type: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub title: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub value: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub href: Option<String>,
69 #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
71 pub extra: HashMap<String, Value>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub content: Option<String>,
75}
76
77impl Link {
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 impl_builder_opt_string_attr!(id);
84 impl_builder_opt_attr!(content_role, ContentRole);
85 impl_builder_opt_string_attr!(content_type);
86 impl_builder_opt_string_attr!(title);
87 impl_builder_opt_string_attr!(value);
88 impl_builder_opt_string_attr!(href);
89 impl_builder_insert_extra!();
91}
92impl_has_content!(Link);
93
94impl VOTableElement for Link {
95 const TAG: &'static str = "LINK";
96
97 type MarkerType = HasContentElem;
98
99 fn from_attrs<K, V, I>(attrs: I) -> Result<Self, VOTableError>
100 where
101 K: AsRef<str> + Into<String>,
102 V: AsRef<str> + Into<String>,
103 I: Iterator<Item = (K, V)>,
104 {
105 Self::new().set_attrs(attrs)
106 }
107
108 fn set_attrs_by_ref<K, V, I>(&mut self, attrs: I) -> Result<(), VOTableError>
109 where
110 K: AsRef<str> + Into<String>,
111 V: AsRef<str> + Into<String>,
112 I: Iterator<Item = (K, V)>,
113 {
114 for (key, val) in attrs {
115 let key = key.as_ref();
116 match key {
117 "ID" => self.set_id_by_ref(val),
118 "content-role" => {
119 self.set_content_role_by_ref(val.as_ref().parse().map_err(VOTableError::Custom)?)
120 }
121 "content-type" => self.set_content_type_by_ref(val),
122 "title" => self.set_title_by_ref(val),
123 "value" => self.set_value_by_ref(val),
124 "href" => self.set_href_by_ref(val),
125 _ => self.insert_extra_str_by_ref(key, val),
126 }
127 }
128 Ok(())
129 }
130
131 fn for_each_attribute<F>(&self, mut f: F)
132 where
133 F: FnMut(&str, &str),
134 {
135 if let Some(id) = &self.id {
136 f("ID", id.as_str());
137 }
138 if let Some(content_role) = &self.content_role {
139 f("content-role", content_role.into());
140 }
141 if let Some(content_type) = &self.content_type {
142 f("content-type", content_type.as_str());
143 }
144 if let Some(title) = &self.title {
145 f("title", title.as_str());
146 }
147 if let Some(value) = &self.value {
148 f("value", value.as_str());
149 }
150 if let Some(href) = &self.href {
151 f("href", href.as_str());
152 }
153 for_each_extra_attribute!(self, f);
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use crate::{
160 link::Link,
161 tests::{test_read, test_writer},
162 };
163
164 #[test]
165 fn test_link_read_write() {
166 let xml =
167 r#"<LINK ID="id" content-role="doc" content-type="text/text" href="http://127.0.0.1/"/>"#; let link = test_read::<Link>(xml);
169 assert_eq!(link.id, Some("id".to_string()));
170 assert_eq!(link.href, Some("http://127.0.0.1/".to_string()));
171 let role = format!("{}", link.content_role.as_ref().unwrap());
172 assert_eq!(role, "Doc".to_string());
173 assert_eq!(link.content_type, Some("text/text".to_string()));
174 test_writer(link, xml);
176 }
177}