1use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::Error;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum ParamType {
15 #[serde(rename = "integer")]
17 Integer {
18 min: Option<i64>,
20 max: Option<i64>,
22 },
23 #[serde(rename = "number")]
25 Float {
26 min: Option<f64>,
28 max: Option<f64>,
30 unit: Option<String>,
32 },
33 #[serde(rename = "boolean")]
35 Boolean,
36 Choice {
38 choices: Vec<String>,
40 },
41 #[serde(rename = "string")]
43 Text,
44 #[serde(rename = "array")]
46 Vector3 {
47 unit: Option<String>,
49 },
50 Path {
52 must_exist: bool,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct ParamSpec {
60 pub name: String,
62 pub kind: ParamType,
64 pub default: Value,
66 pub help: String,
68 pub required: bool,
70 pub group: Option<String>,
72}
73
74impl ParamSpec {
75 pub fn validate(&self, value: &Value) -> Result<(), Error> {
81 let fail = |reason: &str| {
82 Err(Error::InvalidInput(format!(
83 "parameter {}: {reason}",
84 self.name
85 )))
86 };
87 match &self.kind {
88 ParamType::Integer { min, max } => {
89 let Some(number) = value.as_i64() else {
90 return fail("expected an integer");
91 };
92 if min.is_some_and(|min| number < min) {
93 return fail("below the minimum");
94 }
95 if max.is_some_and(|max| number > max) {
96 return fail("above the maximum");
97 }
98 }
99 ParamType::Float { min, max, .. } => {
100 let Some(number) = value.as_f64() else {
101 return fail("expected a number");
102 };
103 if min.is_some_and(|min| number < min) {
104 return fail("below the minimum");
105 }
106 if max.is_some_and(|max| number > max) {
107 return fail("above the maximum");
108 }
109 }
110 ParamType::Boolean => {
111 if !value.is_boolean() {
112 return fail("expected true or false");
113 }
114 }
115 ParamType::Choice { choices } => {
116 let Some(text) = value.as_str() else {
117 return fail("expected a choice string");
118 };
119 if !choices.iter().any(|choice| choice == text) {
120 return fail("not one of the allowed choices");
121 }
122 }
123 ParamType::Text => {
124 if !value.is_string() {
125 return fail("expected text");
126 }
127 }
128 ParamType::Vector3 { .. } => {
129 let Some(values) = value.as_array() else {
130 return fail("expected three numbers");
131 };
132 if values.len() != 3 || !values.iter().all(Value::is_number) {
133 return fail("expected three numbers");
134 }
135 }
136 ParamType::Path { .. } => {
137 if !value.is_string() {
138 return fail("expected a path string");
139 }
140 }
141 }
142 Ok(())
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct ParamSet {
149 pub specs: Vec<ParamSpec>,
151 pub values: BTreeMap<String, Value>,
153}
154
155impl ParamSet {
156 #[must_use]
158 pub fn new() -> Self {
159 Self {
160 specs: Vec::new(),
161 values: BTreeMap::new(),
162 }
163 }
164
165 pub fn with_param(mut self, spec: ParamSpec) -> Result<Self, Error> {
172 if self.specs.iter().any(|existing| existing.name == spec.name) {
173 return Err(Error::InvalidInput(format!(
174 "duplicate parameter {}",
175 spec.name
176 )));
177 }
178 spec.validate(&spec.default)?;
179 self.values.insert(spec.name.clone(), spec.default.clone());
180 self.specs.push(spec);
181 Ok(self)
182 }
183
184 pub fn set(&mut self, name: &str, value: Value) -> Result<(), Error> {
191 let spec = self
192 .specs
193 .iter()
194 .find(|spec| spec.name == name)
195 .ok_or_else(|| Error::InvalidInput(format!("unknown parameter {name}")))?;
196 spec.validate(&value)?;
197 self.values.insert(name.to_owned(), value);
198 Ok(())
199 }
200
201 #[must_use]
203 pub fn get(&self, name: &str) -> Option<&Value> {
204 self.values.get(name)
205 }
206
207 pub fn check_required(&self) -> Result<(), Error> {
213 for spec in &self.specs {
214 if spec.required && !self.values.contains_key(&spec.name) {
215 return Err(Error::InvalidInput(format!(
216 "missing required parameter {}",
217 spec.name
218 )));
219 }
220 }
221 Ok(())
222 }
223
224 #[must_use]
226 pub fn schema(&self) -> Value {
227 let properties: BTreeMap<&str, Value> = self
228 .specs
229 .iter()
230 .map(|spec| (spec.name.as_str(), spec.kind.clone().into_value(spec)))
231 .collect();
232 serde_json::json!({
233 "type": "object",
234 "properties": properties,
235 "required": self.specs.iter().filter(|spec| spec.required).map(|spec| spec.name.as_str()).collect::<Vec<_>>(),
236 })
237 }
238}
239
240impl Default for ParamSet {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246impl ParamType {
247 fn into_value(self, spec: &ParamSpec) -> Value {
248 let mut object = serde_json::to_value(&self).unwrap_or(Value::Null);
249 if let Some(map) = object.as_object_mut() {
250 map.insert("default".to_owned(), spec.default.clone());
251 map.insert("description".to_owned(), Value::String(spec.help.clone()));
252 if let Some(group) = &spec.group {
253 map.insert("group".to_owned(), Value::String(group.clone()));
254 }
255 }
256 object
257 }
258}
259
260impl ParamSet {
261 pub fn to_json(&self) -> Result<String, Error> {
267 serde_json::to_string_pretty(self).map_err(|error| {
268 Error::InvalidInput(format!("could not serialize parameters: {error}"))
269 })
270 }
271
272 pub fn from_json(text: &str) -> Result<Self, Error> {
279 let set: Self = serde_json::from_str(text)
280 .map_err(|error| Error::InvalidInput(format!("invalid parameters: {error}")))?;
281 for (name, value) in &set.values {
282 let spec = set
283 .specs
284 .iter()
285 .find(|spec| spec.name == *name)
286 .ok_or_else(|| Error::InvalidInput(format!("unknown parameter {name}")))?;
287 spec.validate(value)?;
288 }
289 Ok(set)
290 }
291}