1use std::collections::{BTreeMap, BTreeSet};
3
4use rustev_contract::{
5 Document,
6 extraction::{
7 AbsenceReason as Reason, Distributions, Extract, ExtractionSchema, ExtractionType, Fill,
8 },
9 extraction_documents::{
10 ExtractRules, ExtractionCondition as Condition, ExtractionValue as Expr,
11 },
12 extraction_results::{ExtractionFieldEvidence, ExtractionOrigin},
13 judgment::Derivation,
14 output::RawOutput,
15};
16use serde_json::Value as Json;
17
18use crate::extraction_validate::{self, ValidatedField, fallback_derivation};
19
20#[derive(Debug, Clone, Copy)]
23pub enum Source<'a> {
24 Readable {
25 bytes: &'a [u8],
26 derivation: Derivation,
27 },
28 Unavailable,
29}
30
31#[derive(Debug, Clone, PartialEq)]
34pub struct FallbackField {
35 pub field: ValidatedField,
36 pub sources_read: Vec<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct BoundProgram {
42 program: ExtractRules,
43 schema: ExtractionSchema,
44}
45
46pub fn bind(program: &ExtractRules, step: &Extract) -> Result<BoundProgram, String> {
50 let bytes = serde_json::to_vec(program).map_err(|e| e.to_string())?;
52 let program = ExtractRules::parse(&bytes).map_err(|e| e.to_string())?;
53 let mut names = BTreeSet::new();
54 for s in &program.sources {
55 if !step.projection.contains(&s.reference) || !s.reference.starts_with("input:") {
56 return Err(format!("source {} is not in the input projection", s.name));
57 }
58 if !names.insert(s.name.as_str()) {
59 return Err(format!("duplicate source {}", s.name));
60 }
61 }
62 let mut fields = BTreeSet::new();
63 for f in &program.fields {
64 if !fields.insert(&f.field) {
65 return Err(format!("duplicate field {}", f.field));
66 }
67 let declared = step
68 .output
69 .fields
70 .iter()
71 .find(|d| d.name == f.field)
72 .ok_or_else(|| format!("undeclared field {}", f.field))?;
73 let mut literals = Vec::new();
74 for rule in &f.rules {
75 let mut used = BTreeSet::new();
76 condition_sources(&rule.when, &mut used);
77 value_sources(&rule.then, &mut used);
78 if let Some(s) = used.iter().find(|s| !names.contains(**s)) {
79 return Err(format!("undeclared source {s}"));
80 }
81 if let ExtractionType::List { item, .. } = &declared.ty {
82 let Expr::Literal(v) = &rule.then else {
83 return Err(format!("list field {} requires literal items", f.field));
84 };
85 if !literal_type(v, item) {
86 return Err(format!("wrong list item type for {}", f.field));
87 }
88 if literals.contains(&v) {
89 return Err(format!("duplicate literal item for {}", f.field));
90 }
91 literals.push(v);
92 } else if !value_type(&rule.then, &declared.ty) {
93 return Err(format!("incompatible value for {}", f.field));
94 }
95 }
96 }
97 Ok(BoundProgram {
98 program,
99 schema: step.output.clone(),
100 })
101}
102
103fn literal_type(v: &Json, ty: &ExtractionType) -> bool {
104 match ty {
105 ExtractionType::Bool {} => v.is_boolean(),
106 ExtractionType::Integer { .. } => v.is_i64() || v.is_u64(),
107 ExtractionType::Decimal(_)
108 | ExtractionType::Text { .. }
109 | ExtractionType::Date {}
110 | ExtractionType::Enum { .. } => v.is_string(),
111 ExtractionType::Record { fields } => v.as_object().is_some_and(|obj| {
112 obj.iter().all(|(name, value)| {
113 fields
114 .iter()
115 .any(|f| f.name == *name && literal_type(value, &f.ty))
116 })
117 }),
118 ExtractionType::List { .. } => false,
119 }
120}
121
122fn value_type(v: &Expr, ty: &ExtractionType) -> bool {
123 match (v, ty) {
124 (Expr::Literal(v), ty) => literal_type(v, ty),
125 (Expr::IntegerAfter { .. }, ExtractionType::Integer { .. })
126 | (Expr::DateIso { .. }, ExtractionType::Date {}) => true,
127 (Expr::Record(values), ExtractionType::Record { fields }) => values
128 .iter()
129 .all(|(n, v)| fields.iter().any(|f| f.name == *n && value_type(v, &f.ty))),
130 _ => false,
131 }
132}
133
134fn condition_sources<'a>(c: &'a Condition, out: &mut BTreeSet<&'a str>) {
135 match c {
136 Condition::Always => {}
137 Condition::ContainsAny { source, .. } | Condition::ContainsAll { source, .. } => {
138 out.insert(source);
139 }
140 Condition::All(cs) | Condition::Any(cs) => {
141 for c in cs {
142 condition_sources(c, out);
143 }
144 }
145 Condition::Not(c) => condition_sources(c, out),
146 }
147}
148fn value_sources<'a>(v: &'a Expr, out: &mut BTreeSet<&'a str>) {
149 match v {
150 Expr::Literal(_) => {}
151 Expr::IntegerAfter { source, .. } | Expr::DateIso { source, .. } => {
152 out.insert(source);
153 }
154 Expr::Record(vs) => {
155 for v in vs.values() {
156 value_sources(v, out);
157 }
158 }
159 }
160}
161
162struct Reads<'a> {
163 sources: &'a BTreeMap<String, Source<'a>>,
164 references: &'a BTreeMap<&'a str, &'a str>,
165 max_bytes: u64,
166 read: BTreeMap<String, Derivation>,
167}
168impl<'a> Reads<'a> {
169 fn get(&mut self, source: &str) -> Result<&'a [u8], Reason> {
170 let reference = self
171 .references
172 .get(source)
173 .ok_or(Reason::SourceUnavailable)?;
174 match self.sources.get(*reference) {
175 Some(Source::Readable { bytes, derivation })
176 if bytes.len() as u64 <= self.max_bytes =>
177 {
178 self.read.insert((*reference).into(), *derivation);
179 Ok(bytes)
180 }
181 _ => Err(Reason::SourceUnavailable),
182 }
183 }
184}
185
186fn occurrence(text: &[u8], term: &str) -> Option<usize> {
189 text.windows(term.len())
190 .position(|w| w.eq_ignore_ascii_case(term.as_bytes()))
191}
192fn condition(c: &Condition, reads: &mut Reads<'_>) -> Result<bool, Reason> {
193 Ok(match c {
194 Condition::Always => true,
195 Condition::ContainsAny { source, terms } => {
196 let text = reads.get(source)?;
197 terms.iter().any(|t| occurrence(text, t).is_some())
198 }
199 Condition::ContainsAll { source, terms } => {
200 let text = reads.get(source)?;
201 terms.iter().all(|t| occurrence(text, t).is_some())
202 }
203 Condition::All(cs) => {
204 for c in cs {
205 if !condition(c, reads)? {
206 return Ok(false);
207 }
208 }
209 true
210 }
211 Condition::Any(cs) => {
212 for c in cs {
213 if condition(c, reads)? {
214 return Ok(true);
215 }
216 }
217 false
218 }
219 Condition::Not(c) => !condition(c, reads)?,
220 })
221}
222fn value(v: &Expr, reads: &mut Reads<'_>) -> Result<Option<Json>, Reason> {
223 Ok(match v {
224 Expr::Literal(v) => Some(v.clone()),
225 Expr::IntegerAfter {
226 source,
227 terms,
228 max_digits,
229 } => {
230 let text = reads.get(source)?;
231 let first = terms
233 .iter()
234 .filter_map(|t| occurrence(text, t).map(|i| (i, t.len())))
235 .enumerate()
236 .min_by_key(|(order, (i, _))| (*i, *order));
237 let Some((_, (i, len))) = first else {
238 return Ok(None);
239 };
240 let tail = &text[i + len..];
241 let spaces = tail.iter().take_while(|b| **b == b' ').count();
242 let digits = &tail[spaces..];
243 let n = digits.iter().take_while(|b| b.is_ascii_digit()).count();
244 if n == 0 || n > usize::from(*max_digits) {
245 return Ok(None);
246 }
247 Some(Json::from(
248 digits[..n]
249 .iter()
250 .fold(0i64, |v, b| v * 10 + i64::from(b - b'0')),
251 ))
252 }
253 Expr::DateIso { source, occurrence } => {
254 let text = reads.get(source)?;
255 text.windows(10)
256 .filter(|w| {
257 w.iter().enumerate().all(|(i, b)| {
258 if i == 4 || i == 7 {
259 *b == b'-'
260 } else {
261 b.is_ascii_digit()
262 }
263 })
264 })
265 .nth(usize::from(*occurrence) - 1)
266 .map(|w| Json::String(w.iter().map(|b| char::from(*b)).collect()))
267 }
268 Expr::Record(fields) => {
269 let mut obj = serde_json::Map::new();
270 for (name, v) in fields {
271 let Some(v) = value(v, reads)? else {
272 return Ok(None);
273 };
274 obj.insert(name.clone(), v);
275 }
276 Some(Json::Object(obj))
277 }
278 })
279}
280
281impl BoundProgram {
282 pub fn evaluate(
286 &self,
287 sources: &BTreeMap<String, Source<'_>>,
288 vocabularies: &BTreeMap<String, Vec<String>>,
289 max_projection_bytes: u64,
290 ) -> Vec<FallbackField> {
291 let references = self
292 .program
293 .sources
294 .iter()
295 .map(|s| (s.name.as_str(), s.reference.as_str()))
296 .collect();
297 self.schema
298 .fields
299 .iter()
300 .map(|field| {
301 let mut reads = Reads {
302 sources,
303 references: &references,
304 max_bytes: max_projection_bytes,
305 read: BTreeMap::new(),
306 };
307 let result = (|| {
308 let mut items = Vec::new();
309 if let Some(f) = self.program.fields.iter().find(|f| f.field == field.name) {
310 for rule in &f.rules {
311 if condition(&rule.when, &mut reads)?
312 && let Some(v) = value(&rule.then, &mut reads)?
313 {
314 if matches!(field.ty, ExtractionType::List { .. }) {
315 items.push(v);
316 } else {
317 return Ok(v);
318 }
319 }
320 }
321 }
322 if items.is_empty() {
323 Err(Reason::FallbackNoMatch)
324 } else {
325 Ok(Json::Array(items))
326 }
327 })();
328 let validated = result
329 .and_then(|v| {
330 let schema = ExtractionSchema {
331 fields: vec![field.clone()],
332 };
333 let raw = RawOutput::TypedRecord {
334 fields: BTreeMap::from([(field.name.clone(), v)]),
335 logprobs: vec![],
336 provider_reported: BTreeMap::new(),
337 };
338 let mut validated = extraction_validate::validate(
339 &schema,
340 vocabularies,
341 &raw,
342 Distributions::None,
343 false,
344 )
345 .map_err(|_| Reason::FallbackInvalid)?
346 .remove(0);
347 if validated.value.is_none() || !validated.evidence.rejected.is_empty() {
350 return Err(Reason::FallbackInvalid);
351 }
352 validated.evidence.origin = ExtractionOrigin::Fallback;
353 validated.evidence.derivation =
354 Some(fallback_derivation(reads.read.values().copied()));
355 Ok(validated)
356 })
357 .unwrap_or_else(|reason| ValidatedField {
358 value: None,
359 evidence: ExtractionFieldEvidence::new(
360 field.name.clone(),
361 &field.ty,
362 ExtractionOrigin::Absent { reason },
363 None,
364 None,
365 None,
366 vec![],
367 ),
368 });
369 FallbackField {
370 field: validated,
371 sources_read: reads.read.into_keys().collect(),
372 }
373 })
374 .collect()
375 }
376}
377
378pub fn merge(
383 model: &mut [ValidatedField],
384 fallback: &[FallbackField],
385 fill: Fill,
386 request_produced_output: bool,
387) -> BTreeMap<String, Vec<String>> {
388 let mut reads = BTreeMap::new();
389 if fill == Fill::RequestFailure && request_produced_output {
390 return reads;
391 }
392 for field in model {
393 if field.value.is_none()
394 && let Some(f) = fallback
395 .iter()
396 .find(|f| f.field.evidence.name == field.evidence.name)
397 {
398 *field = f.field.clone();
399 reads.insert(field.evidence.name.clone(), f.sources_read.clone());
400 }
401 }
402 reads
403}