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}