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