Skip to main content

rustev_contract/
acquisition_step.rs

1//! Definition 2 acquisition vocabulary (spec 036, 3.4 to 3.8).
2use crate::Decimal;
3use crate::definition::{Expr, FieldDecl, Literal, Predicate};
4use crate::execution::{Delay, FailureClass, MAX_DELAY_MS, RetryPolicy};
5use serde::{Deserialize, Deserializer, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
9#[serde(deny_unknown_fields)]
10pub struct Acquire {
11    pub source: String,
12    pub operation: String,
13    pub request: BTreeMap<String, Expr>,
14    pub when: Guard,
15    pub limits: AcquisitionLimits,
16    pub stop: AcquireStop,
17    pub on_failure: OnFailure,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
21#[serde(rename_all = "snake_case", deny_unknown_fields)]
22pub enum Guard {
23    Always,
24    #[serde(untagged)]
25    Predicate(Predicate),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
29#[serde(rename_all = "snake_case", deny_unknown_fields)]
30pub enum AcquireStop {
31    None,
32    ItemsAtLeast {
33        #[serde(deserialize_with = "count")]
34        count: Expr,
35    },
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
39#[serde(rename_all = "snake_case")]
40pub enum OnFailure {
41    Unresolved,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
45#[serde(deny_unknown_fields)]
46pub struct AcquisitionLimits {
47    #[schemars(range(min = 1, max = 64))]
48    pub max_pages: u64,
49    #[schemars(range(min = 1, max = 100000))]
50    pub max_items: u64,
51    pub deadline_share: Decimal,
52    #[schemars(range(min = 1, max = 8))]
53    pub max_attempts: u32,
54    pub retry_on: Vec<FailureClass>,
55    pub delay: Delay,
56    #[schemars(range(min = 0, max = 60000))]
57    pub as_of_skew_ms: u64,
58}
59impl<'de> Deserialize<'de> for AcquisitionLimits {
60    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
61        #[derive(Deserialize)]
62        #[serde(deny_unknown_fields)]
63        struct Wire {
64            max_pages: u64,
65            max_items: u64,
66            deadline_share: Decimal,
67            max_attempts: u32,
68            retry_on: Vec<FailureClass>,
69            delay: Delay,
70            as_of_skew_ms: u64,
71        }
72        let w = Wire::deserialize(d)?;
73        let retry = RetryPolicy {
74            max_attempts: w.max_attempts,
75            on: w.retry_on,
76            delay: w.delay,
77        };
78        let delay_ok = match retry.delay {
79            Delay::None => true,
80            Delay::Fixed { ms } => (1..=MAX_DELAY_MS).contains(&ms),
81            Delay::Exponential { initial_ms, max_ms } => {
82                1 <= initial_ms && initial_ms <= max_ms && max_ms <= MAX_DELAY_MS
83            }
84        };
85        if !(1..=64).contains(&w.max_pages)
86            || !(1..=100000).contains(&w.max_items)
87            || w.deadline_share <= Decimal::ZERO
88            || w.deadline_share > Decimal::from_i64(1)
89            || !(1..=8).contains(&retry.max_attempts)
90            || (retry.max_attempts == 1) != retry.on.is_empty()
91            || retry.on.iter().any(|c| !c.retryable())
92            || retry.on.iter().collect::<BTreeSet<_>>().len() != retry.on.len()
93            || !delay_ok
94            || w.as_of_skew_ms > 60000
95        {
96            return Err(serde::de::Error::custom("invalid acquisition limits"));
97        }
98        Ok(Self {
99            max_pages: w.max_pages,
100            max_items: w.max_items,
101            deadline_share: w.deadline_share,
102            max_attempts: retry.max_attempts,
103            retry_on: retry.on,
104            delay: retry.delay,
105            as_of_skew_ms: w.as_of_skew_ms,
106        })
107    }
108}
109
110fn count<'de, D: Deserializer<'de>>(d: D) -> Result<Expr, D::Error> {
111    let e = Expr::deserialize(d)?;
112    if matches!(&e, Expr::Lit(l) if !matches!(l, Literal::Integer(n) if *n >= 1)) {
113        return Err(serde::de::Error::custom(
114            "stop count must be a positive integer",
115        ));
116    }
117    Ok(e)
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
121#[serde(deny_unknown_fields)]
122pub struct Widen {
123    pub item: PoolItem,
124    #[schemars(length(min = 1, max = 4))]
125    pub sources: Vec<WidenSource>,
126    pub base_request: BTreeMap<String, Expr>,
127    pub groups: BTreeMap<String, Vec<String>>,
128    pub protected: Vec<String>,
129    #[schemars(length(min = 1, max = 8))]
130    pub tiers: Vec<Tier>,
131    #[schemars(range(min = 1, max = 8))]
132    pub max_tiers: u64,
133    pub dedupe_key: String,
134    pub order: PoolOrder,
135    pub stop: WidenStop,
136    pub on_tier_failure: OnTierFailure,
137    pub limits: AcquisitionLimits,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
141#[serde(deny_unknown_fields)]
142pub struct PoolItem {
143    #[serde(deserialize_with = "fields")]
144    #[schemars(length(min = 1))]
145    pub fields: Vec<FieldDecl>,
146}
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
148#[serde(deny_unknown_fields)]
149pub struct WidenSource {
150    pub source: String,
151    pub operation: String,
152    pub map: BTreeMap<String, String>,
153}
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct Tier {
157    pub name: String,
158    pub source_index: u64,
159    pub request_patch: BTreeMap<String, Expr>,
160    pub when: Guard,
161    pub relax: Vec<String>,
162}
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
164#[serde(rename_all = "snake_case")]
165pub enum PoolOrder {
166    TierThenSourceRank,
167}
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
169#[serde(rename_all = "snake_case")]
170pub enum OnTierFailure {
171    Continue,
172    Stop,
173}
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
175#[serde(rename_all = "snake_case", deny_unknown_fields)]
176pub enum WidenStop {
177    None,
178    UniqueAtLeast(#[serde(deserialize_with = "count")] Expr),
179}
180
181fn list<'de, D: Deserializer<'de>, T: Deserialize<'de>, const MIN: usize, const MAX: usize>(
182    d: D,
183) -> Result<Vec<T>, D::Error> {
184    let v = Vec::<T>::deserialize(d)?;
185    if !(MIN..=MAX).contains(&v.len()) {
186        return Err(serde::de::Error::custom("list outside acquisition bounds"));
187    }
188    Ok(v)
189}
190fn sources<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<WidenSource>, D::Error> {
191    list::<D, _, 1, 4>(d)
192}
193fn tiers<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Tier>, D::Error> {
194    list::<D, _, 1, 8>(d)
195}
196fn fields<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<FieldDecl>, D::Error> {
197    list::<D, _, 1, { usize::MAX }>(d)
198}
199fn max_tiers<'de, D: Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
200    let n = u64::deserialize(d)?;
201    if !(1..=8).contains(&n) {
202        return Err(serde::de::Error::custom("max_tiers outside 1..=8"));
203    }
204    Ok(n)
205}
206fn groups<'de, D: Deserializer<'de>>(d: D) -> Result<BTreeMap<String, Vec<String>>, D::Error> {
207    let g = BTreeMap::<String, Vec<String>>::deserialize(d)?;
208    let mut seen = BTreeSet::new();
209    if !(1..=16).contains(&g.len())
210        || g.values().any(|fields| {
211            !(1..=8).contains(&fields.len()) || fields.iter().any(|f| !seen.insert(f))
212        })
213    {
214        return Err(serde::de::Error::custom(
215            "groups must be disjoint with 1..=16 groups of 1..=8 fields",
216        ));
217    }
218    Ok(g)
219}
220
221/// A skipped step is a declared path, not an unresolved failure.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
223#[serde(rename_all = "snake_case", deny_unknown_fields)]
224pub enum AcquisitionOutcome {
225    Resolved,
226    Skipped { guard: String },
227    Unresolved { reason: crate::judgment::Unresolved },
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
231#[serde(deny_unknown_fields)]
232struct WidenWire {
233    pub item: PoolItem,
234    #[serde(deserialize_with = "sources")]
235    pub sources: Vec<WidenSource>,
236    pub base_request: BTreeMap<String, Expr>,
237    #[serde(deserialize_with = "groups")]
238    pub groups: BTreeMap<String, Vec<String>>,
239    pub protected: Vec<String>,
240    #[serde(deserialize_with = "tiers")]
241    pub tiers: Vec<Tier>,
242    #[serde(deserialize_with = "max_tiers")]
243    pub max_tiers: u64,
244    pub dedupe_key: String,
245    pub order: PoolOrder,
246    pub stop: WidenStop,
247    pub on_tier_failure: OnTierFailure,
248    pub limits: AcquisitionLimits,
249}
250
251// Group names in `protected` and in a tier's `relax` are not resolved here:
252// spec 036 3.5.4 makes "names an undeclared group" a compile refusal,
253// `invalid_acquisition` (category 20, phases b and e of 3.9.3), together
254// with the other protected-group rules, so parse admits the document and
255// core refuses it with that category.
256impl<'de> Deserialize<'de> for Widen {
257    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
258        let w = WidenWire::deserialize(d)?;
259        if w.tiers.len() as u64 > w.max_tiers
260            || w.tiers
261                .iter()
262                .any(|t| t.source_index >= w.sources.len() as u64)
263        {
264            return Err(serde::de::Error::custom(
265                "tier count or source index outside bounds",
266            ));
267        }
268        Ok(Self {
269            item: w.item,
270            sources: w.sources,
271            base_request: w.base_request,
272            groups: w.groups,
273            protected: w.protected,
274            tiers: w.tiers,
275            max_tiers: w.max_tiers,
276            dedupe_key: w.dedupe_key,
277            order: w.order,
278            stop: w.stop,
279            on_tier_failure: w.on_tier_failure,
280            limits: w.limits,
281        })
282    }
283}
284
285/// Plan-wide acquisition ceilings (spec 036, 3.9.4).
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
287#[serde(deny_unknown_fields)]
288pub struct AcquisitionBudget {
289    #[serde(deserialize_with = "budget_calls")]
290    #[schemars(range(min = 1, max = 4096))]
291    pub max_calls: u64,
292    // Spec 036, 3.9.4: zero items or bytes is valid on the wire; exceeding
293    // either ceiling with the steps' sums is a category 7 compile refusal.
294    pub max_items: u64,
295    pub max_bytes: u64,
296}
297
298fn budget_calls<'de, D: Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
299    let n = u64::deserialize(d)?;
300    if !(1..=4096).contains(&n) {
301        return Err(serde::de::Error::custom(
302            "acquisition.max_calls outside 1..=4096",
303        ));
304    }
305    Ok(n)
306}