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;