Skip to main content

apif_source_row/
definition.rs

1use crate::detect::SourceFormat;
2use crate::filter::FilterCondition;
3use serde::Deserialize;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct SourceDefinition {
8    pub file: String,
9    #[serde(default)]
10    pub name: Option<String>,
11    #[serde(default, deserialize_with = "deserialize_format_opt")]
12    pub format: Option<SourceFormat>,
13    #[serde(default)]
14    pub delimiter: Option<u8>,
15    #[serde(default)]
16    pub header: Option<bool>,
17    #[serde(default)]
18    pub indexed_by: Option<Vec<String>>,
19    #[serde(default)]
20    pub index_mode: Option<IndexMode>,
21    #[serde(default)]
22    pub memory_budget: Option<String>,
23    #[serde(default)]
24    pub filter: Option<Vec<FilterCondition>>,
25    #[serde(default)]
26    pub join_type: Option<JoinType>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum JoinType {
32    Inner,
33    Left,
34    Cross,
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum IndexMode {
40    #[default]
41    OnDemand,
42    BuildOnce,
43    Memory,
44}
45
46fn deserialize_format_opt<'de, D>(de: D) -> Result<Option<SourceFormat>, D::Error>
47where
48    D: serde::Deserializer<'de>,
49{
50    let s: Option<String> = Option::deserialize(de)?;
51    match s {
52        None => Ok(None),
53        Some(ref val) => SourceFormat::from_str(val)
54            .map(Some)
55            .map_err(serde::de::Error::custom),
56    }
57}
58
59impl SourceDefinition {
60    pub fn from_file_raw(file: &str, key_column: &str, format: Option<&SourceFormat>) -> Self {
61        Self {
62            file: file.to_string(),
63            name: None,
64            format: format.cloned(),
65            delimiter: None,
66            header: None,
67            indexed_by: Some(vec![key_column.to_string()]),
68            index_mode: Some(IndexMode::BuildOnce),
69            memory_budget: None,
70            filter: None,
71            join_type: None,
72        }
73    }
74
75    pub fn join_type_or_default(&self) -> JoinType {
76        self.join_type.unwrap_or(JoinType::Left)
77    }
78
79    pub fn effective_index_mode(&self) -> IndexMode {
80        self.index_mode.unwrap_or_default()
81    }
82
83    pub fn indexed_columns(&self) -> Vec<&str> {
84        match &self.indexed_by {
85            None => Vec::new(),
86            Some(v) => v.iter().map(|s| s.as_str()).collect(),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn deserialize_minimal() {
97        let def: SourceDefinition = serde_yaml_ng::from_str("file: data/users.csv").unwrap();
98        assert_eq!(def.file, "data/users.csv");
99        assert!(def.name.is_none());
100        assert!(def.format.is_none());
101        assert!(def.indexed_by.is_none());
102    }
103
104    #[test]
105    fn deserialize_full() {
106        let yaml = "\
107file: data/pvz.csv
108name: pvz
109format: csv
110indexed_by: [pvz_id]
111index_mode: build_once
112memory_budget: 256mb
113";
114        let def: SourceDefinition = serde_yaml_ng::from_str(yaml).unwrap();
115        assert_eq!(def.file, "data/pvz.csv");
116        assert_eq!(def.name.as_deref(), Some("pvz"));
117        assert_eq!(def.format, Some(SourceFormat::Csv));
118        assert_eq!(def.effective_index_mode(), IndexMode::BuildOnce);
119        assert_eq!(def.indexed_columns(), vec!["pvz_id"]);
120        assert_eq!(def.memory_budget.as_deref(), Some("256mb"));
121    }
122
123    #[test]
124    fn deserialize_multi_indexed_by() {
125        let yaml = "\
126file: data/pvz.csv
127indexed_by:
128  - pvz_id
129  - region_id
130";
131        let def: SourceDefinition = serde_yaml_ng::from_str(yaml).unwrap();
132        assert_eq!(def.indexed_columns(), vec!["pvz_id", "region_id"]);
133    }
134
135    #[test]
136    fn index_mode_default_is_on_demand() {
137        let def: SourceDefinition = serde_yaml_ng::from_str("file: x.csv").unwrap();
138        assert_eq!(def.effective_index_mode(), IndexMode::OnDemand);
139    }
140
141    #[test]
142    fn index_mode_all_variants() {
143        assert_eq!(IndexMode::default(), IndexMode::OnDemand);
144        assert_ne!(IndexMode::OnDemand, IndexMode::BuildOnce);
145        assert_ne!(IndexMode::BuildOnce, IndexMode::Memory);
146    }
147
148    #[test]
149    fn deserialize_format_aliases() {
150        for fmt in &["csv", "tsv", "ndjson", "json", "jsonl"] {
151            let yaml = format!("file: x\nformat: {fmt}");
152            let def: SourceDefinition = serde_yaml_ng::from_str(&yaml).unwrap();
153            assert!(def.format.is_some());
154        }
155    }
156
157    #[test]
158    fn deserialize_invalid_format_errors() {
159        let yaml = "file: x\nformat: excel";
160        let result: Result<SourceDefinition, _> = serde_yaml_ng::from_str(yaml);
161        assert!(result.is_err());
162    }
163
164    #[test]
165    fn deserialize_filter_conditions() {
166        let yaml = "\
167file: data/pvz.csv
168filter:
169  - field: status
170    in: [active, suspended]
171  - field: created_at
172    gte: \"2024-01-01\"
173";
174        let def: SourceDefinition = serde_yaml_ng::from_str(yaml).unwrap();
175        let filter = def.filter.expect("filter should exist");
176        assert_eq!(filter.len(), 2);
177        assert_eq!(filter[0].field, "status");
178        assert_eq!(filter[0].in_values.as_ref().map(Vec::len), Some(2));
179        assert_eq!(filter[1].field, "created_at");
180        assert_eq!(filter[1].gte.as_deref(), Some("2024-01-01"));
181    }
182}