Skip to main content

openjd_model/template/
expr_parameters.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! EXPR extension parameter types (§2.9-2.16).
6//!
7//! These types are only available when the EXPR extension is enabled.
8
9use super::constrained_strings::{Description, Identifier};
10use super::parameters::{validate_ui_label, FileFilter, FlexFloat, FlexInt};
11use crate::types::{DataFlow, ObjectType};
12use openjd_expr::ExprValue;
13use serde::Deserialize;
14
15/// User interface definition for BOOL parameters.
16#[derive(Debug, Clone, Deserialize)]
17#[serde(rename_all = "camelCase", deny_unknown_fields)]
18pub struct BoolUserInterface {
19    pub control: Option<String>,
20    pub label: Option<String>,
21    pub group_label: Option<String>,
22}
23
24/// User interface definition for RANGE_EXPR parameters.
25#[derive(Debug, Clone, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct RangeExprUserInterface {
28    pub control: Option<String>,
29    pub label: Option<String>,
30    pub group_label: Option<String>,
31}
32
33/// User interface definition for `LIST[STRING]` and `LIST[BOOL]` parameters.
34#[derive(Debug, Clone, Deserialize)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct ListSimpleUserInterface {
37    pub control: Option<String>,
38    pub label: Option<String>,
39    pub group_label: Option<String>,
40}
41
42/// User interface definition for `LIST[PATH]` parameters.
43#[derive(Debug, Clone, Deserialize)]
44#[serde(rename_all = "camelCase", deny_unknown_fields)]
45pub struct ListPathUserInterface {
46    pub control: Option<String>,
47    pub label: Option<String>,
48    pub group_label: Option<String>,
49    pub file_filters: Option<Vec<FileFilter>>,
50    pub file_filter_default: Option<FileFilter>,
51}
52
53/// User interface definition for `LIST[INT]` parameters.
54#[derive(Debug, Clone, Deserialize)]
55#[serde(rename_all = "camelCase", deny_unknown_fields)]
56pub struct ListIntUserInterface {
57    pub control: Option<String>,
58    pub label: Option<String>,
59    pub group_label: Option<String>,
60    pub single_step_delta: Option<FlexInt>,
61}
62
63/// User interface definition for `LIST[FLOAT]` parameters.
64#[derive(Debug, Clone, Deserialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct ListFloatUserInterface {
67    pub control: Option<String>,
68    pub label: Option<String>,
69    pub group_label: Option<String>,
70    pub decimals: Option<FlexInt>,
71    pub single_step_delta: Option<FlexFloat>,
72}
73
74/// User interface definition for `LIST[LIST[INT]]` parameters (HIDDEN only).
75#[derive(Debug, Clone, Deserialize)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct HiddenOnlyUserInterface {
78    pub control: Option<String>,
79    pub label: Option<String>,
80    pub group_label: Option<String>,
81}
82
83/// §2.9 JobBoolParameterDefinition
84#[derive(Debug, Clone, Deserialize)]
85#[serde(rename_all = "camelCase", deny_unknown_fields)]
86pub struct JobBoolParameterDefinition {
87    pub name: Identifier,
88    pub description: Option<Description>,
89    pub default: Option<BoolValue>,
90    pub user_interface: Option<BoolUserInterface>,
91}
92
93/// A bool value that accepts: true/false, 0/1, 0.0/1.0, "true"/"false"/"yes"/"no"/"on"/"off"/"1"/"0"
94#[derive(Debug, Clone)]
95pub struct BoolValue(pub bool);
96
97impl<'de> Deserialize<'de> for BoolValue {
98    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99        let val = serde_json::Value::deserialize(deserializer)?;
100        match &val {
101            serde_json::Value::Bool(b) => Ok(BoolValue(*b)),
102            serde_json::Value::Number(n) => {
103                if let Some(i) = n.as_i64() {
104                    match i {
105                        0 => Ok(BoolValue(false)),
106                        1 => Ok(BoolValue(true)),
107                        _ => Err(serde::de::Error::custom(format!("Invalid bool value: {i}"))),
108                    }
109                } else if let Some(f) = n.as_f64() {
110                    if f == 0.0 {
111                        Ok(BoolValue(false))
112                    } else if f == 1.0 {
113                        Ok(BoolValue(true))
114                    } else {
115                        Err(serde::de::Error::custom(format!("Invalid bool value: {f}")))
116                    }
117                } else {
118                    Err(serde::de::Error::custom("Invalid bool value"))
119                }
120            }
121            serde_json::Value::String(s) => match s.to_lowercase().as_str() {
122                "true" | "yes" | "on" | "1" => Ok(BoolValue(true)),
123                "false" | "no" | "off" | "0" => Ok(BoolValue(false)),
124                _ => Err(serde::de::Error::custom(format!(
125                    "Invalid bool value: '{s}'"
126                ))),
127            },
128            _ => Err(serde::de::Error::custom("Invalid bool value")),
129        }
130    }
131}
132
133impl JobBoolParameterDefinition {
134    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
135        match value {
136            ExprValue::Bool(_) => Ok(()),
137            ExprValue::Int(0) | ExprValue::Int(1) => Ok(()),
138            ExprValue::String(s) => match s.to_lowercase().as_str() {
139                "true" | "false" | "yes" | "no" | "on" | "off" | "1" | "0" => Ok(()),
140                _ => Err(format!(
141                    "Parameter '{}': value '{}' is not a valid bool",
142                    self.name, s
143                )),
144            },
145            _ => Err(format!(
146                "Parameter '{}': expected bool, got {}",
147                self.name,
148                value.type_name()
149            )),
150        }
151    }
152
153    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
154        let mut errors = Vec::new();
155        if let Some(ui) = &self.user_interface {
156            validate_ui(
157                self.name.as_str(),
158                &ui.label,
159                &ui.group_label,
160                &ui.control,
161                &["CHECK_BOX", "HIDDEN"],
162                &mut errors,
163            );
164        }
165        if errors.is_empty() {
166            Ok(())
167        } else {
168            Err(errors)
169        }
170    }
171}
172
173/// §2.10 JobRangeExprParameterDefinition
174#[derive(Debug, Clone, Deserialize)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176pub struct JobRangeExprParameterDefinition {
177    pub name: Identifier,
178    pub description: Option<Description>,
179    pub default: Option<String>,
180    pub min_length: Option<usize>,
181    pub max_length: Option<usize>,
182    pub user_interface: Option<RangeExprUserInterface>,
183}
184
185impl JobRangeExprParameterDefinition {
186    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
187        let s = match value {
188            ExprValue::RangeExpr(r) => r.to_string(),
189            ExprValue::String(s) => {
190                s.parse::<openjd_expr::RangeExpr>().map_err(|_| {
191                    format!(
192                        "Parameter '{}': value '{}' is not a valid range expression",
193                        self.name, s
194                    )
195                })?;
196                s.clone()
197            }
198            _ => {
199                return Err(format!(
200                    "Parameter '{}': expected range_expr, got {}",
201                    self.name,
202                    value.type_name()
203                ))
204            }
205        };
206        if let Some(min) = self.min_length {
207            let char_len = s.chars().count();
208            if char_len < min {
209                return Err(format!(
210                    "Parameter '{}': value length {} is less than minimum {min}",
211                    self.name, char_len
212                ));
213            }
214        }
215        if let Some(max) = self.max_length {
216            let char_len = s.chars().count();
217            if char_len > max {
218                return Err(format!(
219                    "Parameter '{}': value length {} exceeds maximum {max}",
220                    self.name, char_len
221                ));
222            }
223        }
224        Ok(())
225    }
226
227    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
228        let mut errors = Vec::new();
229        if let Some(default) = &self.default {
230            if default.parse::<openjd_expr::RangeExpr>().is_err() {
231                errors.push(format!(
232                    "Parameter '{}': default '{}' is not a valid range expression.",
233                    self.name, default
234                ));
235            }
236        }
237        if let Some(ui) = &self.user_interface {
238            validate_ui(
239                self.name.as_str(),
240                &ui.label,
241                &ui.group_label,
242                &ui.control,
243                &["LINE_EDIT", "HIDDEN"],
244                &mut errors,
245            );
246        }
247        if errors.is_empty() {
248            Ok(())
249        } else {
250            Err(errors)
251        }
252    }
253}
254
255/// §2.11 JobListStringParameterDefinition
256#[derive(Debug, Clone, Deserialize)]
257#[serde(rename_all = "camelCase", deny_unknown_fields)]
258pub struct JobListStringParameterDefinition {
259    pub name: Identifier,
260    pub description: Option<Description>,
261    pub default: Option<Vec<String>>,
262    pub min_length: Option<usize>,
263    pub max_length: Option<usize>,
264    pub item: Option<ListStringItemConstraints>,
265    pub user_interface: Option<ListSimpleUserInterface>,
266}
267
268#[derive(Debug, Clone, Deserialize)]
269#[serde(rename_all = "camelCase", deny_unknown_fields)]
270pub struct ListStringItemConstraints {
271    pub allowed_values: Option<Vec<String>>,
272    pub min_length: Option<usize>,
273    pub max_length: Option<usize>,
274}
275
276impl JobListStringParameterDefinition {
277    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
278        let items = match value {
279            ExprValue::ListString(v, _) | ExprValue::ListPath(v, _, _) => v,
280            _ => {
281                return Err(format!(
282                    "Parameter '{}': expected list[string], got {}",
283                    self.name,
284                    value.type_name()
285                ))
286            }
287        };
288        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
289        if let Some(item) = &self.item {
290            check_string_items(&self.name, items, item)?;
291        }
292        Ok(())
293    }
294
295    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
296        let mut errors = Vec::new();
297        validate_list_length(
298            &self.name,
299            &self.default,
300            self.min_length,
301            self.max_length,
302            &mut errors,
303        );
304        if let (Some(default), Some(item)) = (&self.default, &self.item) {
305            validate_string_item_defaults(&self.name, default, item, &mut errors);
306        }
307        if let Some(ui) = &self.user_interface {
308            validate_ui(
309                self.name.as_str(),
310                &ui.label,
311                &ui.group_label,
312                &ui.control,
313                &["LINE_EDIT_LIST", "HIDDEN"],
314                &mut errors,
315            );
316        }
317        if errors.is_empty() {
318            Ok(())
319        } else {
320            Err(errors)
321        }
322    }
323}
324
325/// §2.12 JobListPathParameterDefinition
326#[derive(Debug, Clone, Deserialize)]
327#[serde(rename_all = "camelCase", deny_unknown_fields)]
328pub struct JobListPathParameterDefinition {
329    pub name: Identifier,
330    pub description: Option<Description>,
331    pub object_type: Option<ObjectType>,
332    pub data_flow: Option<DataFlow>,
333    pub default: Option<Vec<String>>,
334    pub min_length: Option<usize>,
335    pub max_length: Option<usize>,
336    pub item: Option<ListStringItemConstraints>,
337    pub user_interface: Option<ListPathUserInterface>,
338}
339
340impl JobListPathParameterDefinition {
341    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
342        let items = match value {
343            ExprValue::ListString(v, _) | ExprValue::ListPath(v, _, _) => v,
344            _ => {
345                return Err(format!(
346                    "Parameter '{}': expected list[path], got {}",
347                    self.name,
348                    value.type_name()
349                ))
350            }
351        };
352        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
353        if let Some(item) = &self.item {
354            check_string_items(&self.name, items, item)?;
355        }
356        Ok(())
357    }
358
359    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
360        let mut errors = Vec::new();
361        validate_list_length(
362            &self.name,
363            &self.default,
364            self.min_length,
365            self.max_length,
366            &mut errors,
367        );
368        if let (Some(default), Some(item)) = (&self.default, &self.item) {
369            validate_string_item_defaults(&self.name, default, item, &mut errors);
370        }
371        if let Some(ui) = &self.user_interface {
372            validate_ui(
373                self.name.as_str(),
374                &ui.label,
375                &ui.group_label,
376                &ui.control,
377                &[
378                    "CHOOSE_INPUT_FILE_LIST",
379                    "CHOOSE_OUTPUT_FILE_LIST",
380                    "CHOOSE_DIRECTORY_LIST",
381                    "HIDDEN",
382                ],
383                &mut errors,
384            );
385        }
386        if errors.is_empty() {
387            Ok(())
388        } else {
389            Err(errors)
390        }
391    }
392}
393
394/// §2.13 JobListIntParameterDefinition
395#[derive(Debug, Clone, Deserialize)]
396#[serde(rename_all = "camelCase", deny_unknown_fields)]
397pub struct JobListIntParameterDefinition {
398    pub name: Identifier,
399    pub description: Option<Description>,
400    pub default: Option<Vec<FlexInt>>,
401    pub min_length: Option<usize>,
402    pub max_length: Option<usize>,
403    pub item: Option<ListIntItemConstraints>,
404    pub user_interface: Option<ListIntUserInterface>,
405}
406
407#[derive(Debug, Clone, Deserialize)]
408#[serde(rename_all = "camelCase", deny_unknown_fields)]
409pub struct ListIntItemConstraints {
410    pub allowed_values: Option<Vec<FlexInt>>,
411    pub min_value: Option<FlexInt>,
412    pub max_value: Option<FlexInt>,
413}
414
415impl JobListIntParameterDefinition {
416    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
417        let items = match value {
418            ExprValue::ListInt(v) => v,
419            _ => {
420                return Err(format!(
421                    "Parameter '{}': expected list[int], got {}",
422                    self.name,
423                    value.type_name()
424                ))
425            }
426        };
427        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
428        if let Some(item) = &self.item {
429            check_int_items(&self.name, items, item, "item")?;
430        }
431        Ok(())
432    }
433
434    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
435        let mut errors = Vec::new();
436        validate_list_length(
437            &self.name,
438            &self.default,
439            self.min_length,
440            self.max_length,
441            &mut errors,
442        );
443        if let (Some(default), Some(item)) = (&self.default, &self.item) {
444            validate_int_item_defaults(&self.name, default, item, "default", "item", &mut errors);
445        }
446        if let Some(ui) = &self.user_interface {
447            validate_ui(
448                self.name.as_str(),
449                &ui.label,
450                &ui.group_label,
451                &ui.control,
452                &["SPIN_BOX_LIST", "HIDDEN"],
453                &mut errors,
454            );
455        }
456        if errors.is_empty() {
457            Ok(())
458        } else {
459            Err(errors)
460        }
461    }
462}
463
464/// §2.14 JobListFloatParameterDefinition
465#[derive(Debug, Clone, Deserialize)]
466#[serde(rename_all = "camelCase", deny_unknown_fields)]
467pub struct JobListFloatParameterDefinition {
468    pub name: Identifier,
469    pub description: Option<Description>,
470    pub default: Option<Vec<super::parameters::FlexFloat>>,
471    pub min_length: Option<usize>,
472    pub max_length: Option<usize>,
473    pub item: Option<ListFloatItemConstraints>,
474    pub user_interface: Option<ListFloatUserInterface>,
475}
476
477#[derive(Debug, Clone, Deserialize)]
478#[serde(rename_all = "camelCase", deny_unknown_fields)]
479pub struct ListFloatItemConstraints {
480    pub allowed_values: Option<Vec<super::parameters::FlexFloat>>,
481    pub min_value: Option<super::parameters::FlexFloat>,
482    pub max_value: Option<super::parameters::FlexFloat>,
483}
484
485impl JobListFloatParameterDefinition {
486    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
487        let items = match value {
488            ExprValue::ListFloat(v) => v,
489            _ => {
490                return Err(format!(
491                    "Parameter '{}': expected list[float], got {}",
492                    self.name,
493                    value.type_name()
494                ))
495            }
496        };
497        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
498        if let Some(item) = &self.item {
499            for (i, v) in items.iter().enumerate() {
500                if let Some(min) = &item.min_value {
501                    if v.value() < min.0 {
502                        return Err(format!(
503                            "Parameter '{}': item[{i}] {} is less than minimum {}",
504                            self.name, v, min.0
505                        ));
506                    }
507                }
508                if let Some(max) = &item.max_value {
509                    if v.value() > max.0 {
510                        return Err(format!(
511                            "Parameter '{}': item[{i}] {} exceeds maximum {}",
512                            self.name, v, max.0
513                        ));
514                    }
515                }
516                if let Some(allowed) = &item.allowed_values {
517                    if !allowed.iter().any(|a| a.0 == v.value()) {
518                        return Err(format!(
519                            "Parameter '{}': item[{i}] {} is not in allowed values",
520                            self.name, v
521                        ));
522                    }
523                }
524            }
525        }
526        Ok(())
527    }
528
529    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
530        let mut errors = Vec::new();
531        validate_list_length(
532            &self.name,
533            &self.default,
534            self.min_length,
535            self.max_length,
536            &mut errors,
537        );
538        if let (Some(default), Some(item)) = (&self.default, &self.item) {
539            for (i, v) in default.iter().enumerate() {
540                if let Some(min) = &item.min_value {
541                    if v.0 < min.0 {
542                        errors.push(format!(
543                            "Parameter '{}': default[{i}] {} < item minValue {}.",
544                            self.name, v.0, min.0
545                        ));
546                    }
547                }
548                if let Some(max) = &item.max_value {
549                    if v.0 > max.0 {
550                        errors.push(format!(
551                            "Parameter '{}': default[{i}] {} > item maxValue {}.",
552                            self.name, v.0, max.0
553                        ));
554                    }
555                }
556            }
557        }
558        if let Some(ui) = &self.user_interface {
559            validate_ui(
560                self.name.as_str(),
561                &ui.label,
562                &ui.group_label,
563                &ui.control,
564                &["SPIN_BOX_LIST", "HIDDEN"],
565                &mut errors,
566            );
567        }
568        if errors.is_empty() {
569            Ok(())
570        } else {
571            Err(errors)
572        }
573    }
574}
575
576/// §2.15 JobListBoolParameterDefinition
577#[derive(Debug, Clone, Deserialize)]
578#[serde(rename_all = "camelCase", deny_unknown_fields)]
579pub struct JobListBoolParameterDefinition {
580    pub name: Identifier,
581    pub description: Option<Description>,
582    pub default: Option<Vec<BoolValue>>,
583    pub min_length: Option<usize>,
584    pub max_length: Option<usize>,
585    pub user_interface: Option<ListSimpleUserInterface>,
586}
587
588impl JobListBoolParameterDefinition {
589    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
590        let items = match value {
591            ExprValue::ListBool(v) => v,
592            _ => {
593                return Err(format!(
594                    "Parameter '{}': expected list[bool], got {}",
595                    self.name,
596                    value.type_name()
597                ))
598            }
599        };
600        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
601        Ok(())
602    }
603
604    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
605        let mut errors = Vec::new();
606        validate_list_length(
607            &self.name,
608            &self.default,
609            self.min_length,
610            self.max_length,
611            &mut errors,
612        );
613        if let Some(ui) = &self.user_interface {
614            validate_ui(
615                self.name.as_str(),
616                &ui.label,
617                &ui.group_label,
618                &ui.control,
619                &["CHECK_BOX_LIST", "HIDDEN"],
620                &mut errors,
621            );
622        }
623        if errors.is_empty() {
624            Ok(())
625        } else {
626            Err(errors)
627        }
628    }
629}
630
631/// §2.16 JobListListIntParameterDefinition
632#[derive(Debug, Clone, Deserialize)]
633#[serde(rename_all = "camelCase", deny_unknown_fields)]
634pub struct JobListListIntParameterDefinition {
635    pub name: Identifier,
636    pub description: Option<Description>,
637    pub default: Option<Vec<Vec<FlexInt>>>,
638    pub min_length: Option<usize>,
639    pub max_length: Option<usize>,
640    pub item: Option<ListListIntItemConstraints>,
641    pub user_interface: Option<HiddenOnlyUserInterface>,
642}
643
644#[derive(Debug, Clone, Deserialize)]
645#[serde(rename_all = "camelCase", deny_unknown_fields)]
646pub struct ListListIntItemConstraints {
647    pub min_length: Option<usize>,
648    pub max_length: Option<usize>,
649    pub item: Option<ListIntItemConstraints>,
650}
651
652impl JobListListIntParameterDefinition {
653    pub fn check_value_constraints(&self, value: &ExprValue) -> Result<(), String> {
654        let items = match value {
655            ExprValue::ListList(v, _, _) => v,
656            _ => {
657                return Err(format!(
658                    "Parameter '{}': expected list[list[int]], got {}",
659                    self.name,
660                    value.type_name()
661                ))
662            }
663        };
664        check_list_length(&self.name, items.len(), self.min_length, self.max_length)?;
665        if let Some(item) = &self.item {
666            for (i, inner) in items.iter().enumerate() {
667                let ints = match inner {
668                    ExprValue::ListInt(v) => v,
669                    _ => {
670                        return Err(format!(
671                            "Parameter '{}': item[{i}] expected list[int], got {}",
672                            self.name,
673                            inner.type_name()
674                        ))
675                    }
676                };
677                if let Some(min) = item.min_length {
678                    if ints.len() < min {
679                        return Err(format!(
680                            "Parameter '{}': item[{i}] length {} is less than minimum {min}",
681                            self.name,
682                            ints.len()
683                        ));
684                    }
685                }
686                if let Some(max) = item.max_length {
687                    if ints.len() > max {
688                        return Err(format!(
689                            "Parameter '{}': item[{i}] length {} exceeds maximum {max}",
690                            self.name,
691                            ints.len()
692                        ));
693                    }
694                }
695                if let Some(inner_item) = &item.item {
696                    check_int_items(&self.name, ints, inner_item, &format!("item[{i}]"))?;
697                }
698            }
699        }
700        Ok(())
701    }
702
703    pub fn validate_definition(&self) -> Result<(), Vec<String>> {
704        let mut errors = Vec::new();
705        validate_list_length(
706            &self.name,
707            &self.default,
708            self.min_length,
709            self.max_length,
710            &mut errors,
711        );
712        if let (Some(default), Some(item)) = (&self.default, &self.item) {
713            for (i, inner) in default.iter().enumerate() {
714                if let Some(min) = item.min_length {
715                    if inner.len() < min {
716                        errors.push(format!("Parameter '{}': default[{i}] inner list length {} < item minLength {min}.", self.name, inner.len()));
717                    }
718                }
719                if let Some(max) = item.max_length {
720                    if inner.len() > max {
721                        errors.push(format!("Parameter '{}': default[{i}] inner list length {} > item maxLength {max}.", self.name, inner.len()));
722                    }
723                }
724                if let Some(inner_item) = &item.item {
725                    validate_int_item_defaults(
726                        &self.name,
727                        inner,
728                        inner_item,
729                        &format!("default[{i}]"),
730                        "item.item",
731                        &mut errors,
732                    );
733                }
734            }
735        }
736        if let Some(ui) = &self.user_interface {
737            validate_ui(
738                self.name.as_str(),
739                &ui.label,
740                &ui.group_label,
741                &ui.control,
742                &["HIDDEN"],
743                &mut errors,
744            );
745        }
746        if errors.is_empty() {
747            Ok(())
748        } else {
749            Err(errors)
750        }
751    }
752}
753
754/// Helper: check list length against minLength/maxLength for runtime constraint checking.
755fn check_list_length(
756    param_name: &Identifier,
757    len: usize,
758    min_length: Option<usize>,
759    max_length: Option<usize>,
760) -> Result<(), String> {
761    if let Some(min) = min_length {
762        if len < min {
763            return Err(format!(
764                "Parameter '{param_name}': list length {len} is less than minimum {min}"
765            ));
766        }
767    }
768    if let Some(max) = max_length {
769        if len > max {
770            return Err(format!(
771                "Parameter '{param_name}': list length {len} exceeds maximum {max}"
772            ));
773        }
774    }
775    Ok(())
776}
777
778/// Helper: validate list default length against minLength/maxLength.
779fn validate_list_length<T>(
780    param_name: &Identifier,
781    default: &Option<Vec<T>>,
782    min_length: Option<usize>,
783    max_length: Option<usize>,
784    errors: &mut Vec<String>,
785) {
786    if let Some(default) = default {
787        if let Some(min) = min_length {
788            if default.len() < min {
789                errors.push(format!(
790                    "Parameter '{param_name}': default list length {} < minLength {min}.",
791                    default.len()
792                ));
793            }
794        }
795        if let Some(max) = max_length {
796            if default.len() > max {
797                errors.push(format!(
798                    "Parameter '{param_name}': default list length {} > maxLength {max}.",
799                    default.len()
800                ));
801            }
802        }
803    }
804}
805
806/// Shared UI validation: labels + control allowlist.
807fn validate_ui(
808    name: &str,
809    label: &Option<String>,
810    group_label: &Option<String>,
811    control: &Option<String>,
812    allowed_controls: &[&str],
813    errors: &mut Vec<String>,
814) {
815    errors.extend(validate_ui_label(label, "label", name));
816    errors.extend(validate_ui_label(group_label, "groupLabel", name));
817    if let Some(c) = control {
818        if !allowed_controls.contains(&c.as_str()) {
819            errors.push(format!("Parameter '{name}': unknown control '{c}'."));
820        }
821    }
822}
823
824/// Helper: check string items against item constraints at runtime.
825fn check_string_items(
826    name: &Identifier,
827    items: &[String],
828    item: &ListStringItemConstraints,
829) -> Result<(), String> {
830    for (i, s) in items.iter().enumerate() {
831        if let Some(min) = item.min_length {
832            let char_len = s.chars().count();
833            if char_len < min {
834                return Err(format!(
835                    "Parameter '{name}': item[{i}] length {} is less than minimum {min}",
836                    char_len
837                ));
838            }
839        }
840        if let Some(max) = item.max_length {
841            let char_len = s.chars().count();
842            if char_len > max {
843                return Err(format!(
844                    "Parameter '{name}': item[{i}] length {} exceeds maximum {max}",
845                    char_len
846                ));
847            }
848        }
849        if let Some(allowed) = &item.allowed_values {
850            if !allowed.contains(s) {
851                return Err(format!(
852                    "Parameter '{name}': item[{i}] '{s}' is not in allowed values"
853                ));
854            }
855        }
856    }
857    Ok(())
858}
859
860/// Helper: validate string item defaults against item constraints.
861fn validate_string_item_defaults(
862    name: &Identifier,
863    default: &[String],
864    item: &ListStringItemConstraints,
865    errors: &mut Vec<String>,
866) {
867    for (i, v) in default.iter().enumerate() {
868        if let Some(allowed) = &item.allowed_values {
869            if !allowed.contains(v) {
870                errors.push(format!(
871                    "Parameter '{name}': default[{i}] '{v}' not in item allowedValues."
872                ));
873            }
874        }
875        if let Some(min) = item.min_length {
876            let char_len = v.chars().count();
877            if char_len < min {
878                errors.push(format!(
879                    "Parameter '{name}': default[{i}] length {} < item minLength {min}.",
880                    char_len
881                ));
882            }
883        }
884        if let Some(max) = item.max_length {
885            let char_len = v.chars().count();
886            if char_len > max {
887                errors.push(format!(
888                    "Parameter '{name}': default[{i}] length {} > item maxLength {max}.",
889                    char_len
890                ));
891            }
892        }
893    }
894}
895
896/// Helper: check int items against item constraints at runtime.
897fn check_int_items(
898    name: &Identifier,
899    items: &[i64],
900    item: &ListIntItemConstraints,
901    prefix: &str,
902) -> Result<(), String> {
903    for (i, v) in items.iter().enumerate() {
904        if let Some(min) = &item.min_value {
905            if *v < min.0 {
906                return Err(format!(
907                    "Parameter '{name}': {prefix}[{i}] {v} is less than minimum {}",
908                    min.0
909                ));
910            }
911        }
912        if let Some(max) = &item.max_value {
913            if *v > max.0 {
914                return Err(format!(
915                    "Parameter '{name}': {prefix}[{i}] {v} exceeds maximum {}",
916                    max.0
917                ));
918            }
919        }
920        if let Some(allowed) = &item.allowed_values {
921            if !allowed.iter().any(|a| a.0 == *v) {
922                return Err(format!(
923                    "Parameter '{name}': {prefix}[{i}] {v} is not in allowed values"
924                ));
925            }
926        }
927    }
928    Ok(())
929}
930
931/// Helper: validate int item defaults against item constraints.
932fn validate_int_item_defaults(
933    name: &Identifier,
934    defaults: &[FlexInt],
935    item: &ListIntItemConstraints,
936    prefix: &str,
937    constraint_label: &str,
938    errors: &mut Vec<String>,
939) {
940    for (i, v) in defaults.iter().enumerate() {
941        if let Some(min) = &item.min_value {
942            if v.0 < min.0 {
943                errors.push(format!(
944                    "Parameter '{name}': {prefix}[{i}] {} < {constraint_label} minValue {}.",
945                    v.0, min.0
946                ));
947            }
948        }
949        if let Some(max) = &item.max_value {
950            if v.0 > max.0 {
951                errors.push(format!(
952                    "Parameter '{name}': {prefix}[{i}] {} > {constraint_label} maxValue {}.",
953                    v.0, max.0
954                ));
955            }
956        }
957        if let Some(allowed) = &item.allowed_values {
958            if !allowed.iter().any(|a| a.0 == v.0) {
959                errors.push(format!(
960                    "Parameter '{name}': {prefix}[{i}] {} not in {constraint_label} allowedValues.",
961                    v.0
962                ));
963            }
964        }
965    }
966}