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(
254    Debug,
255    Clone,
256    Copy,
257    PartialEq,
258    Eq,
259    PartialOrd,
260    Ord,
261    Hash,
262    Serialize,
263    Deserialize,
264    schemars::JsonSchema,
265)]
266#[serde(rename_all = "snake_case", deny_unknown_fields)]
267pub enum AbsenceReason {
268    Missing,
269    WrongType,
270    TooLong,
271    ControlCharacter,
272    NotInVocabulary,
273    DuplicateItem,
274    TooManyItems,
275    OutOfBounds,
276    InvalidDate,
277    VocabularyUnavailable,
278    SourceUnavailable,
279    ScreenBlocked,
280    ScreenUnavailable,
281    RequestFailed { reason: RequestFailure },
282    FallbackNoMatch,
283    FallbackInvalid,
284}
285#[derive(
286    Debug,
287    Clone,
288    Copy,
289    PartialEq,
290    Eq,
291    PartialOrd,
292    Ord,
293    Hash,
294    Serialize,
295    Deserialize,
296    schemars::JsonSchema,
297)]
298#[serde(rename_all = "snake_case")]
299pub enum RequestFailure {
300    BackendUnavailable,
301    InvalidBackendOutput,
302    DeadlineExceeded,
303    BudgetExhausted,
304    Cancelled,
305}
306fn fields<'de, D: Deserializer<'de>, const MAX: usize>(
307    d: D,
308) -> Result<Vec<ExtractionField>, D::Error> {
309    let v = Vec::<ExtractionField>::deserialize(d)?;
310    let mut names = BTreeSet::new();
311    if !(1..=MAX).contains(&v.len())
312        || v.iter()
313            .any(|f| !crate::parameter::is_parameter_name(&f.name) || !names.insert(&f.name))
314    {
315        return Err(serde::de::Error::custom(
316            "invalid extraction field count or names",
317        ));
318    }
319    Ok(v)
320}
321fn record_fields<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<ExtractionField>, D::Error> {
322    let v = fields::<D, 16>(d)?;
323    if v.iter().any(|f| {
324        matches!(
325            f.ty,
326            ExtractionType::List { .. } | ExtractionType::Record { .. }
327        )
328    }) {
329        return Err(serde::de::Error::custom("record subfields must be scalar"));
330    }
331    Ok(v)
332}
333fn list_item<'de, D: Deserializer<'de>>(d: D) -> Result<Box<ExtractionType>, D::Error> {
334    let v = ExtractionType::deserialize(d)?;
335    if !matches!(v, ExtractionType::Text { .. } | ExtractionType::Enum { .. }) {
336        return Err(serde::de::Error::custom("list items must be text or enum"));
337    }
338    Ok(Box::new(v))
339}
340fn integer_bounds<'de, D: Deserializer<'de>>(d: D) -> Result<[i64; 2], D::Error> {
341    let v = <[i64; 2]>::deserialize(d)?;
342    if v[0] > v[1] {
343        return Err(serde::de::Error::custom("reversed integer bounds"));
344    }
345    Ok(v)
346}
347fn notice<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
348    let v = String::deserialize(d)?;
349    if !crate::parameter::is_parameter_name(&v) {
350        return Err(serde::de::Error::custom("invalid notice code"));
351    }
352    Ok(v)
353}
354fn valid_ref(s: &str, prefix: &str, min: usize, max: usize) -> bool {
355    s.strip_prefix(prefix).is_some_and(|s| {
356        let parts: Vec<_> = s.split('/').collect();
357        (min..=max).contains(&parts.len())
358            && parts
359                .iter()
360                .all(|p| !p.is_empty() && !p.contains(':') && !p.chars().any(char::is_whitespace))
361    })
362}
363pub(crate) fn input_ref<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
364    let v = String::deserialize(d)?;
365    if !valid_ref(&v, "input:", 1, 2) {
366        return Err(serde::de::Error::custom("invalid input reference"));
367    }
368    Ok(v)
369}
370fn field_ref<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
371    let v = String::deserialize(d)?;
372    if !valid_ref(&v, "input:", 1, 2) && !valid_ref(&v, "step:", 2, 2) {
373        return Err(serde::de::Error::custom(
374            "invalid acceptance field reference",
375        ));
376    }
377    Ok(v)
378}
379fn projection<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
380    let v = Vec::<String>::deserialize(d)?;
381    if !(1..=8).contains(&v.len())
382        || v.iter()
383            .any(|s| !valid_ref(s, "input:", 1, 2) && !valid_ref(s, "step:", 1, 3))
384    {
385        return Err(serde::de::Error::custom("invalid extraction projection"));
386    }
387    Ok(v)
388}
389fn rules_program<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
390    let v = String::deserialize(d)?;
391    if !v
392        .split_once('@')
393        .is_some_and(|(n, ver)| !n.is_empty() && !ver.is_empty() && !ver.contains('@'))
394        || v.chars().any(char::is_whitespace)
395    {
396        return Err(serde::de::Error::custom(
397            "expected rules program name@version",
398        ));
399    }
400    Ok(v)
401}