1use serde_json::{Map, Value, json};
4
5use crate::{CommandError, ParamKind, ParamSpec, Params, legacy};
6
7const MAX_KEY_SHOWN: usize = 64;
9const MAX_UNKNOWN_SHOWN: usize = 8;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum Kind {
15 Bool,
16 Int { min: Option<i64>, max: Option<i64> },
17 Float { min: Option<f64>, max: Option<f64> },
18 String,
19 Enum(&'static [&'static str]),
20 Json,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Param {
27 pub name: &'static str,
28 pub kind: Kind,
29 pub required: bool,
30 pub about: &'static str,
31}
32
33impl Param {
34 pub const fn new(name: &'static str, kind: Kind) -> Self {
35 Self { name, kind, required: false, about: "" }
36 }
37 pub const fn bool(name: &'static str) -> Self {
38 Self::new(name, Kind::Bool)
39 }
40 pub const fn int(name: &'static str) -> Self {
41 Self::new(name, Kind::Int { min: None, max: None })
42 }
43 pub const fn int_in(name: &'static str, min: i64, max: i64) -> Self {
44 Self::new(name, Kind::Int { min: Some(min), max: Some(max) })
45 }
46 pub const fn float(name: &'static str) -> Self {
47 Self::new(name, Kind::Float { min: None, max: None })
48 }
49 pub const fn float_in(name: &'static str, min: f64, max: f64) -> Self {
50 Self::new(name, Kind::Float { min: Some(min), max: Some(max) })
51 }
52 pub const fn string(name: &'static str) -> Self {
53 Self::new(name, Kind::String)
54 }
55 pub const fn one_of(name: &'static str, values: &'static [&'static str]) -> Self {
56 Self::new(name, Kind::Enum(values))
57 }
58 pub const fn json(name: &'static str) -> Self {
59 Self::new(name, Kind::Json)
60 }
61 pub const fn required(mut self) -> Self {
62 self.required = true;
63 self
64 }
65 pub const fn about(mut self, about: &'static str) -> Self {
66 self.about = about;
67 self
68 }
69
70 pub fn to_spec(&self) -> ParamSpec {
72 let kind = match self.kind {
73 Kind::Bool => ParamKind::Bool,
74 Kind::Int { min, max } => ParamKind::Int { min, max },
75 Kind::Float { min, max } => ParamKind::Float { min, max },
76 Kind::String => ParamKind::String,
77 Kind::Enum(values) => ParamKind::Enum { values: values.iter().map(|v| (*v).to_string()).collect() },
78 Kind::Json => ParamKind::Json,
79 };
80 ParamSpec { name: self.name.to_string(), kind, required: self.required, about: self.about.to_string() }
81 }
82}
83
84#[derive(Clone, Copy)]
86enum Values<'a> {
87 Owned(&'a [String]),
88 Static(&'a [&'static str]),
89}
90
91impl Values<'_> {
92 fn contains(self, s: &str) -> bool {
93 match self {
94 Values::Owned(v) => v.iter().any(|x| x == s),
95 Values::Static(v) => v.contains(&s),
96 }
97 }
98 fn strings(self) -> Vec<String> {
99 match self {
100 Values::Owned(v) => v.to_vec(),
101 Values::Static(v) => v.iter().map(|s| (*s).to_string()).collect(),
102 }
103 }
104}
105
106#[derive(Clone, Copy)]
107enum KindView<'a> {
108 Bool,
109 Int(Option<i64>, Option<i64>),
110 Float(Option<f64>, Option<f64>),
111 Str,
112 Enum(Values<'a>),
113 Json,
114}
115
116#[derive(Clone, Copy)]
118struct View<'a> {
119 name: &'a str,
120 kind: KindView<'a>,
121 required: bool,
122 about: &'a str,
123}
124
125fn views(params: &Params) -> Option<Vec<View<'_>>> {
126 match params {
127 Params::None => Some(vec![]),
128 Params::Text(_) => None,
129 Params::Typed(specs) => Some(
130 specs
131 .iter()
132 .map(|p| View {
133 name: &p.name,
134 kind: match &p.kind {
135 ParamKind::Bool => KindView::Bool,
136 ParamKind::Int { min, max } => KindView::Int(*min, *max),
137 ParamKind::Float { min, max } => KindView::Float(*min, *max),
138 ParamKind::String => KindView::Str,
139 ParamKind::Enum { values } => KindView::Enum(Values::Owned(values)),
140 ParamKind::Json => KindView::Json,
141 },
142 required: p.required,
143 about: &p.about,
144 })
145 .collect(),
146 ),
147 Params::Static(specs) => Some(
148 specs
149 .iter()
150 .map(|p| View {
151 name: p.name,
152 kind: match p.kind {
153 Kind::Bool => KindView::Bool,
154 Kind::Int { min, max } => KindView::Int(min, max),
155 Kind::Float { min, max } => KindView::Float(min, max),
156 Kind::String => KindView::Str,
157 Kind::Enum(values) => KindView::Enum(Values::Static(values)),
158 Kind::Json => KindView::Json,
159 },
160 required: p.required,
161 about: p.about,
162 })
163 .collect(),
164 ),
165 }
166}
167
168pub(crate) fn shown(key: &str) -> String {
169 match key.char_indices().nth(MAX_KEY_SHOWN) {
170 Some((i, _)) => format!("`{}…`", key.get(..i).unwrap_or_default()),
171 None => format!("`{key}`"),
172 }
173}
174
175pub(crate) fn json_type(v: &Value) -> &'static str {
176 match v {
177 Value::Null => "null",
178 Value::Bool(_) => "a boolean",
179 Value::Number(_) => "a number",
180 Value::String(_) => "a string",
181 Value::Array(_) => "an array",
182 Value::Object(_) => "an object",
183 }
184}
185
186pub(crate) fn unknown_keys_error(unknown: &[&str], accepted: &[&str]) -> String {
188 let mut names: Vec<String> = unknown
189 .iter()
190 .take(MAX_UNKNOWN_SHOWN)
191 .map(|k| match crate::suggest::closest(k, accepted.iter().copied(), 1).first() {
192 Some(meant) => format!("{} (did you mean `{meant}`?)", shown(k)),
193 None => shown(k),
194 })
195 .collect();
196 if unknown.len() > MAX_UNKNOWN_SHOWN {
197 names.push(format!("and {} more", unknown.len() - MAX_UNKNOWN_SHOWN));
198 }
199 let list = if accepted.is_empty() { "none".to_string() } else { accepted.join(", ") };
200 format!("unknown parameter(s) {}; accepted: {list}", names.join(", "))
201}
202
203fn as_int(v: &Value) -> Option<i64> {
205 if let Some(i) = v.as_i64() {
206 return Some(i);
207 }
208 let f = v.as_f64()?;
209 const EXACT: f64 = 9_007_199_254_740_992.0;
211 (f.is_finite() && f.fract() == 0.0 && f.abs() <= EXACT).then_some(f as i64)
212}
213
214fn check_value(p: &View<'_>, v: &Value) -> Result<(), String> {
215 let name = shown(p.name);
216 match p.kind {
217 KindView::Json => Ok(()),
218 KindView::Bool => v.is_boolean().then_some(()).ok_or_else(|| format!("{name} must be true or false, got {}", json_type(v))),
219 KindView::Str => v.is_string().then_some(()).ok_or_else(|| format!("{name} must be a string, got {}", json_type(v))),
220 KindView::Enum(values) => {
221 let all = values.strings().join(", ");
222 match v.as_str() {
223 Some(s) if values.contains(s) => Ok(()),
224 Some(s) => Err(format!("{name} must be one of {all}, got {}", shown(s))),
225 None => Err(format!("{name} must be one of {all}, got {}", json_type(v))),
226 }
227 }
228 KindView::Int(min, max) => {
229 let Some(i) = as_int(v) else {
230 return Err(if v.is_number() {
231 format!("{name} must be a whole number, got {v}")
232 } else {
233 format!("{name} must be a whole number, got {}", json_type(v))
234 });
235 };
236 if let Some(m) = min.filter(|m| i < *m) {
237 return Err(format!("{name} must be at least {m}, got {i}"));
238 }
239 if let Some(m) = max.filter(|m| i > *m) {
240 return Err(format!("{name} must be at most {m}, got {i}"));
241 }
242 Ok(())
243 }
244 KindView::Float(min, max) => {
245 let Some(f) = v.as_f64().filter(|f| f.is_finite()) else {
246 return Err(format!("{name} must be a number, got {}", json_type(v)));
247 };
248 if let Some(m) = min.filter(|m| f < *m) {
249 return Err(format!("{name} must be at least {m}, got {f}"));
250 }
251 if let Some(m) = max.filter(|m| f > *m) {
252 return Err(format!("{name} must be at most {m}, got {f}"));
253 }
254 Ok(())
255 }
256 }
257}
258
259pub fn validate_params(id: &str, params: &Params, value: &Value, common: &[&str], aliases: &[(&str, &str)]) -> Result<(), CommandError> {
264 let Some(views) = views(params) else {
265 return match params {
266 Params::Text(doc) => legacy::check(doc, value, common, aliases).map_err(|msg| CommandError::bad_params(id, msg)),
267 _ => Ok(()),
268 };
269 };
270 let empty = Map::new();
271 let obj = match value {
272 Value::Null => &empty,
273 Value::Object(m) => m,
274 other => return Err(CommandError::bad_params(id, format!("expected an object of parameters, got {}", json_type(other)))),
275 };
276 let mut errors: Vec<String> = Vec::new();
277 let unknown: Vec<&str> = obj.keys().map(String::as_str).filter(|k| !common.contains(k) && !views.iter().any(|p| p.name == *k)).collect();
278 if !unknown.is_empty() {
279 let accepted: Vec<&str> = views.iter().map(|p| p.name).collect();
280 errors.push(unknown_keys_error(&unknown, &accepted));
281 }
282 for p in &views {
283 match obj.get(p.name) {
284 None | Some(Value::Null) if p.required => errors.push(format!("missing required parameter {}", shown(p.name))),
285 None | Some(Value::Null) => {}
286 Some(v) => {
287 if let Err(e) = check_value(p, v) {
288 errors.push(e);
289 }
290 }
291 }
292 }
293 if errors.is_empty() { Ok(()) } else { Err(CommandError::bad_params(id, errors.join("; "))) }
294}
295
296fn property(p: &View<'_>) -> Value {
297 let mut m = Map::new();
298 match p.kind {
299 KindView::Bool => {
300 m.insert("type".into(), "boolean".into());
301 }
302 KindView::Str => {
303 m.insert("type".into(), "string".into());
304 }
305 KindView::Json => {}
306 KindView::Enum(values) => {
307 m.insert("type".into(), "string".into());
308 m.insert("enum".into(), values.strings().into());
309 }
310 KindView::Int(min, max) => {
311 m.insert("type".into(), "integer".into());
312 if let Some(v) = min {
313 m.insert("minimum".into(), v.into());
314 }
315 if let Some(v) = max {
316 m.insert("maximum".into(), v.into());
317 }
318 }
319 KindView::Float(min, max) => {
320 m.insert("type".into(), "number".into());
321 if let Some(v) = min {
322 m.insert("minimum".into(), v.into());
323 }
324 if let Some(v) = max {
325 m.insert("maximum".into(), v.into());
326 }
327 }
328 }
329 if !p.about.is_empty() {
330 m.insert("description".into(), p.about.into());
331 }
332 Value::Object(m)
333}
334
335pub fn params_schema(params: &Params) -> Value {
338 match views(params) {
339 Some(views) => {
340 let props: Map<String, Value> = views.iter().map(|p| (p.name.to_string(), property(p))).collect();
341 let required: Vec<&str> = views.iter().filter(|p| p.required).map(|p| p.name).collect();
342 json!({"type": "object", "properties": props, "required": required, "additionalProperties": false})
343 }
344 None => match params {
345 Params::Text(doc) => legacy::schema(doc),
346 _ => json!({"type": "object"}),
347 },
348 }
349}