votable/
link.rs

1//! Struct dedicated to the `LINK` tag.
2
3use 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/// Enum for the possible values of the `content-role` attriute.
15#[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/// Struct corresponding to the `LINK` XML tag.
55#[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  // extra attributes
70  #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
71  pub extra: HashMap<String, Value>,
72  // content
73  #[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  // attributes
83  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  // extra attributes
90  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/"/>"#; // Test read
168    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 write
175    test_writer(link, xml);
176  }
177}