1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
6#[serde(rename_all = "camelCase")]
7pub struct Listing {
8 pub ticker: String,
10 pub exchange_code: String,
12 pub exchange_country: String,
14}
15
16#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
18#[serde(rename_all = "camelCase")]
19pub struct Company {
20 pub company_id: i64,
22 #[serde(
25 default,
26 with = "flex::opt_f64",
27 skip_serializing_if = "Option::is_none"
28 )]
29 pub confidence: Option<f64>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub country: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub exchange: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub industry: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub sector: Option<String>,
42 pub name: String,
44 pub ticker: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub isin: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub openfigi: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub primary_listing: Option<Listing>,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub isins: Vec<String>,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub other_listings: Vec<Listing>,
61}
62
63#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct Article {
68 pub link: String,
70 pub title: String,
72 #[serde(with = "flex::datetime")]
75 pub publish_date: DateTime<Utc>,
76 pub source: String,
78 pub language: String,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub summary: Option<String>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub images: Vec<String>,
86 #[serde(
88 default,
89 with = "flex::opt_datetime",
90 skip_serializing_if = "Option::is_none"
91 )]
92 pub created_at: Option<DateTime<Utc>>,
93 #[serde(
95 default,
96 with = "flex::opt_datetime",
97 skip_serializing_if = "Option::is_none"
98 )]
99 pub revised_date: Option<DateTime<Utc>>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub is_update: Option<bool>,
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub categories: Vec<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub sentiment: Option<String>,
109 #[serde(
111 default,
112 with = "flex::opt_f64",
113 skip_serializing_if = "Option::is_none"
114 )]
115 pub confidence: Option<f64>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub content: Option<String>,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
121 pub companies: Vec<Company>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub countries: Vec<String>,
125}
126
127#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
130#[serde(rename_all = "camelCase")]
131pub struct RawArticle {
132 pub link: String,
134 pub title: String,
136 #[serde(with = "flex::datetime")]
138 pub publish_date: DateTime<Utc>,
139 pub source: String,
141 pub language: String,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub summary: Option<String>,
146 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub images: Vec<String>,
149 #[serde(
151 default,
152 with = "flex::opt_datetime",
153 skip_serializing_if = "Option::is_none"
154 )]
155 pub created_at: Option<DateTime<Utc>>,
156 #[serde(
158 default,
159 with = "flex::opt_datetime",
160 skip_serializing_if = "Option::is_none"
161 )]
162 pub revised_date: Option<DateTime<Utc>>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub is_update: Option<bool>,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub categories: Vec<String>,
169}
170
171#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
173#[serde(rename_all = "camelCase")]
174pub struct ArticleResponse {
175 pub status: String,
177 pub page: u32,
179 pub page_size: u32,
181 pub articles: Vec<Article>,
183}
184
185#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
187#[serde(rename_all = "camelCase")]
188pub struct Source {
189 pub domain: String,
191 pub is_content_available: bool,
193 pub is_default_source: bool,
195}
196
197pub(crate) mod flex {
200 use chrono::{DateTime, NaiveDate, NaiveDateTime, SecondsFormat, Utc};
201 use serde::de::{Deserializer, Error as _};
202 use serde::{Deserialize, Serializer};
203
204 #[derive(Deserialize)]
205 #[serde(untagged)]
206 enum NumOrStr {
207 Num(f64),
208 Str(String),
209 }
210
211 fn num_or_str_to_f64<E: serde::de::Error>(v: NumOrStr) -> Result<f64, E> {
212 match v {
213 NumOrStr::Num(n) => Ok(n),
214 NumOrStr::Str(s) => s
215 .trim()
216 .parse::<f64>()
217 .map_err(|_| E::custom(format!("cannot parse {s:?} as float"))),
218 }
219 }
220
221 pub(crate) mod opt_f64 {
223 use super::*;
224
225 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
226 de: D,
227 ) -> Result<Option<f64>, D::Error> {
228 Option::<NumOrStr>::deserialize(de)?
229 .map(num_or_str_to_f64)
230 .transpose()
231 }
232
233 pub(crate) fn serialize<S: Serializer>(v: &Option<f64>, ser: S) -> Result<S::Ok, S::Error> {
234 match v {
235 Some(f) => ser.serialize_f64(*f),
236 None => ser.serialize_none(),
237 }
238 }
239 }
240
241 pub(crate) fn parse_datetime(s: &str) -> Option<DateTime<Utc>> {
242 let s = s.trim();
243 if let Ok(t) = DateTime::parse_from_rfc3339(s) {
244 return Some(t.with_timezone(&Utc));
245 }
246 for format in ["%Y-%m-%dT%H:%M:%S%.f", "%Y-%m-%d %H:%M:%S%.f"] {
247 if let Ok(t) = NaiveDateTime::parse_from_str(s, format) {
248 return Some(t.and_utc());
249 }
250 }
251 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
252 return Some(d.and_hms_opt(0, 0, 0)?.and_utc());
253 }
254 None
255 }
256
257 pub(crate) mod datetime {
260 use super::*;
261
262 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
263 de: D,
264 ) -> Result<DateTime<Utc>, D::Error> {
265 let s = String::deserialize(de)?;
266 parse_datetime(&s)
267 .ok_or_else(|| D::Error::custom(format!("cannot parse {s:?} as timestamp")))
268 }
269
270 pub(crate) fn serialize<S: Serializer>(
271 t: &DateTime<Utc>,
272 ser: S,
273 ) -> Result<S::Ok, S::Error> {
274 ser.serialize_str(&t.to_rfc3339_opts(SecondsFormat::AutoSi, true))
275 }
276 }
277
278 pub(crate) mod opt_datetime {
280 use super::*;
281
282 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
283 de: D,
284 ) -> Result<Option<DateTime<Utc>>, D::Error> {
285 Option::<String>::deserialize(de)?
286 .map(|s| {
287 parse_datetime(&s)
288 .ok_or_else(|| D::Error::custom(format!("cannot parse {s:?} as timestamp")))
289 })
290 .transpose()
291 }
292
293 pub(crate) fn serialize<S: Serializer>(
294 t: &Option<DateTime<Utc>>,
295 ser: S,
296 ) -> Result<S::Ok, S::Error> {
297 match t {
298 Some(t) => datetime::serialize(t, ser),
299 None => ser.serialize_none(),
300 }
301 }
302 }
303}