Skip to main content

rustev_contract/
ranking.rs

1//! Ranking and selection declarations (spec 039). Compilation remains closed.
2use crate::definition::{BackendReq, ItemSource};
3use serde::{Deserialize, Deserializer, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
6#[serde(try_from = "RankWire")]
7pub struct Rank {
8    pub task: String,
9    pub question: String,
10    pub candidates: String,
11    pub items: ItemSource,
12    pub bind: String,
13    pub project: Vec<String>,
14    pub mode: RankMode,
15    pub bias: Bias,
16    pub tie_break: TieBreak,
17    pub backend: BackendReq,
18    pub fallback: RankFallback,
19}
20
21#[derive(Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23#[schemars(rename = "Rank")]
24struct RankWire {
25    task: String,
26    question: String,
27    #[serde(deserialize_with = "step_ref")]
28    candidates: String,
29    items: ItemSource,
30    bind: String,
31    project: Vec<String>,
32    mode: RankMode,
33    bias: Bias,
34    tie_break: TieBreak,
35    backend: BackendReq,
36    fallback: RankFallback,
37}
38impl TryFrom<RankWire> for Rank {
39    type Error = String;
40    fn try_from(w: RankWire) -> Result<Self, Self::Error> {
41        if matches!(w.mode, RankMode::Pointwise) && w.bias != Bias::None {
42            return Err("bias on pointwise".into());
43        }
44        if matches!(w.mode, RankMode::Pairwise { .. })
45            && matches!(w.bias, Bias::PermutationAverage { orders, .. } if orders != 2)
46        {
47            return Err("pairwise orders must be 2".into());
48        }
49        Ok(Self {
50            task: w.task,
51            question: w.question,
52            candidates: w.candidates,
53            items: w.items,
54            bind: w.bind,
55            project: w.project,
56            mode: w.mode,
57            bias: w.bias,
58            tie_break: w.tie_break,
59            backend: w.backend,
60            fallback: w.fallback,
61        })
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(rename_all = "snake_case", deny_unknown_fields)]
67pub enum RankMode {
68    Listwise {
69        scores: RankScores,
70    },
71    Pointwise,
72    Pairwise {
73        schedule: PairwiseSchedule,
74        #[serde(deserialize_with = "bound::<_, 1, 4096>")]
75        max_comparisons: u64,
76    },
77}
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
79#[serde(rename_all = "snake_case")]
80pub enum RankScores {
81    None,
82    Optional,
83    Required,
84}
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
86pub enum PairwiseSchedule {
87    #[serde(rename = "round_robin@1")]
88    RoundRobin,
89    #[serde(rename = "merge_sort@1")]
90    MergeSort,
91}
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
93#[serde(rename_all = "snake_case", deny_unknown_fields)]
94pub enum Bias {
95    None,
96    Shuffle {
97        #[serde(deserialize_with = "seed")]
98        seed: String,
99    },
100    PermutationAverage {
101        #[serde(deserialize_with = "bound::<_, 2, 8>")]
102        orders: u64,
103        #[serde(deserialize_with = "seed")]
104        seed: String,
105    },
106}
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
108#[serde(rename_all = "snake_case", deny_unknown_fields)]
109pub enum TieBreak {
110    IdAscending,
111    OrderOf(#[serde(deserialize_with = "step_ref")] String),
112}
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
114#[serde(rename_all = "snake_case")]
115pub enum RankFallback {
116    None,
117    Unsupported,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
121#[serde(try_from = "SelectWire")]
122pub struct Select {
123    pub from: String,
124    pub k: u64,
125    pub backfill: Backfill,
126    pub on_backend_failure: BackendFailure,
127}
128#[derive(Deserialize, schemars::JsonSchema)]
129#[serde(deny_unknown_fields)]
130#[schemars(rename = "Select")]
131struct SelectWire {
132    #[serde(deserialize_with = "step_ref")]
133    from: String,
134    #[serde(deserialize_with = "bound::<_, 1, 1024>")]
135    k: u64,
136    backfill: Backfill,
137    on_backend_failure: BackendFailure,
138}
139impl TryFrom<SelectWire> for Select {
140    type Error = String;
141    fn try_from(w: SelectWire) -> Result<Self, Self::Error> {
142        if w.backfill == Backfill::None && w.on_backend_failure == BackendFailure::BackfillAll {
143            return Err("backfill_all requires backfill".into());
144        }
145        Ok(Self {
146            from: w.from,
147            k: w.k,
148            backfill: w.backfill,
149            on_backend_failure: w.on_backend_failure,
150        })
151    }
152}
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
154#[serde(rename_all = "snake_case", deny_unknown_fields)]
155pub enum Backfill {
156    None,
157    // Variant-level `untagged` (serde 1.0.181 and later; the lockfile has
158    // 1.0.229): `none` stays a tagged string and `From` is the flat
159    // `{"from", "exclude_selected"}` object of spec 039 3.10.
160    // Serde applies the enum's `deny_unknown_fields` to this untagged
161    // struct variant too; extra keys are rejected, not silently dropped.
162    #[serde(untagged)]
163    From {
164        #[serde(deserialize_with = "step_ref")]
165        from: String,
166        #[serde(deserialize_with = "must_be_true")]
167        exclude_selected: bool,
168    },
169}
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
171#[serde(rename_all = "snake_case")]
172pub enum BackendFailure {
173    BackfillAll,
174    Propagate,
175}
176
177pub(crate) fn bound<'de, D: Deserializer<'de>, const MIN: u64, const MAX: u64>(
178    d: D,
179) -> Result<u64, D::Error> {
180    let n = u64::deserialize(d)?;
181    if !(MIN..=MAX).contains(&n) {
182        return Err(serde::de::Error::custom("ranking bound out of range"));
183    }
184    Ok(n)
185}
186fn seed<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
187    let s = String::deserialize(d)?;
188    if !(1..=64).contains(&s.len())
189        || !s
190            .bytes()
191            .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
192    {
193        return Err(serde::de::Error::custom("bad ranking seed"));
194    }
195    Ok(s)
196}
197fn step_ref<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
198    let s = String::deserialize(d)?;
199    if !s.strip_prefix("step:").is_some_and(|id| {
200        !id.is_empty() && !id.contains(':') && !id.chars().any(char::is_whitespace)
201    }) {
202        return Err(serde::de::Error::custom("expected step:<id>"));
203    }
204    Ok(s)
205}
206fn must_be_true<'de, D: Deserializer<'de>>(d: D) -> Result<bool, D::Error> {
207    if !bool::deserialize(d)? {
208        return Err(serde::de::Error::custom("exclude_selected must be true"));
209    }
210    Ok(true)
211}