1use rayon::prelude::*;
8use rustc_hash::FxHashMap;
9use serde_json::Value;
10
11pub(crate) mod scan;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct LookupEntry {
16 pub field: String,
18 pub key: String,
20 pub value: Value,
22}
23
24#[derive(Debug)]
26pub struct Extracted {
27 pub rows: Vec<Option<String>>,
28 pub lookup: Vec<LookupEntry>,
29}
30
31type Chunk = (Vec<Option<String>>, Lookup);
33
34struct Held {
37 text: Option<String>,
38 value: Option<Value>,
39 nesting: usize,
41}
42
43impl Held {
44 fn value(&mut self) -> &Value {
45 let Held { text, value, .. } = self;
46 value.get_or_insert_with(|| {
47 let text = text
48 .as_deref()
49 .expect("a held value has its text or its value");
50 serde_json::from_str(text).expect("a field's text was validated by the scan")
51 })
52 }
53
54 fn same(&mut self, other: &mut Held) -> bool {
57 match (&self.text, &other.text) {
58 (Some(a), Some(b)) if a == b => true,
59 _ => self.value() == other.value(),
60 }
61 }
62}
63
64#[derive(Default)]
66struct Lookup {
67 ids: Vec<(String, String)>,
68 held: Vec<Held>,
69 index: FxHashMap<String, FxHashMap<String, usize>>,
71}
72
73fn not_invariant(field: &str, determinant: &str, key: &str) -> String {
74 format!(
75 "extract_invariants: field '{}' is not invariant for its determinant '{}' \
76 ('{}' has two different values)",
77 field, determinant, key
78 )
79}
80
81impl Lookup {
82 fn position(&self, field: &str, key: &str) -> Option<usize> {
83 self.index.get(field)?.get(key).copied()
84 }
85
86 fn insert_held(
87 &mut self,
88 field: &str,
89 key: &str,
90 mut held: Held,
91 determinant: &str,
92 ) -> Result<(), String> {
93 match self.position(field, key) {
94 Some(i) if !self.held[i].same(&mut held) => Err(not_invariant(field, determinant, key)),
95 Some(_) => Ok(()),
96 None => {
97 let at = self.held.len();
98 self.index
99 .entry(field.to_string())
100 .or_default()
101 .insert(key.to_string(), at);
102 self.ids.push((field.to_string(), key.to_string()));
103 self.held.push(held);
104 Ok(())
105 }
106 }
107 }
108
109 fn insert(&mut self, entry: LookupEntry, determinant: &str) -> Result<(), String> {
110 let held = Held {
111 text: None,
112 value: Some(entry.value),
113 nesting: 0,
114 };
115 self.insert_held(&entry.field, &entry.key, held, determinant)
116 }
117
118 fn insert_text(
121 &mut self,
122 field: &str,
123 key: &str,
124 text: &str,
125 nesting: usize,
126 determinant: &str,
127 ) -> Result<(), String> {
128 if let Some(i) = self.position(field, key) {
129 if self.held[i].text.as_deref() == Some(text) {
130 return Ok(());
131 }
132 }
133 let held = Held {
134 text: Some(text.to_string()),
135 value: None,
136 nesting,
137 };
138 self.insert_held(field, key, held, determinant)
139 }
140
141 fn into_entries(self) -> Vec<LookupEntry> {
142 self.ids
143 .into_iter()
144 .zip(self.held)
145 .map(|((field, key), mut held)| {
146 held.value();
147 LookupEntry {
148 field,
149 key,
150 value: held.value.unwrap(),
151 }
152 })
153 .collect()
154 }
155}
156
157fn strip(node: &mut Value, spec: &[(String, String)], out: &mut Lookup) -> Result<(), String> {
159 match node {
160 Value::Object(obj) => {
161 for (field, key_field) in spec {
162 if !obj.contains_key(field) {
163 continue;
164 }
165 let key = match obj.get(key_field) {
166 Some(Value::String(s)) => s.clone(),
167 Some(Value::Null) | None => continue,
168 Some(other) => other.to_string(),
169 };
170 let value = obj.shift_remove(field).unwrap();
172 out.insert(
173 LookupEntry {
174 field: field.clone(),
175 key,
176 value,
177 },
178 key_field,
179 )?;
180 }
181 obj.values_mut().try_for_each(|v| strip(v, spec, out))
182 }
183 Value::Array(arr) => arr.iter_mut().try_for_each(|v| strip(v, spec, out)),
184 _ => Ok(()),
185 }
186}
187
188pub fn extract_invariants(
193 rows: Vec<Option<String>>,
194 spec: &[(String, String)],
195) -> Result<Extracted, String> {
196 extract_invariants_from(&rows, spec)
197}
198
199pub fn extract_invariants_from<S: AsRef<str> + Sync>(
202 rows: &[Option<S>],
203 spec: &[(String, String)],
204) -> Result<Extracted, String> {
205 extract(rows, spec, true)
206}
207
208#[doc(hidden)]
212pub fn extract_invariants_by_value<S: AsRef<str> + Sync>(
213 rows: &[Option<S>],
214 spec: &[(String, String)],
215) -> Result<Extracted, String> {
216 extract(rows, spec, false)
217}
218
219fn extract<S: AsRef<str> + Sync>(
220 rows: &[Option<S>],
221 spec: &[(String, String)],
222 scan_rows: bool,
223) -> Result<Extracted, String> {
224 let chunk = rows.len().div_ceil(rayon::current_num_threads() * 4).max(1);
225 let chunks: Vec<Result<Chunk, String>> = rows
229 .par_chunks(chunk)
230 .map(|rows| {
231 let mut lookup = Lookup::default();
232 let slim = rows
233 .iter()
234 .map(|row| {
235 let Some(s) = row else { return Ok(None) };
236 let s = s.as_ref();
237 let known = |field: usize, key: &str| {
241 let held = &lookup.held[lookup.position(&spec[field].0, key)?];
242 Some((held.text.as_deref()?, held.nesting))
243 };
244 if let Some(Ok(scanned)) = scan_rows.then(|| scan::scan_known(s, spec, &known))
245 {
246 for removed in &scanned.removed {
247 let (field, key_field) = &spec[removed.field];
248 let text = &s[removed.value.clone()];
249 let nesting = removed.nesting;
250 lookup.insert_text(field, &removed.key, text, nesting, key_field)?;
251 }
252 return Ok(Some(scanned.slim));
253 }
254 let Ok(mut v) = serde_json::from_str::<Value>(s) else {
255 return Ok(Some(s.to_string()));
256 };
257 strip(&mut v, spec, &mut lookup)?;
258 Ok(Some(serde_json::to_string(&v).unwrap()))
259 })
260 .collect::<Result<Vec<_>, String>>()?;
261 Ok((slim, lookup))
262 })
263 .collect();
264
265 let mut slim_rows = Vec::new();
266 let mut lookup = Lookup::default();
267 for chunk in chunks {
268 let (rows, part) = chunk?;
269 slim_rows.extend(rows);
270 for ((field, key), held) in part.ids.into_iter().zip(part.held) {
271 let determinant = spec
272 .iter()
273 .find(|(f, _)| *f == field)
274 .map_or("", |(_, k)| k.as_str());
275 lookup.insert_held(&field, &key, held, determinant)?;
276 }
277 }
278 Ok(Extracted {
279 rows: slim_rows,
280 lookup: lookup.into_entries(),
281 })
282}
283
284#[cfg(test)]
285mod tests {
286 include!("tests/extract.rs");
287}