1#[allow(dead_code, clippy::all, clippy::pedantic)]
11mod generated {
12 include!(concat!(env!("OUT_DIR"), "/tessera_generated.rs"));
13}
14
15pub use generated::{
16 DatasetSummary, DatasetsResponse, DownloadResponse, ErrorBody, MonthRange, Partition,
17 PartitionsResponse,
18};
19
20use crate::error::TesseraError;
21
22pub(crate) fn validate_month(month: &str) -> Result<(), TesseraError> {
27 let digits = |bytes: &[u8]| bytes.iter().all(u8::is_ascii_digit);
28 let valid = month.len() == 7
29 && month.as_bytes()[4] == b'-'
30 && digits(&month.as_bytes()[0..4])
31 && digits(&month.as_bytes()[5..7]);
32 if !valid {
33 return Err(TesseraError::InvalidArgument(format!(
34 "month must be in YYYY-MM format, got {month:?}"
35 )));
36 }
37 let m: u8 = month[5..7].parse().expect("digits checked above");
38 if !(1..=12).contains(&m) {
39 return Err(TesseraError::InvalidArgument(format!(
40 "month must be in YYYY-MM format, got {month:?}"
41 )));
42 }
43 Ok(())
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct PartitionRef {
52 pub asset: String,
54 pub coin: String,
56 pub month: String,
58}
59
60impl PartitionRef {
61 pub fn new(
67 asset: impl Into<String>,
68 coin: impl Into<String>,
69 month: impl Into<String>,
70 ) -> Result<Self, TesseraError> {
71 let month = month.into();
72 validate_month(&month)?;
73 Ok(Self {
74 asset: asset.into(),
75 coin: coin.into(),
76 month,
77 })
78 }
79
80 #[must_use]
82 pub fn object_key(&self) -> String {
83 format!(
84 "{}/coin={}/month={}.parquet",
85 self.asset, self.coin, self.month
86 )
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct MonthSpan {
96 pub start: String,
98 pub end: String,
100}
101
102impl MonthSpan {
103 pub fn new(start: impl Into<String>, end: impl Into<String>) -> Result<Self, TesseraError> {
110 let start = start.into();
111 let end = end.into();
112 validate_month(&start)?;
113 validate_month(&end)?;
114 if start > end {
115 return Err(TesseraError::InvalidArgument(format!(
116 "MonthSpan start {start:?} is after end {end:?}"
117 )));
118 }
119 Ok(Self { start, end })
120 }
121
122 #[must_use]
124 pub fn months(&self) -> Vec<String> {
125 let (mut year, mut month) = parse_ym(&self.start);
126 let (end_year, end_month) = parse_ym(&self.end);
127 let mut months = Vec::new();
128 while (year, month) <= (end_year, end_month) {
129 months.push(format!("{year:04}-{month:02}"));
130 month += 1;
131 if month > 12 {
132 month = 1;
133 year += 1;
134 }
135 }
136 months
137 }
138}
139
140impl IntoIterator for MonthSpan {
141 type Item = String;
142 type IntoIter = std::vec::IntoIter<String>;
143
144 fn into_iter(self) -> Self::IntoIter {
146 self.months().into_iter()
147 }
148}
149
150fn parse_ym(month: &str) -> (i32, i32) {
155 (
156 month[0..4]
157 .parse()
158 .expect("validated month has a numeric year"),
159 month[5..7]
160 .parse()
161 .expect("validated month has a numeric month"),
162 )
163}
164
165pub trait IntoCoins {
167 fn into_coins(self) -> Result<Vec<String>, TesseraError>;
173}
174
175pub trait IntoMonths {
178 fn into_months(self) -> Result<Vec<String>, TesseraError>;
185}
186
187impl IntoCoins for &str {
188 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
189 Ok(vec![self.to_string()])
190 }
191}
192
193impl IntoCoins for String {
194 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
195 Ok(vec![self])
196 }
197}
198
199impl IntoCoins for &String {
200 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
201 self.as_str().into_coins()
202 }
203}
204
205impl<C: AsRef<str>> IntoCoins for Vec<C> {
206 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
207 self.as_slice().into_coins()
208 }
209}
210
211impl<C: AsRef<str>> IntoCoins for &Vec<C> {
212 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
213 self.as_slice().into_coins()
214 }
215}
216
217impl<C: AsRef<str>, const N: usize> IntoCoins for [C; N] {
218 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
219 self.as_slice().into_coins()
220 }
221}
222
223impl<C: AsRef<str>, const N: usize> IntoCoins for &[C; N] {
224 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
225 self.as_slice().into_coins()
226 }
227}
228
229impl IntoMonths for &str {
230 fn into_months(self) -> Result<Vec<String>, TesseraError> {
231 validate_month(self)?;
232 Ok(vec![self.to_string()])
233 }
234}
235
236impl IntoMonths for String {
237 fn into_months(self) -> Result<Vec<String>, TesseraError> {
238 validate_month(&self)?;
239 Ok(vec![self])
240 }
241}
242
243impl IntoMonths for &String {
244 fn into_months(self) -> Result<Vec<String>, TesseraError> {
245 self.as_str().into_months()
246 }
247}
248
249impl IntoMonths for MonthSpan {
250 fn into_months(self) -> Result<Vec<String>, TesseraError> {
251 Ok(self.months())
252 }
253}
254
255impl<C: AsRef<str>> IntoMonths for &[C] {
256 fn into_months(self) -> Result<Vec<String>, TesseraError> {
257 let months: Vec<String> = self.iter().map(|m| m.as_ref().to_string()).collect();
258 if months.is_empty() {
259 return Err(TesseraError::InvalidArgument(
260 "at least one month is required".to_string(),
261 ));
262 }
263 for m in &months {
264 validate_month(m)?;
265 }
266 Ok(months)
267 }
268}
269
270impl<C: AsRef<str>> IntoMonths for Vec<C> {
271 fn into_months(self) -> Result<Vec<String>, TesseraError> {
272 self.as_slice().into_months()
273 }
274}
275
276impl<C: AsRef<str>> IntoMonths for &Vec<C> {
277 fn into_months(self) -> Result<Vec<String>, TesseraError> {
278 self.as_slice().into_months()
279 }
280}
281
282impl<C: AsRef<str>, const N: usize> IntoMonths for [C; N] {
283 fn into_months(self) -> Result<Vec<String>, TesseraError> {
284 self.as_slice().into_months()
285 }
286}
287
288impl<C: AsRef<str>, const N: usize> IntoMonths for &[C; N] {
289 fn into_months(self) -> Result<Vec<String>, TesseraError> {
290 self.as_slice().into_months()
291 }
292}
293
294impl<C: AsRef<str>> IntoCoins for &[C] {
295 fn into_coins(self) -> Result<Vec<String>, TesseraError> {
296 let coins: Vec<String> = self.iter().map(|c| c.as_ref().to_string()).collect();
297 if coins.is_empty() {
298 return Err(TesseraError::InvalidArgument(
299 "at least one coin is required".to_string(),
300 ));
301 }
302 Ok(coins)
303 }
304}