1use super::constrained_strings::{Description, Identifier};
8use crate::types::{DataFlow, ObjectType};
9use serde::Deserialize;
10
11#[derive(Debug, Clone)]
21pub struct NullableVec<T>(pub Option<Vec<T>>);
22
23impl<T> Default for NullableVec<T> {
24 fn default() -> Self {
25 NullableVec(None)
26 }
27}
28
29impl<T> NullableVec<T> {
30 pub fn as_ref(&self) -> Option<&Vec<T>> {
31 self.0.as_ref()
32 }
33 pub fn is_some(&self) -> bool {
34 self.0.is_some()
35 }
36 pub fn is_none(&self) -> bool {
37 self.0.is_none()
38 }
39}
40
41impl<'de, T: serde::de::DeserializeOwned> Deserialize<'de> for NullableVec<T> {
42 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43 match Option::<Vec<T>>::deserialize(deserializer)? {
44 None => Err(serde::de::Error::custom(
45 "null is not allowed for this field",
46 )),
47 Some(vec) => Ok(NullableVec(Some(vec))),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
57#[allow(non_camel_case_types)]
58pub enum JobParameterDefinition {
59 STRING(JobStringParameterDefinition),
60 INT(JobIntParameterDefinition),
61 FLOAT(JobFloatParameterDefinition),
62 PATH(JobPathParameterDefinition),
63 BOOL(super::expr_parameters::JobBoolParameterDefinition),
65 RANGE_EXPR(super::expr_parameters::JobRangeExprParameterDefinition),
66 LIST_STRING(super::expr_parameters::JobListStringParameterDefinition),
67 LIST_PATH(super::expr_parameters::JobListPathParameterDefinition),
68 LIST_INT(super::expr_parameters::JobListIntParameterDefinition),
69 LIST_FLOAT(super::expr_parameters::JobListFloatParameterDefinition),
70 LIST_BOOL(super::expr_parameters::JobListBoolParameterDefinition),
71 LIST_LIST_INT(super::expr_parameters::JobListListIntParameterDefinition),
72}
73
74fn strip_type_field(mut value: serde_json::Value) -> serde_json::Value {
76 if let Some(obj) = value.as_object_mut() {
77 obj.remove("type");
78 }
79 value
80}
81
82impl<'de> serde::Deserialize<'de> for JobParameterDefinition {
83 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84 let value = serde_json::Value::deserialize(deserializer)?;
85 let type_str = value
86 .get("type")
87 .and_then(|v| v.as_str())
88 .ok_or_else(|| {
89 serde::de::Error::custom("missing 'type' field in parameter definition")
90 })?
91 .to_string();
92
93 let normalized = type_str.to_uppercase();
94 let stripped = strip_type_field(value);
95
96 match normalized.as_str() {
97 "STRING" => serde_json::from_value(stripped)
98 .map(Self::STRING)
99 .map_err(serde::de::Error::custom),
100 "INT" => serde_json::from_value(stripped)
101 .map(Self::INT)
102 .map_err(serde::de::Error::custom),
103 "FLOAT" => serde_json::from_value(stripped)
104 .map(Self::FLOAT)
105 .map_err(serde::de::Error::custom),
106 "PATH" => serde_json::from_value(stripped)
107 .map(Self::PATH)
108 .map_err(serde::de::Error::custom),
109 "BOOL" => serde_json::from_value(stripped)
110 .map(Self::BOOL)
111 .map_err(serde::de::Error::custom),
112 "RANGE_EXPR" => serde_json::from_value(stripped)
113 .map(Self::RANGE_EXPR)
114 .map_err(serde::de::Error::custom),
115 "LIST[STRING]" => serde_json::from_value(stripped)
116 .map(Self::LIST_STRING)
117 .map_err(serde::de::Error::custom),
118 "LIST[PATH]" => serde_json::from_value(stripped)
119 .map(Self::LIST_PATH)
120 .map_err(serde::de::Error::custom),
121 "LIST[INT]" => serde_json::from_value(stripped)
122 .map(Self::LIST_INT)
123 .map_err(serde::de::Error::custom),
124 "LIST[FLOAT]" => serde_json::from_value(stripped)
125 .map(Self::LIST_FLOAT)
126 .map_err(serde::de::Error::custom),
127 "LIST[BOOL]" => serde_json::from_value(stripped)
128 .map(Self::LIST_BOOL)
129 .map_err(serde::de::Error::custom),
130 "LIST[LIST[INT]]" => serde_json::from_value(stripped)
131 .map(Self::LIST_LIST_INT)
132 .map_err(serde::de::Error::custom),
133 _ => Err(serde::de::Error::custom(format!(
134 "unknown parameter type: '{type_str}'"
135 ))),
136 }
137 }
138}
139
140impl JobParameterDefinition {
141 pub fn job_param_type(&self) -> crate::types::JobParameterType {
142 use crate::types::JobParameterType;
143 match self {
144 Self::STRING(_) => JobParameterType::String,
145 Self::INT(_) => JobParameterType::Int,
146 Self::FLOAT(_) => JobParameterType::Float,
147 Self::PATH(_) => JobParameterType::Path,
148 Self::BOOL(_) => JobParameterType::Bool,
149 Self::RANGE_EXPR(_) => JobParameterType::RangeExpr,
150 Self::LIST_STRING(_) => JobParameterType::ListString,
151 Self::LIST_PATH(_) => JobParameterType::ListPath,
152 Self::LIST_INT(_) => JobParameterType::ListInt,
153 Self::LIST_FLOAT(_) => JobParameterType::ListFloat,
154 Self::LIST_BOOL(_) => JobParameterType::ListBool,
155 Self::LIST_LIST_INT(_) => JobParameterType::ListListInt,
156 }
157 }
158
159 pub fn name(&self) -> &str {
160 match self {
161 Self::STRING(p) => p.name.as_str(),
162 Self::INT(p) => p.name.as_str(),
163 Self::FLOAT(p) => p.name.as_str(),
164 Self::PATH(p) => p.name.as_str(),
165 Self::BOOL(p) => p.name.as_str(),
166 Self::RANGE_EXPR(p) => p.name.as_str(),
167 Self::LIST_STRING(p) => p.name.as_str(),
168 Self::LIST_PATH(p) => p.name.as_str(),
169 Self::LIST_INT(p) => p.name.as_str(),
170 Self::LIST_FLOAT(p) => p.name.as_str(),
171 Self::LIST_BOOL(p) => p.name.as_str(),
172 Self::LIST_LIST_INT(p) => p.name.as_str(),
173 }
174 }
175
176 pub fn description(&self) -> Option<&str> {
177 match self {
178 Self::STRING(p) => p.description.as_ref().map(|d| d.0.as_str()),
179 Self::INT(p) => p.description.as_ref().map(|d| d.0.as_str()),
180 Self::FLOAT(p) => p.description.as_ref().map(|d| d.0.as_str()),
181 Self::PATH(p) => p.description.as_ref().map(|d| d.0.as_str()),
182 Self::BOOL(p) => p.description.as_ref().map(|d| d.0.as_str()),
183 Self::RANGE_EXPR(p) => p.description.as_ref().map(|d| d.0.as_str()),
184 Self::LIST_STRING(p) => p.description.as_ref().map(|d| d.0.as_str()),
185 Self::LIST_PATH(p) => p.description.as_ref().map(|d| d.0.as_str()),
186 Self::LIST_INT(p) => p.description.as_ref().map(|d| d.0.as_str()),
187 Self::LIST_FLOAT(p) => p.description.as_ref().map(|d| d.0.as_str()),
188 Self::LIST_BOOL(p) => p.description.as_ref().map(|d| d.0.as_str()),
189 Self::LIST_LIST_INT(p) => p.description.as_ref().map(|d| d.0.as_str()),
190 }
191 }
192
193 pub fn type_name(&self) -> &str {
194 match self {
195 Self::STRING(_) => "STRING",
196 Self::INT(_) => "INT",
197 Self::FLOAT(_) => "FLOAT",
198 Self::PATH(_) => "PATH",
199 Self::BOOL(_) => "BOOL",
200 Self::RANGE_EXPR(_) => "RANGE_EXPR",
201 Self::LIST_STRING(_) => "LIST[STRING]",
202 Self::LIST_PATH(_) => "LIST[PATH]",
203 Self::LIST_INT(_) => "LIST[INT]",
204 Self::LIST_FLOAT(_) => "LIST[FLOAT]",
205 Self::LIST_BOOL(_) => "LIST[BOOL]",
206 Self::LIST_LIST_INT(_) => "LIST[LIST[INT]]",
207 }
208 }
209
210 pub fn path_properties(&self) -> (Option<ObjectType>, Option<DataFlow>) {
211 match self {
212 Self::PATH(p) => (p.object_type, p.data_flow),
213 Self::LIST_PATH(p) => (p.object_type, p.data_flow),
214 _ => (None, None),
215 }
216 }
217
218 pub fn default_value(&self) -> Option<String> {
219 match self {
220 Self::STRING(p) => p.default.clone(),
221 Self::INT(p) => p.default.as_ref().map(|v| v.to_string()),
222 Self::FLOAT(p) => p
223 .default
224 .as_ref()
225 .map(|v| v.1.clone().unwrap_or_else(|| v.0.to_string())),
226 Self::PATH(p) => p.default.clone(),
227 Self::BOOL(p) => p.default.as_ref().map(|v| v.0.to_string()),
228 Self::RANGE_EXPR(p) => p.default.clone(),
229 Self::LIST_STRING(p) => p
230 .default
231 .as_ref()
232 .map(|v| serde_json::to_string(v).unwrap_or_default()),
233 Self::LIST_PATH(p) => p
234 .default
235 .as_ref()
236 .map(|v| serde_json::to_string(v).unwrap_or_default()),
237 Self::LIST_INT(p) => p.default.as_ref().map(|v| {
238 let ints: Vec<i64> = v.iter().map(|i| i.0).collect();
239 serde_json::to_string(&ints).unwrap_or_default()
240 }),
241 Self::LIST_FLOAT(p) => p.default.as_ref().map(|v| {
242 let floats: Vec<f64> = v.iter().map(|f| f.0).collect();
243 serde_json::to_string(&floats).unwrap_or_default()
244 }),
245 Self::LIST_BOOL(p) => p.default.as_ref().map(|v| {
246 let bools: Vec<bool> = v.iter().map(|b| b.0).collect();
247 serde_json::to_string(&bools).unwrap_or_default()
248 }),
249 Self::LIST_LIST_INT(p) => p.default.as_ref().map(|v| {
250 let lists: Vec<Vec<i64>> = v
251 .iter()
252 .map(|inner| inner.iter().map(|i| i.0).collect())
253 .collect();
254 serde_json::to_string(&lists).unwrap_or_default()
255 }),
256 }
257 }
258
259 pub fn min_value_i64(&self) -> Option<i64> {
260 match self {
261 Self::INT(p) => p.min_value.as_ref().map(|v| v.0),
262 _ => None,
263 }
264 }
265
266 pub fn max_value_i64(&self) -> Option<i64> {
267 match self {
268 Self::INT(p) => p.max_value.as_ref().map(|v| v.0),
269 _ => None,
270 }
271 }
272
273 pub fn allowed_values_i64(&self) -> Option<Vec<i64>> {
274 match self {
275 Self::INT(p) => p
276 .allowed_values
277 .as_ref()
278 .map(|v| v.iter().map(|i| i.0).collect()),
279 _ => None,
280 }
281 }
282
283 pub fn allowed_values_f64(&self) -> Option<Vec<f64>> {
284 match self {
285 Self::FLOAT(p) => p
286 .allowed_values
287 .as_ref()
288 .map(|v| v.iter().map(|f| f.0).collect()),
289 _ => None,
290 }
291 }
292
293 pub fn allowed_values_strings(&self) -> Option<Vec<String>> {
294 match self {
295 Self::STRING(p) => p.allowed_values.clone(),
296 Self::PATH(p) => p.allowed_values.clone(),
297 _ => None,
298 }
299 }
300
301 pub fn min_length(&self) -> Option<usize> {
302 match self {
303 Self::STRING(p) => p.min_length,
304 Self::PATH(p) => p.min_length,
305 Self::RANGE_EXPR(p) => p.min_length,
306 Self::LIST_STRING(p) => p.min_length,
307 Self::LIST_PATH(p) => p.min_length,
308 Self::LIST_INT(p) => p.min_length,
309 Self::LIST_FLOAT(p) => p.min_length,
310 Self::LIST_BOOL(p) => p.min_length,
311 Self::LIST_LIST_INT(p) => p.min_length,
312 _ => None,
313 }
314 }
315
316 pub fn max_length(&self) -> Option<usize> {
317 match self {
318 Self::STRING(p) => p.max_length,
319 Self::PATH(p) => p.max_length,
320 Self::RANGE_EXPR(p) => p.max_length,
321 Self::LIST_STRING(p) => p.max_length,
322 Self::LIST_PATH(p) => p.max_length,
323 Self::LIST_INT(p) => p.max_length,
324 Self::LIST_FLOAT(p) => p.max_length,
325 Self::LIST_BOOL(p) => p.max_length,
326 Self::LIST_LIST_INT(p) => p.max_length,
327 _ => None,
328 }
329 }
330
331 pub fn min_value_f64(&self) -> Option<f64> {
332 match self {
333 Self::FLOAT(p) => p.min_value.as_ref().map(|v| v.0),
334 _ => None,
335 }
336 }
337
338 pub fn max_value_f64(&self) -> Option<f64> {
339 match self {
340 Self::FLOAT(p) => p.max_value.as_ref().map(|v| v.0),
341 _ => None,
342 }
343 }
344
345 pub fn item_min_value_i64(&self) -> Option<i64> {
349 match self {
350 Self::LIST_INT(p) => p
351 .item
352 .as_ref()
353 .and_then(|i| i.min_value.as_ref().map(|v| v.0)),
354 _ => None,
355 }
356 }
357
358 pub fn item_max_value_i64(&self) -> Option<i64> {
360 match self {
361 Self::LIST_INT(p) => p
362 .item
363 .as_ref()
364 .and_then(|i| i.max_value.as_ref().map(|v| v.0)),
365 _ => None,
366 }
367 }
368
369 pub fn item_allowed_values_i64(&self) -> Option<Vec<i64>> {
371 match self {
372 Self::LIST_INT(p) => p.item.as_ref().and_then(|i| {
373 i.allowed_values
374 .as_ref()
375 .map(|v| v.iter().map(|x| x.0).collect())
376 }),
377 _ => None,
378 }
379 }
380
381 pub fn item_min_value_f64(&self) -> Option<f64> {
383 match self {
384 Self::LIST_FLOAT(p) => p
385 .item
386 .as_ref()
387 .and_then(|i| i.min_value.as_ref().map(|v| v.0)),
388 _ => None,
389 }
390 }
391
392 pub fn item_max_value_f64(&self) -> Option<f64> {
394 match self {
395 Self::LIST_FLOAT(p) => p
396 .item
397 .as_ref()
398 .and_then(|i| i.max_value.as_ref().map(|v| v.0)),
399 _ => None,
400 }
401 }
402
403 pub fn item_allowed_values_f64(&self) -> Option<Vec<f64>> {
405 match self {
406 Self::LIST_FLOAT(p) => p.item.as_ref().and_then(|i| {
407 i.allowed_values
408 .as_ref()
409 .map(|v| v.iter().map(|x| x.0).collect())
410 }),
411 _ => None,
412 }
413 }
414
415 pub fn item_min_length(&self) -> Option<usize> {
417 match self {
418 Self::LIST_STRING(p) => p.item.as_ref().and_then(|i| i.min_length),
419 Self::LIST_PATH(p) => p.item.as_ref().and_then(|i| i.min_length),
420 Self::LIST_LIST_INT(p) => p.item.as_ref().and_then(|i| i.min_length),
421 _ => None,
422 }
423 }
424
425 pub fn item_max_length(&self) -> Option<usize> {
427 match self {
428 Self::LIST_STRING(p) => p.item.as_ref().and_then(|i| i.max_length),
429 Self::LIST_PATH(p) => p.item.as_ref().and_then(|i| i.max_length),
430 Self::LIST_LIST_INT(p) => p.item.as_ref().and_then(|i| i.max_length),
431 _ => None,
432 }
433 }
434
435 pub fn item_allowed_values_strings(&self) -> Option<Vec<String>> {
437 match self {
438 Self::LIST_STRING(p) => p.item.as_ref().and_then(|i| i.allowed_values.clone()),
439 Self::LIST_PATH(p) => p.item.as_ref().and_then(|i| i.allowed_values.clone()),
440 _ => None,
441 }
442 }
443
444 pub fn item_item_min_value_i64(&self) -> Option<i64> {
446 match self {
447 Self::LIST_LIST_INT(p) => p
448 .item
449 .as_ref()
450 .and_then(|i| i.item.as_ref())
451 .and_then(|ii| ii.min_value.as_ref().map(|v| v.0)),
452 _ => None,
453 }
454 }
455
456 pub fn item_item_max_value_i64(&self) -> Option<i64> {
458 match self {
459 Self::LIST_LIST_INT(p) => p
460 .item
461 .as_ref()
462 .and_then(|i| i.item.as_ref())
463 .and_then(|ii| ii.max_value.as_ref().map(|v| v.0)),
464 _ => None,
465 }
466 }
467
468 pub fn item_item_allowed_values_i64(&self) -> Option<Vec<i64>> {
470 match self {
471 Self::LIST_LIST_INT(p) => {
472 p.item
473 .as_ref()
474 .and_then(|i| i.item.as_ref())
475 .and_then(|ii| {
476 ii.allowed_values
477 .as_ref()
478 .map(|v| v.iter().map(|x| x.0).collect())
479 })
480 }
481 _ => None,
482 }
483 }
484
485 pub fn check_constraints(&self, value: &openjd_expr::ExprValue) -> Result<(), String> {
486 let s;
488 let str_val = match value {
489 openjd_expr::ExprValue::String(v) => v.as_str(),
490 openjd_expr::ExprValue::Int(v) => {
491 s = v.to_string();
492 &s
493 }
494 openjd_expr::ExprValue::Float(v) => {
495 s = v.to_string();
496 &s
497 }
498 openjd_expr::ExprValue::Bool(v) => {
499 s = v.to_string();
500 &s
501 }
502 openjd_expr::ExprValue::Path { value: v, .. } => v.as_str(),
503 _ => "", };
505 match self {
506 Self::STRING(p) => p.check_constraints(str_val),
507 Self::INT(p) => p.check_constraints(str_val),
508 Self::FLOAT(p) => p.check_constraints(str_val),
509 Self::PATH(p) => p.check_constraints(str_val),
510 Self::BOOL(p) => p.check_value_constraints(value),
511 Self::RANGE_EXPR(p) => p.check_value_constraints(value),
512 Self::LIST_STRING(p) => p.check_value_constraints(value),
513 Self::LIST_PATH(p) => p.check_value_constraints(value),
514 Self::LIST_INT(p) => p.check_value_constraints(value),
515 Self::LIST_FLOAT(p) => p.check_value_constraints(value),
516 Self::LIST_BOOL(p) => p.check_value_constraints(value),
517 Self::LIST_LIST_INT(p) => p.check_value_constraints(value),
518 }
519 }
520
521 pub fn validate_definition(
522 &self,
523 limits: &super::validate_v2023_09::EffectiveLimits,
524 ) -> Result<(), Vec<String>> {
525 match self {
526 Self::STRING(p) => p.validate_definition(limits),
527 Self::INT(p) => p.validate_definition(),
528 Self::FLOAT(p) => p.validate_definition(),
529 Self::PATH(p) => p.validate_definition(limits),
530 Self::BOOL(p) => p.validate_definition(),
531 Self::RANGE_EXPR(p) => p.validate_definition(),
532 Self::LIST_STRING(p) => p.validate_definition(),
533 Self::LIST_PATH(p) => p.validate_definition(),
534 Self::LIST_INT(p) => p.validate_definition(),
535 Self::LIST_FLOAT(p) => p.validate_definition(),
536 Self::LIST_BOOL(p) => p.validate_definition(),
537 Self::LIST_LIST_INT(p) => p.validate_definition(),
538 }
539 }
540}
541
542#[derive(Debug, Clone, Deserialize)]
544#[serde(rename_all = "camelCase", deny_unknown_fields)]
545pub struct StringUserInterface {
546 pub control: Option<String>,
547 pub label: Option<String>,
548 pub group_label: Option<String>,
549}
550
551#[derive(Debug, Clone, Deserialize)]
553#[serde(rename_all = "camelCase", deny_unknown_fields)]
554pub struct IntUserInterface {
555 pub control: Option<String>,
556 pub label: Option<String>,
557 pub group_label: Option<String>,
558 pub single_step_delta: Option<FlexInt>,
559}
560
561#[derive(Debug, Clone, Deserialize)]
563#[serde(rename_all = "camelCase", deny_unknown_fields)]
564pub struct FloatUserInterface {
565 pub control: Option<String>,
566 pub label: Option<String>,
567 pub group_label: Option<String>,
568 pub decimals: Option<FlexInt>,
569 pub single_step_delta: Option<FlexFloat>,
570}
571
572#[derive(Debug, Clone, Deserialize)]
574#[serde(rename_all = "camelCase", deny_unknown_fields)]
575pub struct PathUserInterface {
576 pub control: Option<String>,
577 pub label: Option<String>,
578 pub group_label: Option<String>,
579 pub file_filters: Option<Vec<FileFilter>>,
580 pub file_filter_default: Option<FileFilter>,
581}
582
583#[derive(Debug, Clone, Deserialize)]
585#[serde(rename_all = "camelCase", deny_unknown_fields)]
586pub struct FileFilter {
587 pub label: String,
588 pub patterns: Vec<String>,
589}
590
591pub(crate) fn validate_ui_label(
592 label: &Option<String>,
593 field_name: &str,
594 param_name: &str,
595) -> Vec<String> {
596 let mut errors = Vec::new();
597 if let Some(l) = label {
598 if l.is_empty() {
599 errors.push(format!(
600 "Parameter '{param_name}': {field_name} must not be empty."
601 ));
602 }
603 if l.chars().count() > 64 {
604 errors.push(format!(
605 "Parameter '{param_name}': {field_name} exceeds 64 characters."
606 ));
607 }
608 if l.chars().any(|c| c.is_control()) {
609 errors.push(format!(
610 "Parameter '{param_name}': {field_name} contains control characters."
611 ));
612 }
613 }
614 errors
615}
616
617#[derive(Debug, Clone, Deserialize)]
619#[serde(rename_all = "camelCase", deny_unknown_fields)]
620pub struct JobStringParameterDefinition {
621 pub name: Identifier,
622 pub description: Option<Description>,
623 pub default: Option<String>,
624 pub allowed_values: Option<Vec<String>>,
625 pub min_length: Option<usize>,
626 pub max_length: Option<usize>,
627 pub user_interface: Option<StringUserInterface>,
628}
629
630impl JobStringParameterDefinition {
631 pub fn check_constraints(&self, value: &str) -> Result<(), String> {
632 let char_len = value.chars().count();
633 if let Some(min) = self.min_length {
634 if char_len < min {
635 return Err(format!(
636 "Parameter '{}': value length {} is less than minimum {min}",
637 self.name, char_len
638 ));
639 }
640 }
641 if let Some(max) = self.max_length {
642 if char_len > max {
643 return Err(format!(
644 "Parameter '{}': value length {} exceeds maximum {max}",
645 self.name, char_len
646 ));
647 }
648 }
649 if let Some(allowed) = self.allowed_values.as_ref() {
650 if !allowed.iter().any(|a| a == value) {
651 return Err(format!(
652 "Parameter '{}': value '{value}' is not in allowed values",
653 self.name
654 ));
655 }
656 }
657 Ok(())
658 }
659
660 pub fn validate_definition(
661 &self,
662 limits: &super::validate_v2023_09::EffectiveLimits,
663 ) -> Result<(), Vec<String>> {
664 let mut errors = Vec::new();
665
666 if let Some(allowed) = self.allowed_values.as_ref() {
668 if allowed.is_empty() {
669 errors.push(format!(
670 "Parameter '{}': allowedValues must not be empty.",
671 self.name
672 ));
673 }
674 for (i, v) in allowed.iter().enumerate() {
675 let vlen = v.chars().count();
676 if vlen > limits.max_job_param_string_len {
677 errors.push(format!(
678 "Parameter '{}': allowedValues[{i}] exceeds {} characters.",
679 self.name, limits.max_job_param_string_len
680 ));
681 }
682 if let Some(min) = self.min_length {
683 if vlen < min {
684 errors.push(format!("Parameter '{}': allowedValues[{i}] length {vlen} is less than minLength {min}.", self.name));
685 }
686 }
687 if let Some(max) = self.max_length {
688 if vlen > max {
689 errors.push(format!(
690 "Parameter '{}': allowedValues[{i}] length {vlen} exceeds maxLength {max}.",
691 self.name,
692 ));
693 }
694 }
695 }
696 }
697
698 if let (Some(min), Some(max)) = (self.min_length, self.max_length) {
700 if min > max {
701 errors.push(format!(
702 "Parameter '{}': minLength ({min}) > maxLength ({max}).",
703 self.name
704 ));
705 }
706 }
707 if let Some(max) = self.max_length {
708 if max == 0 {
709 errors.push(format!("Parameter '{}': maxLength must be > 0.", self.name));
710 }
711 }
712
713 if let Some(default) = &self.default {
715 let dlen = default.chars().count();
716 if dlen > limits.max_job_param_string_len {
717 errors.push(format!(
718 "Parameter '{}': default exceeds {} characters.",
719 self.name, limits.max_job_param_string_len
720 ));
721 }
722 if let Some(min) = self.min_length {
723 if dlen < min {
724 errors.push(format!(
725 "Parameter '{}': default length {dlen} is less than minLength {min}.",
726 self.name,
727 ));
728 }
729 }
730 if let Some(max) = self.max_length {
731 if dlen > max {
732 errors.push(format!(
733 "Parameter '{}': default length {dlen} exceeds maxLength {max}.",
734 self.name,
735 ));
736 }
737 }
738 if let Some(allowed) = self.allowed_values.as_ref() {
739 if !allowed.contains(default) {
740 errors.push(format!(
741 "Parameter '{}': default '{}' is not in allowedValues.",
742 self.name, default
743 ));
744 }
745 }
746 }
747
748 if let Some(ui) = &self.user_interface {
750 errors.extend(validate_ui_label(&ui.label, "label", self.name.as_str()));
751 errors.extend(validate_ui_label(
752 &ui.group_label,
753 "groupLabel",
754 self.name.as_str(),
755 ));
756
757 if let Some(control) = &ui.control {
758 match control.as_str() {
759 "LINE_EDIT" | "MULTILINE_EDIT" => {
760 if self.allowed_values.is_some() {
761 errors.push(format!("Parameter '{}': control '{control}' cannot be used with allowedValues.", self.name));
762 }
763 }
764 "DROPDOWN_LIST" => {
765 if self.allowed_values.is_none() {
766 errors.push(format!(
767 "Parameter '{}': DROPDOWN_LIST requires allowedValues.",
768 self.name
769 ));
770 }
771 }
772 "CHECK_BOX" => {
773 if let Some(allowed) = self.allowed_values.as_ref() {
774 if allowed.len() != 2 {
775 errors.push(format!(
776 "Parameter '{}': CHECK_BOX requires exactly 2 allowedValues.",
777 self.name
778 ));
779 } else {
780 let pair: Vec<String> =
781 allowed.iter().map(|s| s.to_lowercase()).collect();
782 let valid_pairs = [
783 vec!["true", "false"],
784 vec!["false", "true"],
785 vec!["yes", "no"],
786 vec!["no", "yes"],
787 vec!["on", "off"],
788 vec!["off", "on"],
789 vec!["1", "0"],
790 vec!["0", "1"],
791 ];
792 if !valid_pairs.iter().any(|vp| vp == &pair) {
793 errors.push(format!("Parameter '{}': CHECK_BOX allowedValues must be a valid boolean pair.", self.name));
794 }
795 }
796 } else {
797 errors.push(format!(
798 "Parameter '{}': CHECK_BOX requires allowedValues.",
799 self.name
800 ));
801 }
802 }
803 "HIDDEN" => {}
804 _ => {
805 errors.push(format!(
806 "Parameter '{}': unknown control '{control}'.",
807 self.name
808 ));
809 }
810 }
811 }
812 }
813
814 if errors.is_empty() {
815 Ok(())
816 } else {
817 Err(errors)
818 }
819 }
820}
821
822#[derive(Debug, Clone)]
825pub struct FlexInt(pub i64);
826
827impl<'de> Deserialize<'de> for FlexInt {
828 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
829 let val = serde_json::Value::deserialize(deserializer)?;
830 match &val {
831 serde_json::Value::Number(n) => {
832 if let Some(i) = n.as_i64() {
833 Ok(FlexInt(i))
834 } else if let Some(f) = n.as_f64() {
835 if f.fract() == 0.0 {
836 Ok(FlexInt(f as i64))
837 } else {
838 Err(serde::de::Error::custom(format!(
839 "Expected integer, got float: {f}"
840 )))
841 }
842 } else {
843 Err(serde::de::Error::custom("Invalid number"))
844 }
845 }
846 serde_json::Value::String(s) => s
847 .trim()
848 .parse::<i64>()
849 .map(FlexInt)
850 .map_err(|_| serde::de::Error::custom(format!("Cannot parse '{s}' as integer"))),
851 serde_json::Value::Bool(_) => {
852 Err(serde::de::Error::custom("Expected integer, got boolean"))
853 }
854 serde_json::Value::Null => Err(serde::de::Error::custom("Expected integer, got null")),
855 _ => Err(serde::de::Error::custom("Expected integer or string")),
856 }
857 }
858}
859
860impl std::fmt::Display for FlexInt {
861 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
862 write!(f, "{}", self.0)
863 }
864}
865
866#[derive(Debug, Clone)]
870pub struct FlexFloat(pub f64, pub Option<String>);
871
872pub(crate) fn reject_nan_inf(f: f64) -> Result<(), String> {
874 if f.is_nan() {
875 Err("NaN is not a valid float value".to_string())
876 } else if f.is_infinite() {
877 Err("Infinity is not a valid float value".to_string())
878 } else {
879 Ok(())
880 }
881}
882
883impl<'de> Deserialize<'de> for FlexFloat {
884 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
885 let val = serde_json::Value::deserialize(deserializer)?;
886 match &val {
887 serde_json::Value::Number(n) => {
888 if let Some(f) = n.as_f64() {
889 reject_nan_inf(f).map_err(serde::de::Error::custom)?;
890 Ok(FlexFloat(f, None))
891 } else {
892 Err(serde::de::Error::custom("Invalid number"))
893 }
894 }
895 serde_json::Value::String(s) => {
896 let f = s.trim().parse::<f64>().map_err(|_| {
897 serde::de::Error::custom(format!("Cannot parse '{s}' as float"))
898 })?;
899 reject_nan_inf(f).map_err(serde::de::Error::custom)?;
900 Ok(FlexFloat(f, Some(s.trim().to_string())))
901 }
902 serde_json::Value::Bool(_) => {
903 Err(serde::de::Error::custom("Expected number, got boolean"))
904 }
905 serde_json::Value::Null => Err(serde::de::Error::custom("Expected number, got null")),
906 _ => Err(serde::de::Error::custom("Expected number or string")),
907 }
908 }
909}
910
911impl std::fmt::Display for FlexFloat {
912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913 if self.0.fract() == 0.0 && self.0 >= i64::MIN as f64 && self.0 < i64::MAX as f64 {
917 write!(f, "{}", self.0 as i64)
918 } else {
919 write!(f, "{}", self.0)
920 }
921 }
922}
923
924#[derive(Debug, Clone, Deserialize)]
926#[serde(rename_all = "camelCase", deny_unknown_fields)]
927pub struct JobIntParameterDefinition {
928 pub name: Identifier,
929 pub description: Option<Description>,
930 pub default: Option<FlexInt>,
931
932 #[serde(default)]
933 pub allowed_values: NullableVec<FlexInt>,
934 pub min_value: Option<FlexInt>,
935 pub max_value: Option<FlexInt>,
936 pub user_interface: Option<IntUserInterface>,
937}
938
939impl JobIntParameterDefinition {
940 pub fn check_constraints(&self, value: &str) -> Result<(), String> {
941 let parsed: i64 = value.parse().map_err(|_| {
942 format!(
943 "Parameter '{}': value '{value}' is not a valid integer",
944 self.name
945 )
946 })?;
947 if let Some(min) = &self.min_value {
948 if parsed < min.0 {
949 return Err(format!(
950 "Parameter '{}': value {parsed} is less than minimum {}",
951 self.name, min.0
952 ));
953 }
954 }
955 if let Some(max) = &self.max_value {
956 if parsed > max.0 {
957 return Err(format!(
958 "Parameter '{}': value {parsed} exceeds maximum {}",
959 self.name, max.0
960 ));
961 }
962 }
963 if let Some(allowed) = self.allowed_values.as_ref() {
964 if !allowed.iter().any(|a| a.0 == parsed) {
965 return Err(format!(
966 "Parameter '{}': value {parsed} is not in allowed values",
967 self.name
968 ));
969 }
970 }
971 Ok(())
972 }
973
974 pub fn validate_definition(&self) -> Result<(), Vec<String>> {
975 let mut errors = Vec::new();
976 if let Some(allowed) = self.allowed_values.as_ref() {
977 if allowed.is_empty() {
978 errors.push(format!(
979 "Parameter '{}': allowedValues must not be empty.",
980 self.name
981 ));
982 }
983 }
984 if let (Some(min), Some(max)) = (&self.min_value, &self.max_value) {
985 if min.0 > max.0 {
986 errors.push(format!(
987 "Parameter '{}': minValue ({}) > maxValue ({}).",
988 self.name, min.0, max.0
989 ));
990 }
991 }
992 if let Some(default) = &self.default {
993 if let Err(e) = self.check_constraints(&default.0.to_string()) {
994 errors.push(e);
995 }
996 }
997 if let Some(ui) = &self.user_interface {
998 errors.extend(validate_ui_label(&ui.label, "label", self.name.as_str()));
999 errors.extend(validate_ui_label(
1000 &ui.group_label,
1001 "groupLabel",
1002 self.name.as_str(),
1003 ));
1004 let control = ui
1005 .control
1006 .as_deref()
1007 .unwrap_or(if self.allowed_values.is_some() {
1008 "DROPDOWN_LIST"
1009 } else {
1010 "SPIN_BOX"
1011 });
1012 match control {
1013 "SPIN_BOX" => {
1014 if self.allowed_values.is_some() {
1015 errors.push(format!(
1016 "Parameter '{}': SPIN_BOX cannot be used with allowedValues.",
1017 self.name
1018 ));
1019 }
1020 }
1021 "DROPDOWN_LIST" => {
1022 if self.allowed_values.is_none() {
1023 errors.push(format!(
1024 "Parameter '{}': DROPDOWN_LIST requires allowedValues.",
1025 self.name
1026 ));
1027 }
1028 if ui.single_step_delta.is_some() {
1029 errors.push(format!(
1030 "Parameter '{}': singleStepDelta is only valid with SPIN_BOX.",
1031 self.name
1032 ));
1033 }
1034 }
1035 "HIDDEN" => {
1036 if ui.single_step_delta.is_some() {
1037 errors.push(format!(
1038 "Parameter '{}': singleStepDelta is not valid with HIDDEN.",
1039 self.name
1040 ));
1041 }
1042 }
1043 _ => errors.push(format!(
1044 "Parameter '{}': unknown control '{control}'.",
1045 self.name
1046 )),
1047 }
1048 if let Some(delta) = &ui.single_step_delta {
1050 if delta.0 <= 0 {
1051 errors.push(format!(
1052 "Parameter '{}': singleStepDelta must be positive.",
1053 self.name
1054 ));
1055 }
1056 }
1057 }
1058 if errors.is_empty() {
1059 Ok(())
1060 } else {
1061 Err(errors)
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Deserialize)]
1068#[serde(rename_all = "camelCase", deny_unknown_fields)]
1069pub struct JobFloatParameterDefinition {
1070 pub name: Identifier,
1071 pub description: Option<Description>,
1072 pub default: Option<FlexFloat>,
1073
1074 #[serde(default)]
1075 pub allowed_values: NullableVec<FlexFloat>,
1076 pub min_value: Option<FlexFloat>,
1077 pub max_value: Option<FlexFloat>,
1078 pub user_interface: Option<FloatUserInterface>,
1079}
1080
1081impl JobFloatParameterDefinition {
1082 pub fn check_constraints(&self, value: &str) -> Result<(), String> {
1083 let parsed: f64 = value.parse().map_err(|_| {
1084 format!(
1085 "Parameter '{}': value '{value}' is not a valid float",
1086 self.name
1087 )
1088 })?;
1089 reject_nan_inf(parsed).map_err(|e| format!("Parameter '{}': {e}", self.name))?;
1090 if let Some(min) = &self.min_value {
1091 if parsed < min.0 {
1092 return Err(format!(
1093 "Parameter '{}': value {parsed} is less than minimum {}",
1094 self.name, min.0
1095 ));
1096 }
1097 }
1098 if let Some(max) = &self.max_value {
1099 if parsed > max.0 {
1100 return Err(format!(
1101 "Parameter '{}': value {parsed} exceeds maximum {}",
1102 self.name, max.0
1103 ));
1104 }
1105 }
1106 if let Some(allowed) = self.allowed_values.as_ref() {
1107 if !allowed.iter().any(|a| a.0 == parsed) {
1108 return Err(format!(
1109 "Parameter '{}': value {parsed} is not in allowed values",
1110 self.name
1111 ));
1112 }
1113 }
1114 Ok(())
1115 }
1116
1117 pub fn validate_definition(&self) -> Result<(), Vec<String>> {
1118 let mut errors = Vec::new();
1119 if let Some(allowed) = self.allowed_values.as_ref() {
1120 if allowed.is_empty() {
1121 errors.push(format!(
1122 "Parameter '{}': allowedValues must not be empty.",
1123 self.name
1124 ));
1125 }
1126 for (i, a) in allowed.iter().enumerate() {
1128 if let Some(min) = &self.min_value {
1129 if a.0 < min.0 {
1130 errors.push(format!(
1131 "Parameter '{}': allowedValues[{i}] ({}) is less than minValue ({}).",
1132 self.name, a.0, min.0
1133 ));
1134 }
1135 }
1136 if let Some(max) = &self.max_value {
1137 if a.0 > max.0 {
1138 errors.push(format!(
1139 "Parameter '{}': allowedValues[{i}] ({}) exceeds maxValue ({}).",
1140 self.name, a.0, max.0
1141 ));
1142 }
1143 }
1144 }
1145 }
1146 if let (Some(min), Some(max)) = (&self.min_value, &self.max_value) {
1147 if min.0 > max.0 {
1148 errors.push(format!(
1149 "Parameter '{}': minValue ({}) > maxValue ({}).",
1150 self.name, min.0, max.0
1151 ));
1152 }
1153 }
1154 if let Some(default) = &self.default {
1156 if let Err(e) = self.check_constraints(&default.0.to_string()) {
1157 errors.push(e);
1158 }
1159 }
1160 if let Some(ui) = &self.user_interface {
1161 errors.extend(validate_ui_label(&ui.label, "label", self.name.as_str()));
1162 errors.extend(validate_ui_label(
1163 &ui.group_label,
1164 "groupLabel",
1165 self.name.as_str(),
1166 ));
1167 let control = ui
1168 .control
1169 .as_deref()
1170 .unwrap_or(if self.allowed_values.is_some() {
1171 "DROPDOWN_LIST"
1172 } else {
1173 "SPIN_BOX"
1174 });
1175 match control {
1176 "SPIN_BOX" => {
1177 if self.allowed_values.is_some() {
1178 errors.push(format!(
1179 "Parameter '{}': SPIN_BOX cannot be used with allowedValues.",
1180 self.name
1181 ));
1182 }
1183 }
1184 "DROPDOWN_LIST" => {
1185 if self.allowed_values.is_none() {
1186 errors.push(format!(
1187 "Parameter '{}': DROPDOWN_LIST requires allowedValues.",
1188 self.name
1189 ));
1190 }
1191 if ui.decimals.is_some() {
1192 errors.push(format!(
1193 "Parameter '{}': decimals is only valid with SPIN_BOX.",
1194 self.name
1195 ));
1196 }
1197 if ui.single_step_delta.is_some() {
1198 errors.push(format!(
1199 "Parameter '{}': singleStepDelta is only valid with SPIN_BOX.",
1200 self.name
1201 ));
1202 }
1203 }
1204 "HIDDEN" => {
1205 if ui.decimals.is_some() {
1206 errors.push(format!(
1207 "Parameter '{}': decimals is not valid with HIDDEN.",
1208 self.name
1209 ));
1210 }
1211 if ui.single_step_delta.is_some() {
1212 errors.push(format!(
1213 "Parameter '{}': singleStepDelta is not valid with HIDDEN.",
1214 self.name
1215 ));
1216 }
1217 }
1218 _ => errors.push(format!(
1219 "Parameter '{}': unknown control '{control}'.",
1220 self.name
1221 )),
1222 }
1223 if let Some(delta) = &ui.single_step_delta {
1224 if delta.0 <= 0.0 {
1225 errors.push(format!(
1226 "Parameter '{}': singleStepDelta must be positive.",
1227 self.name
1228 ));
1229 }
1230 }
1231 }
1232 if errors.is_empty() {
1233 Ok(())
1234 } else {
1235 Err(errors)
1236 }
1237 }
1238}
1239
1240#[derive(Debug, Clone, Deserialize)]
1242#[serde(rename_all = "camelCase", deny_unknown_fields)]
1243pub struct JobPathParameterDefinition {
1244 pub name: Identifier,
1245 pub description: Option<Description>,
1246 pub default: Option<String>,
1247 pub allowed_values: Option<Vec<String>>,
1248 pub min_length: Option<usize>,
1249 pub max_length: Option<usize>,
1250 pub object_type: Option<ObjectType>,
1251 pub data_flow: Option<DataFlow>,
1252 pub user_interface: Option<PathUserInterface>,
1253}
1254
1255impl JobPathParameterDefinition {
1256 pub fn check_constraints(&self, value: &str) -> Result<(), String> {
1257 let char_len = value.chars().count();
1258 if let Some(min) = self.min_length {
1259 if char_len < min {
1260 return Err(format!(
1261 "Parameter '{}': value length {} is less than minimum {min}",
1262 self.name, char_len
1263 ));
1264 }
1265 }
1266 if let Some(max) = self.max_length {
1267 if char_len > max {
1268 return Err(format!(
1269 "Parameter '{}': value length {} exceeds maximum {max}",
1270 self.name, char_len
1271 ));
1272 }
1273 }
1274 if let Some(allowed) = self.allowed_values.as_ref() {
1275 if !allowed.iter().any(|a| a == value) {
1276 return Err(format!(
1277 "Parameter '{}': value '{value}' is not in allowed values",
1278 self.name
1279 ));
1280 }
1281 }
1282 Ok(())
1283 }
1284
1285 pub fn validate_definition(
1286 &self,
1287 limits: &super::validate_v2023_09::EffectiveLimits,
1288 ) -> Result<(), Vec<String>> {
1289 let mut errors = Vec::new();
1290 let object_type = self.object_type.unwrap_or(ObjectType::Directory);
1291 if let Some(allowed) = self.allowed_values.as_ref() {
1292 if allowed.is_empty() {
1293 errors.push(format!(
1294 "Parameter '{}': allowedValues must not be empty.",
1295 self.name
1296 ));
1297 }
1298 for (i, v) in allowed.iter().enumerate() {
1299 let vlen = v.chars().count();
1300 if vlen > limits.max_job_param_string_len {
1301 errors.push(format!(
1302 "Parameter '{}': allowedValues[{i}] exceeds {} characters.",
1303 self.name, limits.max_job_param_string_len
1304 ));
1305 }
1306 if let Some(min) = self.min_length {
1307 if vlen < min {
1308 errors.push(format!(
1309 "Parameter '{}': allowedValues[{i}] length {vlen} < minLength {min}.",
1310 self.name,
1311 ));
1312 }
1313 }
1314 if let Some(max) = self.max_length {
1315 if vlen > max {
1316 errors.push(format!(
1317 "Parameter '{}': allowedValues[{i}] length {vlen} > maxLength {max}.",
1318 self.name,
1319 ));
1320 }
1321 }
1322 }
1323 }
1324 if let (Some(min), Some(max)) = (self.min_length, self.max_length) {
1325 if min > max {
1326 errors.push(format!(
1327 "Parameter '{}': minLength ({min}) > maxLength ({max}).",
1328 self.name
1329 ));
1330 }
1331 }
1332 if let Some(default) = &self.default {
1333 let dlen = default.chars().count();
1334 if dlen > limits.max_job_param_string_len {
1335 errors.push(format!(
1336 "Parameter '{}': default exceeds {} characters.",
1337 self.name, limits.max_job_param_string_len
1338 ));
1339 }
1340 if let Some(min) = self.min_length {
1341 if dlen < min {
1342 errors.push(format!(
1343 "Parameter '{}': default length {dlen} < minLength {min}.",
1344 self.name,
1345 ));
1346 }
1347 }
1348 if let Some(max) = self.max_length {
1349 if dlen > max {
1350 errors.push(format!(
1351 "Parameter '{}': default length {dlen} > maxLength {max}.",
1352 self.name,
1353 ));
1354 }
1355 }
1356 if let Some(allowed) = self.allowed_values.as_ref() {
1357 if !allowed.contains(default) {
1358 errors.push(format!(
1359 "Parameter '{}': default '{}' is not in allowedValues.",
1360 self.name, default
1361 ));
1362 }
1363 }
1364 }
1365 if let Some(ui) = &self.user_interface {
1366 errors.extend(validate_ui_label(&ui.label, "label", self.name.as_str()));
1367 errors.extend(validate_ui_label(
1368 &ui.group_label,
1369 "groupLabel",
1370 self.name.as_str(),
1371 ));
1372 let control = ui
1373 .control
1374 .as_deref()
1375 .unwrap_or(if self.allowed_values.is_some() {
1376 "DROPDOWN_LIST"
1377 } else if object_type == ObjectType::File {
1378 if self.data_flow == Some(DataFlow::Out) {
1379 "CHOOSE_OUTPUT_FILE"
1380 } else {
1381 "CHOOSE_INPUT_FILE"
1382 }
1383 } else {
1384 "CHOOSE_DIRECTORY"
1385 });
1386 match control {
1387 "CHOOSE_INPUT_FILE" | "CHOOSE_OUTPUT_FILE" => {
1388 if self.allowed_values.is_some() {
1389 errors.push(format!(
1390 "Parameter '{}': {control} cannot be used with allowedValues.",
1391 self.name
1392 ));
1393 }
1394 if object_type == ObjectType::Directory {
1395 errors.push(format!(
1396 "Parameter '{}': {control} requires objectType FILE.",
1397 self.name
1398 ));
1399 }
1400 }
1401 "CHOOSE_DIRECTORY" => {
1402 if self.allowed_values.is_some() {
1403 errors.push(format!(
1404 "Parameter '{}': CHOOSE_DIRECTORY cannot be used with allowedValues.",
1405 self.name
1406 ));
1407 }
1408 if object_type == ObjectType::File {
1409 errors.push(format!(
1410 "Parameter '{}': CHOOSE_DIRECTORY requires objectType DIRECTORY.",
1411 self.name
1412 ));
1413 }
1414 }
1415 "DROPDOWN_LIST" => {
1416 if self.allowed_values.is_none() {
1417 errors.push(format!(
1418 "Parameter '{}': DROPDOWN_LIST requires allowedValues.",
1419 self.name
1420 ));
1421 }
1422 }
1423 "HIDDEN" => {}
1424 _ => errors.push(format!(
1425 "Parameter '{}': unknown control '{control}'.",
1426 self.name
1427 )),
1428 }
1429 let is_file_chooser = control == "CHOOSE_INPUT_FILE" || control == "CHOOSE_OUTPUT_FILE";
1431 if let Some(filters) = &ui.file_filters {
1432 if !is_file_chooser {
1433 errors.push(format!(
1434 "Parameter '{}': fileFilters only valid with file chooser controls.",
1435 self.name
1436 ));
1437 }
1438 if filters.len() > 20 {
1439 errors.push(format!(
1440 "Parameter '{}': fileFilters exceeds 20 elements.",
1441 self.name
1442 ));
1443 }
1444 for filter in filters {
1445 if filter.patterns.is_empty() {
1446 errors.push(format!(
1447 "Parameter '{}': fileFilter patterns must not be empty.",
1448 self.name
1449 ));
1450 }
1451 for pattern in &filter.patterns {
1452 validate_file_filter_pattern(pattern, self.name.as_str(), &mut errors);
1453 }
1454 }
1455 }
1456 if let Some(filter) = &ui.file_filter_default {
1457 if !is_file_chooser {
1458 errors.push(format!(
1459 "Parameter '{}': fileFilterDefault only valid with file chooser controls.",
1460 self.name
1461 ));
1462 }
1463 for pattern in &filter.patterns {
1464 validate_file_filter_pattern(pattern, self.name.as_str(), &mut errors);
1465 }
1466 }
1467 }
1468 if errors.is_empty() {
1469 Ok(())
1470 } else {
1471 Err(errors)
1472 }
1473 }
1474}
1475
1476fn validate_file_filter_pattern(pattern: &str, param_name: &str, errors: &mut Vec<String>) {
1477 if pattern.is_empty() || pattern.len() > 20 {
1478 errors.push(format!(
1479 "Parameter '{param_name}': file filter pattern must be 1..=20 characters."
1480 ));
1481 return;
1482 }
1483 if pattern == "*" || pattern == "*.*" {
1484 return;
1485 }
1486 if !pattern.starts_with("*.") {
1487 errors.push(format!("Parameter '{param_name}': file filter pattern '{pattern}' must be '*', '*.*', or '*.ext'."));
1488 return;
1489 }
1490 let ext = &pattern[2..];
1491 if ext.is_empty() {
1492 errors.push(format!(
1493 "Parameter '{param_name}': file filter pattern '{pattern}' has empty extension."
1494 ));
1495 return;
1496 }
1497 let disallowed = [
1498 '\\', '/', '*', '?', '[', ']', '#', '%', '&', '{', '}', '<', '>', '$', '!', '\'', '"', ':',
1499 '@', '`', '|', '=',
1500 ];
1501 for ch in ext.chars() {
1502 if ch.is_control() || disallowed.contains(&ch) {
1503 errors.push(format!("Parameter '{param_name}': file filter pattern '{pattern}' contains disallowed character '{ch}'."));
1504 return;
1505 }
1506 }
1507}