Skip to main content

xisf_header/
keyword.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The [`FitsKeyword`] record.
6
7use crate::value::{FromField, IntoValue, Value};
8
9/// A single FITS keyword extracted from (or destined for) an XISF header:
10/// a name, a value, and an optional comment.
11///
12/// The value's on-disk kind (quoted string vs. bare literal) is preserved; see
13/// [`IntoValue`] for how the kind is chosen when you write one.
14///
15/// ```
16/// use xisf_header::FitsKeyword;
17///
18/// let kw = FitsKeyword::new("IMAGETYP", "Master Dark", "Type of image");
19/// assert_eq!(kw.value_str(), "Master Dark");
20/// assert_eq!(kw.comment, "Type of image");
21/// ```
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct FitsKeyword {
25    /// The keyword name (e.g. `EXPTIME`), kept verbatim.
26    pub name: String,
27    /// The keyword value.
28    pub(crate) value: Value,
29    /// The keyword comment (empty when absent).
30    pub comment: String,
31}
32
33/// Whether `name` is a FITS commentary keyword (`HISTORY`/`COMMENT`), which
34/// carries no FITS value — only free text. XISF represents that text in the
35/// `comment` attribute with an empty `value`, unlike every other keyword
36/// (spec + reference implementations; see PixInsight-written files). Exact,
37/// case-sensitive match: only the canonical uppercase FITS spellings count.
38pub(crate) fn is_commentary(name: &str) -> bool {
39    matches!(name, "HISTORY" | "COMMENT")
40}
41
42impl FitsKeyword {
43    /// Create a keyword from its name, value, and comment.
44    ///
45    /// ```
46    /// use xisf_header::FitsKeyword;
47    /// let kw = FitsKeyword::new("GAIN", 100_i64, "Sensor gain");
48    /// assert_eq!(kw.get::<i64>(), Some(100));
49    /// ```
50    pub fn new(name: impl Into<String>, value: impl IntoValue, comment: impl Into<String>) -> Self {
51        Self {
52            name: name.into(),
53            value: value.into_value(),
54            comment: comment.into(),
55        }
56    }
57
58    /// The value's raw text, regardless of kind.
59    ///
60    /// ```
61    /// use xisf_header::FitsKeyword;
62    ///
63    /// let kw = FitsKeyword::new("OBJECT", "NGC 7000", "Target");
64    /// assert_eq!(kw.value_str(), "NGC 7000");
65    /// ```
66    #[must_use]
67    pub fn value_str(&self) -> &str {
68        self.value.text()
69    }
70
71    /// Interpret the value as `T` (see [`FromField`]).
72    ///
73    /// ```
74    /// use xisf_header::FitsKeyword;
75    ///
76    /// let kw = FitsKeyword::new("EXPTIME", 300.0, "");
77    /// assert_eq!(kw.get::<f64>(), Some(300.0));
78    /// ```
79    #[must_use]
80    pub fn get<T: FromField>(&self) -> Option<T> {
81        T::from_field(self.value.text())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn new_selects_value_kind_from_rust_type() {
91        let s = FitsKeyword::new("OBJECT", "M31", "target");
92        assert_eq!(s.value_str(), "M31");
93        assert_eq!(s.comment, "target");
94        assert_eq!(s.get::<String>(), Some("M31".to_owned()));
95
96        let n = FitsKeyword::new("GAIN", 100_i64, "");
97        assert_eq!(n.value_str(), "100");
98        assert_eq!(n.get::<i64>(), Some(100));
99        assert_eq!(n.get::<bool>(), None);
100    }
101}