Skip to main content

hal_sdk/
doc.rs

1use std::fmt;
2
3use serde::de::{self, Visitor};
4use serde::{Deserialize, Deserializer, Serialize};
5
6/// A single document returned by a HAL search.
7///
8/// HAL is a Solr index with a very large, evolving schema. Only the most common
9/// fields are typed here; any other field requested through
10/// [`SearchQuery::fields`](crate::SearchQuery::fields) is preserved in [`HalDoc::extra`].
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct HalDoc {
13    /// The HAL internal document identifier.
14    ///
15    /// HAL returns this either as a number or as a string depending on the
16    /// endpoint and version; it is always normalised to a `String` here.
17    #[serde(default, deserialize_with = "string_or_number")]
18    pub docid: String,
19
20    /// A ready-to-display citation-style label (default field of a basic search).
21    #[serde(default)]
22    pub label_s: Option<String>,
23
24    /// The canonical URI of the document on HAL (e.g. `https://hal.science/…`).
25    #[serde(default)]
26    pub uri_s: Option<String>,
27
28    /// The document title(s). Requested with `fl=title_s`.
29    #[serde(default)]
30    pub title_s: Option<Vec<String>>,
31
32    /// The full names of the authors. Requested with `fl=authFullName_s`.
33    #[serde(default, rename = "authFullName_s")]
34    pub auth_full_name_s: Option<Vec<String>>,
35
36    /// The production date, as a string (e.g. `2026-06-04`). Requested with `fl=producedDate_s`.
37    #[serde(default)]
38    pub produced_date_s: Option<String>,
39
40    /// The document type (e.g. `ART`, `COMM`, `THESE`). Requested with `fl=docType_s`.
41    #[serde(default)]
42    pub doc_type_s: Option<String>,
43
44    /// Any other field returned by HAL but not modelled above.
45    #[serde(flatten)]
46    pub extra: serde_json::Map<String, serde_json::Value>,
47}
48
49impl HalDoc {
50    /// The first title, if any (HAL stores titles as an array).
51    pub fn title(&self) -> Option<&str> {
52        self.title_s.as_ref()?.first().map(String::as_str)
53    }
54
55    /// The authors joined with `", "`, if any.
56    pub fn authors(&self) -> Option<String> {
57        self.auth_full_name_s.as_ref().map(|a| a.join(", "))
58    }
59
60    /// The best human-readable heading available: the title if present,
61    /// otherwise the citation label.
62    pub fn heading(&self) -> Option<&str> {
63        self.title().or(self.label_s.as_deref())
64    }
65}
66
67impl fmt::Display for HalDoc {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self.heading() {
70            Some(heading) => write!(f, "{} ({})", heading, self.docid),
71            None => write!(f, "{}", self.docid),
72        }
73    }
74}
75
76/// Accept both a JSON string and a JSON number, normalising to `String`.
77fn string_or_number<'de, D>(deserializer: D) -> Result<String, D::Error>
78where
79    D: Deserializer<'de>,
80{
81    struct StringOrNumber;
82
83    impl<'de> Visitor<'de> for StringOrNumber {
84        type Value = String;
85
86        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87            f.write_str("a string or an integer")
88        }
89
90        fn visit_str<E: de::Error>(self, v: &str) -> Result<String, E> {
91            Ok(v.to_owned())
92        }
93
94        fn visit_string<E: de::Error>(self, v: String) -> Result<String, E> {
95            Ok(v)
96        }
97
98        fn visit_i64<E: de::Error>(self, v: i64) -> Result<String, E> {
99            Ok(v.to_string())
100        }
101
102        fn visit_u64<E: de::Error>(self, v: u64) -> Result<String, E> {
103            Ok(v.to_string())
104        }
105    }
106
107    deserializer.deserialize_any(StringOrNumber)
108}