Skip to main content

sora_data/validate/
mod.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use sora_diagnostics::{Result, SoraError};
4use sora_ir::model::{ConfigIr, FieldIr, IndexIr, TableIr, TableModeIr};
5
6use crate::model::{ConfigData, RowData, TableData, Value};
7
8mod value;
9
10use value::{stable_key, table_mode_name, validate_field_value};
11
12pub fn validate_config_data(ir: &ConfigIr, data: &ConfigData) -> Result<()> {
13    let errors = collect_config_data_errors(ir, data);
14    finish_validation(errors)
15}
16
17pub fn validate_config_data_all(ir: &ConfigIr, data: &ConfigData) -> Result<()> {
18    validate_config_data(ir, data)
19}
20
21fn collect_config_data_errors(ir: &ConfigIr, data: &ConfigData) -> Vec<SoraError> {
22    let tables_by_name = data
23        .tables
24        .iter()
25        .map(|table| (table.name.as_str(), table))
26        .collect::<BTreeMap<_, _>>();
27
28    let mut errors = Vec::new();
29    for table in &ir.tables {
30        match tables_by_name.get(table.name.as_str()) {
31            Some(table_data) => {
32                errors.extend(collect_table_data_errors(ir, data, table, table_data));
33            }
34            None if table.mode == TableModeIr::Singleton => {
35                errors.push(SoraError::InvalidTableRowCount {
36                    table: table.name.clone(),
37                    mode: table_mode_name(table.mode),
38                    expected: "exactly 1",
39                    actual: 0,
40                });
41            }
42            None => {}
43        }
44    }
45
46    errors
47}
48
49pub fn validate_table_data(ir: &ConfigIr, table: &TableIr, data: &TableData) -> Result<()> {
50    let config_data = ConfigData {
51        tables: vec![data.clone()],
52    };
53    validate_table_data_with_config(ir, &config_data, table, data)
54}
55
56fn validate_table_data_with_config(
57    ir: &ConfigIr,
58    config_data: &ConfigData,
59    table: &TableIr,
60    data: &TableData,
61) -> Result<()> {
62    finish_validation(collect_table_data_errors(ir, config_data, table, data))
63}
64
65fn collect_table_data_errors(
66    ir: &ConfigIr,
67    config_data: &ConfigData,
68    table: &TableIr,
69    data: &TableData,
70) -> Vec<SoraError> {
71    let mut errors = Vec::new();
72    if let Err(error) = validate_table_row_count(table, data) {
73        errors.push(error);
74    }
75
76    let field_names = table
77        .fields
78        .iter()
79        .map(|field| field.name.as_str())
80        .collect::<BTreeSet<_>>();
81    let mut seen_keys = BTreeSet::new();
82    let mut unique_indexes = table
83        .indexes
84        .iter()
85        .filter(|index| index.unique)
86        .map(|index| UniqueIndexState {
87            index,
88            seen: BTreeSet::new(),
89        })
90        .collect::<Vec<_>>();
91
92    for row in &data.rows {
93        let row_result = validate_row_fields(
94            ir,
95            config_data,
96            &table.name,
97            &table.fields,
98            &field_names,
99            row,
100        );
101        if let Err(error) = row_result {
102            errors.push(error);
103            continue;
104        }
105        if let Err(error) = validate_map_key(table, row, &mut seen_keys) {
106            errors.push(error);
107            continue;
108        }
109        if let Err(error) = validate_unique_indexes(table, row, &mut unique_indexes) {
110            errors.push(error);
111        }
112    }
113
114    errors
115}
116
117fn finish_validation(errors: Vec<SoraError>) -> Result<()> {
118    match errors.len() {
119        0 => Ok(()),
120        1 => Err(errors.into_iter().next().expect("one validation error")),
121        _ => Err(SoraError::validation_errors(errors)),
122    }
123}
124
125fn validate_table_row_count(table: &TableIr, data: &TableData) -> Result<()> {
126    match table.mode {
127        TableModeIr::Singleton if data.rows.len() != 1 => Err(SoraError::InvalidTableRowCount {
128            table: table.name.clone(),
129            mode: table_mode_name(table.mode),
130            expected: "exactly 1",
131            actual: data.rows.len(),
132        }),
133        _ => Ok(()),
134    }
135}
136
137fn validate_row_fields(
138    ir: &ConfigIr,
139    config_data: &ConfigData,
140    table_name: &str,
141    fields: &[FieldIr],
142    field_names: &BTreeSet<&str>,
143    row: &RowData,
144) -> Result<()> {
145    for field_name in row.values.keys() {
146        if !field_names.contains(field_name.as_str()) {
147            return Err(SoraError::UnknownField {
148                table: table_name.to_owned(),
149                field: field_name.clone(),
150            });
151        }
152    }
153
154    for field in fields {
155        match row.values.get(&field.name) {
156            Some(value) => {
157                validate_field_value(ir, config_data, table_name, field, &field.name, value)?
158            }
159            None if field.is_required() => {
160                return Err(SoraError::MissingRequiredField {
161                    table: table_name.to_owned(),
162                    field: field.name.clone(),
163                });
164            }
165            None => {}
166        }
167    }
168
169    Ok(())
170}
171
172struct UniqueIndexState<'a> {
173    index: &'a IndexIr,
174    seen: BTreeSet<String>,
175}
176
177fn validate_unique_indexes(
178    table: &TableIr,
179    row: &RowData,
180    indexes: &mut [UniqueIndexState<'_>],
181) -> Result<()> {
182    for state in indexes {
183        let key = unique_index_key(state.index, row);
184        if !state.seen.insert(key.clone()) {
185            return Err(SoraError::DuplicateIndexKey {
186                table: table.name.clone(),
187                index: state.index.name.clone(),
188                key,
189            });
190        }
191    }
192
193    Ok(())
194}
195
196fn unique_index_key(index: &IndexIr, row: &RowData) -> String {
197    index
198        .fields
199        .iter()
200        .map(|field| {
201            let value = row.values.get(field).unwrap_or(&Value::Null);
202            format!("{field}={}", stable_key(value))
203        })
204        .collect::<Vec<_>>()
205        .join(",")
206}
207
208fn validate_map_key(
209    table: &TableIr,
210    row: &RowData,
211    seen_keys: &mut BTreeSet<String>,
212) -> Result<()> {
213    if table.mode != TableModeIr::Map {
214        return Ok(());
215    }
216
217    let Some(key_field) = table.key.as_deref() else {
218        return Ok(());
219    };
220    let Some(value) = row.values.get(key_field) else {
221        return Err(SoraError::MissingRequiredField {
222            table: table.name.clone(),
223            field: key_field.to_owned(),
224        });
225    };
226    if matches!(value, Value::Null) {
227        return Err(SoraError::MissingRequiredField {
228            table: table.name.clone(),
229            field: key_field.to_owned(),
230        });
231    }
232
233    let key = stable_key(value);
234    if !seen_keys.insert(key.clone()) {
235        return Err(SoraError::DuplicateKey {
236            table: table.name.clone(),
237            key,
238        });
239    }
240
241    Ok(())
242}
243
244#[cfg(test)]
245mod tests;