1use 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#[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
251impl<'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#[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 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}