Skip to main content

rustev_contract/
extraction.rs

1//! Structured extraction declarations (spec 038). Compilation remains closed.
2use crate::{
3    Decimal,
4    definition::{BackendReq, Predicate},
5    generation::{CostLimit, GenerateScreen, Repair, ScreenOutput, TemplateRef, Vocabulary},
6};
7use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
11#[serde(deny_unknown_fields)]
12pub struct Extract {
13    pub for_each: NoneOnly,
14    pub template: TemplateRef,
15    #[serde(deserialize_with = "projection")]
16    pub projection: Vec<String>,
17    pub vocabularies: BTreeMap<String, ExtractionVocabulary>,
18    pub output: ExtractionSchema,
19    pub distributions: Distributions,
20    pub repair: Repair,
21    pub screen: GenerateScreen,
22    pub limits: ExtractionLimits,
23    pub backend: BackendReq,
24    pub fallback: Fallback,
25}
26impl<'de> Deserialize<'de> for Extract {
27    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
28        #[derive(Deserialize)]
29        #[serde(remote = "Extract", deny_unknown_fields)]
30        struct Wire {
31            pub for_each: NoneOnly,
32            pub template: TemplateRef,
33            #[serde(deserialize_with = "projection")]
34            pub projection: Vec<String>,
35            pub vocabularies: BTreeMap<String, ExtractionVocabulary>,
36            pub output: ExtractionSchema,
37            pub distributions: Distributions,
38            pub repair: Repair,
39            pub screen: GenerateScreen,
40            pub limits: ExtractionLimits,
41            pub backend: BackendReq,
42            pub fallback: Fallback,
43        }
44        let v = Wire::deserialize(d)?;
45        if v.repair.max_attempts != 0 || v.screen.output != ScreenOutput::None {
46            return Err(serde::de::Error::custom(
47                "extraction requires no repair or output screening",
48            ));
49        }
50        fn declared(ty: &ExtractionType, vocab: &BTreeMap<String, ExtractionVocabulary>) -> bool {
51            match ty {
52                ExtractionType::Enum { vocabulary } => vocab.contains_key(vocabulary),
53                ExtractionType::List { item, .. } => declared(item, vocab),
54                ExtractionType::Record { fields } => fields.iter().all(|f| declared(&f.ty, vocab)),
55                _ => true,
56            }
57        }
58        if !v
59            .output
60            .fields
61            .iter()
62            .all(|f| declared(&f.ty, &v.vocabularies))
63        {
64            return Err(serde::de::Error::custom("undeclared extraction vocabulary"));
65        }
66        Ok(v)
67    }
68}
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
70#[serde(rename_all = "snake_case")]
71pub enum NoneOnly {
72    None,
73}
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
75// Spec 038 3.3.2 closes vocabulary forms; container-level deny_unknown_fields applies to
76// the Input struct variant, refusing {"input": ..., "extra": ...}, pinned by
77// extraction_envelope_refuses_invalid_members in tests/extraction_wire.rs.
78#[serde(untagged, deny_unknown_fields)]
79pub enum ExtractionVocabulary {
80    Envelope(Vocabulary),
81    Input {
82        #[serde(deserialize_with = "input_ref")]
83        input: String,
84    },
85}
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
87#[serde(rename_all = "snake_case")]
88pub enum Distributions {
89    None,
90    WhenAvailable,
91}
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
93#[serde(deny_unknown_fields)]
94pub struct ExtractionLimits {
95    #[serde(deserialize_with = "crate::generation::deadline_share")]
96    pub deadline_share: Decimal,
97    pub max_cost_units: CostLimit,
98}
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
100#[serde(rename_all = "snake_case", deny_unknown_fields)]
101pub enum Fallback {
102    None,
103    #[serde(untagged)]
104    Rules(#[serde(deserialize_with = "crate::generation::object")] ExtractionRules),
105}
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
107#[serde(deny_unknown_fields)]
108pub struct ExtractionRules {
109    #[serde(deserialize_with = "rules_program")]
110    pub rules_program: String,
111    pub fill: Fill,
112}
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
114#[serde(rename_all = "snake_case")]
115pub enum Fill {
116    RequestFailure,
117    AbsentFields,
118}
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
120#[serde(deny_unknown_fields)]
121pub struct ExtractionSchema {
122    #[serde(deserialize_with = "fields::<_, 32>")]
123    pub fields: Vec<ExtractionField>,
124}
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
126#[serde(deny_unknown_fields)]
127pub struct ExtractionField {
128    pub name: String,
129    #[serde(rename = "type")]
130    pub ty: ExtractionType,
131}
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
133#[serde(rename_all = "snake_case", deny_unknown_fields)]
134pub enum ExtractionType {
135    Bool {},
136    Integer {
137        #[serde(deserialize_with = "integer_bounds")]
138        bounds: [i64; 2],
139    },
140    Decimal(DecimalBounds),
141    Text {
142        #[serde(deserialize_with = "crate::generation::bound::<_, 4096>")]
143        max_bytes: u64,
144    },
145    Date {},
146    Enum {
147        vocabulary: String,
148    },
149    List {
150        #[serde(deserialize_with = "list_item")]
151        item: Box<ExtractionType>,
152        #[serde(deserialize_with = "crate::generation::bound::<_, 64>")]
153        max_items: u64,
154    },
155    Record {
156        #[serde(deserialize_with = "record_fields")]
157        fields: Vec<ExtractionField>,
158    },
159}
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
161#[serde(deny_unknown_fields)]
162pub struct DecimalBounds {
163    pub bounds: [String; 2],
164    pub scale: u32,
165}
166impl<'de> Deserialize<'de> for DecimalBounds {
167    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
168        #[derive(Deserialize)]
169        #[serde(remote = "DecimalBounds", deny_unknown_fields)]
170        struct Wire {
171            bounds: [String; 2],
172            scale: u32,
173        }
174        let v = Wire::deserialize(d)?;
175        let [lo, hi] = [&v.bounds[0], &v.bounds[1]].map(|s| Decimal::parse(s));
176        if v.scale > 9
177            || v.bounds.iter().any(|s| {
178                s.split_once('.')
179                    .is_some_and(|(_, f)| f.len() > v.scale as usize)
180            })
181            || !matches!((lo, hi), (Ok(lo), Ok(hi)) if lo <= hi)
182        {
183            return Err(serde::de::Error::custom(
184                "invalid extraction decimal bounds or scale",
185            ));
186        }
187        Ok(v)
188    }
189}
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
191#[serde(deny_unknown_fields)]
192pub struct AcceptModelDerived {
193    #[serde(deserialize_with = "field_ref")]
194    pub field: String,
195    pub validate: Validation,
196    pub min_mass: MinMass,
197    pub on_fail: OnFail,
198}
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
200#[serde(rename_all = "snake_case", deny_unknown_fields)]
201pub enum Validation {
202    None,
203    Predicate(Predicate),
204}
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
206#[serde(rename_all = "snake_case", deny_unknown_fields)]
207pub enum MinMass {
208    None,
209    #[serde(untagged)]
210    Threshold(#[serde(deserialize_with = "crate::generation::object")] MassThreshold),
211}
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
213#[serde(deny_unknown_fields)]
214pub struct MassThreshold {
215    pub option: String,
216    #[serde(deserialize_with = "probability_threshold")]
217    pub at_least: Decimal,
218    pub basis: MassBasis,
219}
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
221#[serde(rename_all = "snake_case", deny_unknown_fields)]
222pub enum MassBasis {
223    Calibration(String),
224    UncalibratedThreshold(#[serde(deserialize_with = "uncalibrated_reason")] String),
225}
226fn probability_threshold<'de, D: Deserializer<'de>>(d: D) -> Result<Decimal, D::Error> {
227    let v = Decimal::deserialize(d)?;
228    if v < Decimal::ZERO || v > Decimal::from_i64(1) {
229        return Err(serde::de::Error::custom(
230            "a probability threshold is in [0, 1]",
231        ));
232    }
233    Ok(v)
234}
235fn uncalibrated_reason<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
236    let v = String::deserialize(d)?;
237    if v.is_empty() {
238        return Err(serde::de::Error::custom(
239            "an uncalibrated threshold states its reason",
240        ));
241    }
242    Ok(v)
243}
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
245#[serde(rename_all = "snake_case", deny_unknown_fields)]
246pub enum OnFail {
247    DropField {
248        #[serde(deserialize_with = "notice")]
249        notice: String,
250    },
251    Unresolved,
252}
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
254#[serde(rename_all = "snake_case", deny_unknown_fields)]
255pub enum AbsenceReason {
256    Missing,
257    WrongType,
258    TooLong,
259    ControlCharacter,
260    NotInVocabulary,
261    DuplicateItem,
262    TooManyItems,
263    OutOfBounds,
264    InvalidDate,
265    VocabularyUnavailable,
266    SourceUnavailable,
267    ScreenBlocked,
268    ScreenUnavailable,
269    RequestFailed { reason: RequestFailure },
270    FallbackNoMatch,
271    FallbackInvalid,
272}
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
274#[serde(rename_all = "snake_case")]
275pub enum RequestFailure {
276    BackendUnavailable,
277    InvalidBackendOutput,
278    DeadlineExceeded,
279    BudgetExhausted,
280    Cancelled,
281}
282fn fields<'de, D: Deserializer<'de>, const MAX: usize>(
283    d: D,
284) -> Result<Vec<ExtractionField>, D::Error> {
285    let v = Vec::<ExtractionField>::deserialize(d)?;
286    let mut names = BTreeSet::new();
287    if !(1..=MAX).contains(&v.len())
288        || v.iter()
289            .any(|f| !crate::parameter::is_parameter_name(&f.name) || !names.insert(&f.name))
290    {
291        return Err(serde::de::Error::custom(
292            "invalid extraction field count or names",
293        ));
294    }
295    Ok(v)
296}
297fn record_fields<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<ExtractionField>, D::Error> {
298    let v = fields::<D, 16>(d)?;
299    if v.iter().any(|f| {
300        matches!(
301            f.ty,
302            ExtractionType::List { .. } | ExtractionType::Record { .. }
303        )
304    }) {
305        return Err(serde::de::Error::custom("record subfields must be scalar"));
306    }
307    Ok(v)
308}
309fn list_item<'de, D: Deserializer<'de>>(d: D) -> Result<Box<ExtractionType>, D::Error> {
310    let v = ExtractionType::deserialize(d)?;
311    if !matches!(v, ExtractionType::Text { .. } | ExtractionType::Enum { .. }) {
312        return Err(serde::de::Error::custom("list items must be text or enum"));
313    }
314    Ok(Box::new(v))
315}
316fn integer_bounds<'de, D: Deserializer<'de>>(d: D) -> Result<[i64; 2], D::Error> {
317    let v = <[i64; 2]>::deserialize(d)?;
318    if v[0] > v[1] {
319        return Err(serde::de::Error::custom("reversed integer bounds"));
320    }
321    Ok(v)
322}
323fn notice<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
324    let v = String::deserialize(d)?;
325    if !crate::parameter::is_parameter_name(&v) {
326        return Err(serde::de::Error::custom("invalid notice code"));
327    }
328    Ok(v)
329}
330fn valid_ref(s: &str, prefix: &str, min: usize, max: usize) -> bool {
331    s.strip_prefix(prefix).is_some_and(|s| {
332        let parts: Vec<_> = s.split('/').collect();
333        (min..=max).contains(&parts.len())
334            && parts
335                .iter()
336                .all(|p| !p.is_empty() && !p.contains(':') && !p.chars().any(char::is_whitespace))
337    })
338}
339pub(crate) fn input_ref<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
340    let v = String::deserialize(d)?;
341    if !valid_ref(&v, "input:", 1, 2) {
342        return Err(serde::de::Error::custom("invalid input reference"));
343    }
344    Ok(v)
345}
346fn field_ref<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
347    let v = String::deserialize(d)?;
348    if !valid_ref(&v, "input:", 1, 2) && !valid_ref(&v, "step:", 2, 2) {
349        return Err(serde::de::Error::custom(
350            "invalid acceptance field reference",
351        ));
352    }
353    Ok(v)
354}
355fn projection<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
356    let v = Vec::<String>::deserialize(d)?;
357    if !(1..=8).contains(&v.len())
358        || v.iter()
359            .any(|s| !valid_ref(s, "input:", 1, 2) && !valid_ref(s, "step:", 1, 1))
360    {
361        return Err(serde::de::Error::custom("invalid extraction projection"));
362    }
363    Ok(v)
364}
365fn rules_program<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
366    let v = String::deserialize(d)?;
367    if !v
368        .split_once('@')
369        .is_some_and(|(n, ver)| !n.is_empty() && !ver.is_empty() && !ver.contains('@'))
370        || v.chars().any(char::is_whitespace)
371    {
372        return Err(serde::de::Error::custom(
373            "expected rules program name@version",
374        ));
375    }
376    Ok(v)
377}