finance_query_core/models/
earnings_transcripts.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use chrono::{DateTime, Utc};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
6#[serde(rename_all = "UPPERCASE")]
7#[allow(dead_code)]
8pub enum Quarter {
9 Q1,
10 Q2,
11 Q3,
12 Q4,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub struct EarningsCallListing {
18 pub event_id: String,
19 pub quarter: Option<String>,
20 pub year: Option<i32>,
21 pub title: String,
22 pub url: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub struct EarningsCallsList {
28 pub symbol: String,
29 pub earnings_calls: Vec<EarningsCallListing>,
30 pub total: usize,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub struct TranscriptSpeaker {
36 pub name: String,
37 pub role: Option<String>,
38 pub company: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub struct TranscriptParagraph {
44 pub speaker: String,
45 pub text: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub struct EarningsTranscript {
51 pub symbol: String,
52 pub quarter: String,
53 pub year: i32,
54 pub date: DateTime<Utc>,
55 pub title: String,
56 pub speakers: Vec<TranscriptSpeaker>,
57 pub paragraphs: Vec<TranscriptParagraph>,
58 pub metadata: HashMap<String, serde_json::Value>,
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use proptest::prelude::*;
65
66 fn optional_string() -> impl Strategy<Value = Option<String>> {
67 proptest::option::of("[A-Za-z ]{1,30}")
68 }
69
70 proptest! {
73 #![proptest_config(ProptestConfig::with_cases(100))]
74
75 #[test]
76 fn transcript_speaker_roundtrip(
77 name in "[A-Za-z ]{1,30}",
78 role in optional_string(),
79 company in optional_string(),
80 ) {
81 let speaker = TranscriptSpeaker {
82 name: name.clone(),
83 role: role.clone(),
84 company: company.clone(),
85 };
86
87 let json = serde_json::to_string(&speaker).unwrap();
88 let parsed: TranscriptSpeaker = serde_json::from_str(&json).unwrap();
89
90 prop_assert_eq!(speaker.name, parsed.name);
91 prop_assert_eq!(speaker.role, parsed.role);
92 prop_assert_eq!(speaker.company, parsed.company);
93 }
94
95 #[test]
96 fn transcript_paragraph_roundtrip(
97 speaker in "[A-Za-z ]{1,30}",
98 text in "[A-Za-z0-9 .,!?]{1,100}",
99 ) {
100 let para = TranscriptParagraph {
101 speaker: speaker.clone(),
102 text: text.clone(),
103 };
104
105 let json = serde_json::to_string(¶).unwrap();
106 let parsed: TranscriptParagraph = serde_json::from_str(&json).unwrap();
107
108 prop_assert_eq!(para.speaker, parsed.speaker);
109 prop_assert_eq!(para.text, parsed.text);
110 }
111
112 #[test]
113 fn earnings_call_listing_roundtrip(
114 event_id in "[a-z0-9]{8}",
115 quarter in optional_string(),
116 year in proptest::option::of(2000i32..2030i32),
117 title in "[A-Za-z0-9 ]{1,50}",
118 url in "https://[a-z]{5,10}\\.com/[a-z]{5,10}",
119 ) {
120 let listing = EarningsCallListing {
121 event_id: event_id.clone(),
122 quarter: quarter.clone(),
123 year,
124 title: title.clone(),
125 url: url.clone(),
126 };
127
128 let json = serde_json::to_string(&listing).unwrap();
129 let parsed: EarningsCallListing = serde_json::from_str(&json).unwrap();
130
131 prop_assert_eq!(listing.event_id, parsed.event_id);
132 prop_assert_eq!(listing.quarter, parsed.quarter);
133 prop_assert_eq!(listing.year, parsed.year);
134 prop_assert_eq!(listing.title, parsed.title);
135 prop_assert_eq!(listing.url, parsed.url);
136 }
137 }
138}