votable/
info.rs

1//! Struct dedicated to the `INFO` tag.
2
3use std::{collections::HashMap, str};
4
5use paste::paste;
6use serde_json::Value;
7
8use super::{error::VOTableError, HasContent, HasContentElem, VOTableElement};
9
10/// Struct corresponding to the `INFO` XML tag.
11#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
12pub struct Info {
13  // attributes
14  #[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
15  pub id: Option<String>,
16  pub name: String,
17  pub value: String,
18  #[serde(skip_serializing_if = "Option::is_none")]
19  pub xtype: Option<String>,
20  #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
21  pub ref_: Option<String>,
22  #[serde(skip_serializing_if = "Option::is_none")]
23  pub unit: Option<String>,
24  #[serde(skip_serializing_if = "Option::is_none")]
25  pub ucd: Option<String>,
26  #[serde(skip_serializing_if = "Option::is_none")]
27  pub utype: Option<String>,
28  // extra attributes
29  #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
30  pub extra: HashMap<String, Value>,
31  // content
32  #[serde(skip_serializing_if = "Option::is_none")]
33  pub content: Option<String>,
34}
35
36impl Info {
37  pub fn new<N: Into<String>, V: Into<String>>(name: N, value: V) -> Self {
38    Info {
39      id: None,
40      name: name.into(),
41      value: value.into(),
42      xtype: None,
43      ref_: None,
44      unit: None,
45      ucd: None,
46      utype: None,
47      extra: Default::default(),
48      content: None,
49    }
50  }
51
52  // attributes
53  impl_builder_opt_string_attr!(id);
54  impl_builder_mandatory_string_attr!(name);
55  impl_builder_mandatory_string_attr!(value);
56  impl_builder_opt_string_attr!(xtype);
57  impl_builder_opt_string_attr!(ref_, ref);
58  impl_builder_opt_string_attr!(unit);
59  impl_builder_opt_string_attr!(ucd);
60  impl_builder_opt_string_attr!(utype);
61  // extra attributes
62  impl_builder_insert_extra!();
63}
64
65impl_has_content!(Info);
66
67impl VOTableElement for Info {
68  const TAG: &'static str = "INFO";
69
70  type MarkerType = HasContentElem;
71
72  fn from_attrs<K, V, I>(attrs: I) -> Result<Self, VOTableError>
73  where
74    K: AsRef<str> + Into<String>,
75    V: AsRef<str> + Into<String>,
76    I: Iterator<Item = (K, V)>,
77  {
78    const DEFAULT_VALUE: &str = "@TBD";
79    Self::new(DEFAULT_VALUE, DEFAULT_VALUE)
80      .set_attrs(attrs)
81      .and_then(|info| {
82        if info.name.as_str() == DEFAULT_VALUE || info.value.as_str() == DEFAULT_VALUE {
83          Err(VOTableError::Custom(format!(
84            "Attributes 'name' and 'value' are mandatory in tag '{}'",
85            Self::TAG
86          )))
87        } else {
88          Ok(info)
89        }
90      })
91  }
92
93  fn set_attrs_by_ref<K, V, I>(&mut self, attrs: I) -> Result<(), VOTableError>
94  where
95    K: AsRef<str> + Into<String>,
96    V: AsRef<str> + Into<String>,
97    I: Iterator<Item = (K, V)>,
98  {
99    for (key, val) in attrs {
100      let key = key.as_ref();
101      match key {
102        "ID" => self.set_id_by_ref(val),
103        "name" => self.set_name_by_ref(val),
104        "value" => self.set_value_by_ref(val),
105        "xtype" => self.set_xtype_by_ref(val),
106        "ref" => self.set_ref_by_ref(val),
107        "unit" => self.set_unit_by_ref(val),
108        "ucd" => self.set_ucd_by_ref(val),
109        "utype" => self.set_utype_by_ref(val),
110        _ => self.insert_extra_str_by_ref(key, val),
111      }
112    }
113    Ok(())
114  }
115
116  fn for_each_attribute<F>(&self, mut f: F)
117  where
118    F: FnMut(&str, &str),
119  {
120    if let Some(id) = &self.id {
121      f("ID", id.as_str());
122    }
123    f("name", self.name.as_str());
124    f("value", self.value.as_str());
125    if let Some(xtype) = &self.xtype {
126      f("xtype", xtype.to_string().as_str());
127    }
128    if let Some(r) = &self.ref_ {
129      f("ref", r.as_str());
130    }
131    if let Some(unit) = &self.unit {
132      f("unit", unit.as_str());
133    }
134    if let Some(ucd) = &self.ucd {
135      f("ucd", ucd.as_str());
136    }
137    if let Some(utype) = &self.utype {
138      f("utype", utype.as_str());
139    }
140    for_each_extra_attribute!(self, f);
141  }
142}
143
144#[cfg(test)]
145mod tests {
146  use std::io::Cursor;
147
148  use quick_xml::{events::Event, Reader, Writer};
149
150  use crate::{info::Info, QuickXmlReadWrite, VOTableElement};
151
152  fn test_info_read(xml: &str) -> Info {
153    let mut reader = Reader::from_reader(Cursor::new(xml.as_bytes()));
154    let mut buff: Vec<u8> = Vec::with_capacity(xml.len());
155    loop {
156      let mut event = reader.read_event(&mut buff).unwrap();
157      match &mut event {
158        Event::Start(e) if e.local_name() == Info::TAG_BYTES => {
159          let info = Info::from_event_start(&e)
160            .and_then(|info| info.read_content(&mut reader, &mut buff, &()))
161            .unwrap();
162          return info;
163        }
164        Event::Empty(e) if e.local_name() == Info::TAG_BYTES => {
165          let info = Info::from_event_empty(&e).unwrap();
166          return info;
167        }
168        Event::Text(e) if e.escaped().is_empty() => (), // First even read
169        _ => unreachable!(),
170      }
171    }
172  }
173
174  #[test]
175  fn test_info_readwrite_1() {
176    let xml = r#"<INFO ID="VERSION" name="votable-version" value="1.99+ (14-Oct-2013)"/>"#;
177    // Test read
178    let mut info = test_info_read(xml);
179    assert_eq!(info.id.as_ref().map(|s| s.as_str()), Some("VERSION"));
180    assert_eq!(info.name.as_str(), "votable-version");
181    assert_eq!(info.value.as_str(), "1.99+ (14-Oct-2013)");
182    // Test write
183    let mut writer = Writer::new(Cursor::new(Vec::new()));
184    info.write(&mut writer, &()).unwrap();
185    let output = writer.into_inner().into_inner();
186    let output_str = unsafe { std::str::from_utf8_unchecked(output.as_slice()) };
187    assert_eq!(output_str, xml);
188  }
189
190  #[test]
191  fn test_info_readwrite_2() {
192    let xml = r#"<INFO name="queryParameters" value="25">
193  -oc.form=dec
194  -out.max=50
195  -out.all=2
196  -nav=cat:J/ApJ/701/1219&amp;tab:{J/ApJ/701/1219/table4}&amp;key:source=J/ApJ/701/1219&amp;HTTPPRM:&amp;
197  -c.eq=J2000
198  -c.r=  2
199  -c.u=arcmin
200  -c.geom=r
201  -source=J/ApJ/701/1219/table4
202  -order=I
203  -out=ID
204  -out=RAJ2000
205  -out=DEJ2000
206  -out=Sep
207  -out=Dist
208  -out=Bmag
209  -out=e_Bmag
210  -out=Rmag
211  -out=e_Rmag
212  -out=Imag
213  -out=e_Imag
214  -out=z
215  -out=Type
216  -out=RMag
217  -out.all=2
218  </INFO>"#;
219    // Test read
220    let mut info = test_info_read(xml);
221    assert_eq!(info.name.as_str(), "queryParameters");
222    assert_eq!(info.value.as_str(), "25");
223    assert_eq!(
224      info.content.as_ref().map(|s| s.as_str()),
225      Some(
226        r#"
227  -oc.form=dec
228  -out.max=50
229  -out.all=2
230  -nav=cat:J/ApJ/701/1219&tab:{J/ApJ/701/1219/table4}&key:source=J/ApJ/701/1219&HTTPPRM:&
231  -c.eq=J2000
232  -c.r=  2
233  -c.u=arcmin
234  -c.geom=r
235  -source=J/ApJ/701/1219/table4
236  -order=I
237  -out=ID
238  -out=RAJ2000
239  -out=DEJ2000
240  -out=Sep
241  -out=Dist
242  -out=Bmag
243  -out=e_Bmag
244  -out=Rmag
245  -out=e_Rmag
246  -out=Imag
247  -out=e_Imag
248  -out=z
249  -out=Type
250  -out=RMag
251  -out.all=2
252  "#
253      )
254    );
255    // Test write
256    let mut writer = Writer::new(Cursor::new(Vec::new()));
257    info.write(&mut writer, &()).unwrap();
258    let output = writer.into_inner().into_inner();
259    let output_str = unsafe { std::str::from_utf8_unchecked(output.as_slice()) };
260    assert_eq!(output_str, xml);
261  }
262}