1use std::path::Path;
4
5use runmat_accelerate_api::handle_is_logical;
6use runmat_builtins::{
7 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
8 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
9 CellArray, CharArray, NumericDType, SparseTensor, Value,
10};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::identifiers::is_valid_varname;
14use crate::builtins::introspection::class::class_name_for_value;
15use crate::builtins::introspection::underlying_type::underlying_type_matches;
16use crate::{build_runtime_error, BuiltinResult, RuntimeError};
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum ValidationAtom {
20 Number(f64),
21 Text(String),
22 Bool(bool),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26pub struct RangeInclusivity {
27 pub lower: bool,
28 pub upper: bool,
29}
30
31impl RangeInclusivity {
32 pub const CLOSED: Self = Self {
33 lower: true,
34 upper: true,
35 };
36
37 pub const OPEN: Self = Self {
38 lower: false,
39 upper: false,
40 };
41
42 pub const OPEN_LEFT: Self = Self {
43 lower: false,
44 upper: true,
45 };
46
47 pub const OPEN_RIGHT: Self = Self {
48 lower: true,
49 upper: false,
50 };
51}
52
53const VALUE_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
54 name: "A",
55 ty: BuiltinParamType::Any,
56 arity: BuiltinParamArity::Required,
57 default: None,
58 description: "Value to validate.",
59};
60
61const EXTRA_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
62 name: "B",
63 ty: BuiltinParamType::Any,
64 arity: BuiltinParamArity::Variadic,
65 default: None,
66 description: "Additional validator-specific argument.",
67};
68
69const PREDICATE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
70 name: "tf",
71 ty: BuiltinParamType::LogicalArray,
72 arity: BuiltinParamArity::Required,
73 default: None,
74 description: "Validation result.",
75}];
76
77const VALIDATOR_INPUTS: [BuiltinParamDescriptor; 2] = [VALUE_INPUT, EXTRA_INPUT];
78const VALIDATOR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
79 label: "mustBe*(A, ...)",
80 inputs: &VALIDATOR_INPUTS,
81 outputs: &[],
82}];
83
84const PREDICATE_INPUTS: [BuiltinParamDescriptor; 1] = [VALUE_INPUT];
85const ISVARNAME_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
86 label: "tf = isvarname(S)",
87 inputs: &PREDICATE_INPUTS,
88 outputs: &PREDICATE_OUTPUT,
89}];
90
91const NAMEDARGS_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
92 label: "C = namedargs2cell(S)",
93 inputs: &PREDICATE_INPUTS,
94 outputs: &[BuiltinParamDescriptor {
95 name: "C",
96 ty: BuiltinParamType::Any,
97 arity: BuiltinParamArity::Required,
98 default: None,
99 description: "Cell row vector of alternating field names and values.",
100 }],
101}];
102
103const VALIDATION_ERROR_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
104 code: "RM.ARGUMENT_VALIDATION.FAILED",
105 identifier: Some("RunMat:validators:ValidationFailed"),
106 when: "A value does not satisfy the requested validator.",
107 message: "argument validation failed",
108};
109
110const VALIDATION_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
111 code: "RM.ARGUMENT_VALIDATION.INVALID_ARGUMENT",
112 identifier: Some("RunMat:validators:InvalidArgument"),
113 when: "A validator receives an unsupported argument count or argument type.",
114 message: "invalid argument validation input",
115};
116
117const VALIDATION_ERRORS: [BuiltinErrorDescriptor; 2] =
118 [VALIDATION_ERROR_FAILED, VALIDATION_ERROR_INVALID_ARGUMENT];
119
120pub const VALIDATOR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
121 signatures: &VALIDATOR_SIGNATURES,
122 output_mode: BuiltinOutputMode::Fixed,
123 completion_policy: BuiltinCompletionPolicy::Public,
124 errors: &VALIDATION_ERRORS,
125};
126
127pub const ISVARNAME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
128 signatures: &ISVARNAME_SIGNATURES,
129 output_mode: BuiltinOutputMode::Fixed,
130 completion_policy: BuiltinCompletionPolicy::Public,
131 errors: &[],
132};
133
134pub const NAMEDARGS2CELL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
135 signatures: &NAMEDARGS_SIGNATURES,
136 output_mode: BuiltinOutputMode::Fixed,
137 completion_policy: BuiltinCompletionPolicy::Public,
138 errors: &VALIDATION_ERRORS,
139};
140
141pub fn validation_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
142 let detail = detail.as_ref();
143 let message = if detail.is_empty() {
144 format!("{builtin}: validation failed")
145 } else {
146 format!("{builtin}: {detail}")
147 };
148 build_runtime_error(message)
149 .with_builtin(builtin)
150 .with_identifier(format!("RunMat:{builtin}:ValidationFailed"))
151 .build()
152}
153
154fn invalid_argument_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
155 let detail = detail.as_ref();
156 let message = if detail.is_empty() {
157 format!("{builtin}: invalid argument")
158 } else {
159 format!("{builtin}: {detail}")
160 };
161 build_runtime_error(message)
162 .with_builtin(builtin)
163 .with_identifier(format!("RunMat:{builtin}:InvalidArgument"))
164 .build()
165}
166
167fn pass() -> BuiltinResult<Value> {
168 Ok(Value::Num(0.0))
169}
170
171fn require_args<'a>(
172 builtin: &str,
173 args: &'a [Value],
174 min: usize,
175 max: usize,
176) -> BuiltinResult<&'a Value> {
177 if args.len() < min || args.len() > max {
178 return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
179 }
180 args.first()
181 .ok_or_else(|| invalid_argument_error(builtin, "missing value").into())
182}
183
184fn require_arg_count(builtin: &str, args: &[Value], min: usize, max: usize) -> BuiltinResult<()> {
185 if args.len() < min || args.len() > max {
186 return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
187 }
188 Ok(())
189}
190
191fn require_exact_arg_count(builtin: &str, args: &[Value], expected: usize) -> BuiltinResult<()> {
192 require_arg_count(builtin, args, expected, expected)
193}
194
195fn check_validator(builtin: &str, ok: bool) -> BuiltinResult<Value> {
196 if ok {
197 pass()
198 } else {
199 Err(validation_error(builtin, "value does not satisfy validator").into())
200 }
201}
202
203pub fn dispatch_validator(builtin: &str, args: Vec<Value>) -> BuiltinResult<Value> {
204 let value = require_args(builtin, &args, 1, usize::MAX)?;
205 match builtin {
206 "mustBeA" => {
207 require_exact_arg_count(builtin, &args, 2)?;
208 check_validator(builtin, must_be_a(value, type_names_arg(&args, 1)?)?)
209 }
210 "mustBeColumn" => {
211 require_exact_arg_count(builtin, &args, 1)?;
212 check_validator(builtin, value_is_column(value))
213 }
214 "mustBeFile" => check_validator(builtin, {
215 require_exact_arg_count(builtin, &args, 1)?;
216 value_texts(value)?.iter().all(|p| Path::new(p).is_file())
217 }),
218 "mustBeFinite" => {
219 require_exact_arg_count(builtin, &args, 1)?;
220 check_validator(builtin, value_is_finite(value))
221 }
222 "mustBeFloat" => {
223 require_exact_arg_count(builtin, &args, 1)?;
224 check_validator(builtin, value_is_float(value))
225 }
226 "mustBeFolder" => check_validator(builtin, {
227 require_exact_arg_count(builtin, &args, 1)?;
228 value_texts(value)?.iter().all(|p| Path::new(p).is_dir())
229 }),
230 "mustBeGreaterThan" => {
231 require_exact_arg_count(builtin, &args, 2)?;
232 check_validator(
233 builtin,
234 value_is_greater_than(value, numeric_arg(&args, 1)?),
235 )
236 }
237 "mustBeGreaterThanOrEqual" => {
238 require_exact_arg_count(builtin, &args, 2)?;
239 check_validator(
240 builtin,
241 value_is_greater_than_or_equal(value, numeric_arg(&args, 1)?),
242 )
243 }
244 "mustBeInRange" => {
245 require_arg_count(builtin, &args, 3, 5)?;
246 let lower = numeric_arg(&args, 1)?;
247 let upper = numeric_arg(&args, 2)?;
248 let inclusivity = range_inclusivity_arg(builtin, &args[3..])?;
249 check_validator(builtin, value_is_in_range(value, lower, upper, inclusivity))
250 }
251 "mustBeInteger" => {
252 require_exact_arg_count(builtin, &args, 1)?;
253 check_validator(builtin, value_is_integer(value))
254 }
255 "mustBeLessThan" => {
256 require_exact_arg_count(builtin, &args, 2)?;
257 check_validator(builtin, value_is_less_than(value, numeric_arg(&args, 1)?))
258 }
259 "mustBeLessThanOrEqual" => {
260 require_exact_arg_count(builtin, &args, 2)?;
261 check_validator(
262 builtin,
263 value_is_less_than_or_equal(value, numeric_arg(&args, 1)?),
264 )
265 }
266 "mustBeMember" => {
267 require_exact_arg_count(builtin, &args, 2)?;
268 check_validator(builtin, value_is_member(value, &args[1])?)
269 }
270 "mustBeNegative" => {
271 require_exact_arg_count(builtin, &args, 1)?;
272 check_validator(builtin, value_is_negative(value))
273 }
274 "mustBeNonempty" => {
275 require_exact_arg_count(builtin, &args, 1)?;
276 check_validator(builtin, !value_is_empty(value))
277 }
278 "mustBeNonmissing" => {
279 require_exact_arg_count(builtin, &args, 1)?;
280 check_validator(builtin, value_is_nonmissing(value))
281 }
282 "mustBeNonNan" => {
283 require_exact_arg_count(builtin, &args, 1)?;
284 check_validator(builtin, value_is_non_nan(value))
285 }
286 "mustBeNonnegative" => {
287 require_exact_arg_count(builtin, &args, 1)?;
288 check_validator(builtin, value_is_nonnegative(value))
289 }
290 "mustBeNonpositive" => {
291 require_exact_arg_count(builtin, &args, 1)?;
292 check_validator(builtin, value_is_nonpositive(value))
293 }
294 "mustBeNonsparse" => {
295 require_exact_arg_count(builtin, &args, 1)?;
296 check_validator(builtin, !matches!(value, Value::SparseTensor(_)))
297 }
298 "mustBeNonzero" => {
299 require_exact_arg_count(builtin, &args, 1)?;
300 check_validator(builtin, value_is_nonzero(value))
301 }
302 "mustBeNonzeroLengthText" => {
303 require_exact_arg_count(builtin, &args, 1)?;
304 check_validator(builtin, value_is_nonzero_length_text(value))
305 }
306 "mustBeNumeric" => {
307 require_exact_arg_count(builtin, &args, 1)?;
308 check_validator(builtin, value_is_numeric(value))
309 }
310 "mustBeNumericOrLogical" => {
311 require_exact_arg_count(builtin, &args, 1)?;
312 check_validator(builtin, value_is_numeric_or_logical(value))
313 }
314 "mustBePositive" => {
315 require_exact_arg_count(builtin, &args, 1)?;
316 check_validator(builtin, value_is_positive(value))
317 }
318 "mustBeReal" => {
319 require_exact_arg_count(builtin, &args, 1)?;
320 check_validator(builtin, value_is_real(value))
321 }
322 "mustBeScalarOrEmpty" => {
323 require_exact_arg_count(builtin, &args, 1)?;
324 check_validator(builtin, value_is_scalar_or_empty(value))
325 }
326 "mustBeSparse" => {
327 require_exact_arg_count(builtin, &args, 1)?;
328 check_validator(builtin, matches!(value, Value::SparseTensor(_)))
329 }
330 "mustBeText" => {
331 require_exact_arg_count(builtin, &args, 1)?;
332 check_validator(builtin, value_is_text(value))
333 }
334 "mustBeTextScalar" => {
335 require_exact_arg_count(builtin, &args, 1)?;
336 check_validator(builtin, value_is_text_scalar(value))
337 }
338 "mustBeUnderlyingType" => {
339 require_exact_arg_count(builtin, &args, 2)?;
340 check_validator(
341 builtin,
342 value_underlying_type_matches(value, type_names_arg(&args, 1)?)?,
343 )
344 }
345 "mustBeValidVariableName" => {
346 require_exact_arg_count(builtin, &args, 1)?;
347 check_validator(
348 builtin,
349 value_texts(value)?
350 .iter()
351 .all(|name| is_valid_varname(name)),
352 )
353 }
354 "mustBeVector" => {
355 require_exact_arg_count(builtin, &args, 1)?;
356 check_validator(builtin, value_is_vector(value)?)
357 }
358 "validateFunctionSignaturesJSON" => {
359 require_exact_arg_count(builtin, &args, 1)?;
360 validate_function_signatures_json(value)?;
361 pass()
362 }
363 _ => Err(invalid_argument_error(builtin, "unknown validator").into()),
364 }
365}
366
367pub fn value_shape_2d(value: &Value) -> (usize, usize) {
368 match value {
369 Value::Tensor(t) => (t.rows, t.cols),
370 Value::SparseTensor(t) => (t.rows, t.cols),
371 Value::ComplexTensor(t) => (t.rows, t.cols),
372 Value::LogicalArray(a) => {
373 let rows = a.shape.first().copied().unwrap_or(0);
374 let cols = a.shape.get(1).copied().unwrap_or(1);
375 (rows, cols)
376 }
377 Value::Cell(c) => (c.rows, c.cols),
378 Value::CharArray(c) => (c.rows, c.cols),
379 Value::StringArray(s) => (s.rows, s.cols),
380 Value::GpuTensor(handle) => {
381 let rows = handle.shape.first().copied().unwrap_or(1);
382 let cols = handle.shape.get(1).copied().unwrap_or(1);
383 (rows, cols)
384 }
385 _ => (1, 1),
386 }
387}
388
389pub fn value_is_empty(value: &Value) -> bool {
390 match value {
391 Value::Tensor(t) => t.data.is_empty(),
392 Value::SparseTensor(t) => t.rows == 0 || t.cols == 0,
393 Value::ComplexTensor(t) => t.data.is_empty(),
394 Value::LogicalArray(a) => a.data.is_empty(),
395 Value::StringArray(s) => s.data.is_empty(),
396 Value::CharArray(c) => c.rows == 0 || c.cols == 0,
397 Value::Cell(c) => c.data.is_empty(),
398 Value::GpuTensor(handle) => handle.shape.contains(&0),
399 _ => false,
400 }
401}
402
403pub fn value_is_finite(value: &Value) -> bool {
404 match value {
405 Value::Num(v) => v.is_finite(),
406 Value::Int(_) | Value::Bool(_) => true,
407 Value::Complex(re, im) => re.is_finite() && im.is_finite(),
408 Value::Tensor(t) => t.data.iter().all(|v| v.is_finite()),
409 Value::SparseTensor(t) => t.values.iter().all(|v| v.is_finite()),
410 Value::ComplexTensor(t) => t
411 .data
412 .iter()
413 .all(|(re, im)| re.is_finite() && im.is_finite()),
414 Value::LogicalArray(_) | Value::CharArray(_) => true,
415 Value::GpuTensor(_) => true,
416 _ => false,
417 }
418}
419
420pub fn value_is_numeric(value: &Value) -> bool {
421 match value {
422 Value::Num(_)
423 | Value::Int(_)
424 | Value::Complex(_, _)
425 | Value::Tensor(_)
426 | Value::SparseTensor(_)
427 | Value::ComplexTensor(_) => true,
428 Value::GpuTensor(handle) => !handle_is_logical(handle),
429 _ => false,
430 }
431}
432
433pub fn value_is_float(value: &Value) -> bool {
434 match value {
435 Value::Num(_) | Value::Complex(_, _) | Value::ComplexTensor(_) => true,
436 Value::Tensor(t) => matches!(t.dtype, NumericDType::F64 | NumericDType::F32),
437 Value::SparseTensor(_) => true,
438 Value::GpuTensor(handle) => !handle_is_logical(handle),
439 _ => false,
440 }
441}
442
443pub fn value_is_numeric_or_logical(value: &Value) -> bool {
444 value_is_numeric(value) || matches!(value, Value::Bool(_) | Value::LogicalArray(_))
445}
446
447pub fn value_is_text(value: &Value) -> bool {
448 match value {
449 Value::String(_) | Value::StringArray(_) => true,
450 Value::CharArray(chars) => chars.rows == 1,
451 Value::Cell(cell) => cell.data.iter().all(value_is_text),
452 _ => false,
453 }
454}
455
456pub fn value_is_text_scalar(value: &Value) -> bool {
457 match value {
458 Value::String(_) => true,
459 Value::StringArray(strings) => {
460 strings.data.len() == 1 && strings.rows == 1 && strings.cols == 1
461 }
462 Value::CharArray(chars) => chars.rows == 1,
463 _ => false,
464 }
465}
466
467pub fn value_is_nonzero_length_text(value: &Value) -> bool {
468 if !value_is_text(value) {
469 return false;
470 }
471 match value {
472 Value::String(s) => !s.is_empty(),
473 Value::StringArray(s) => s.data.iter().all(|value| !value.is_empty()),
474 Value::CharArray(c) => c.rows == 1 && c.cols > 0,
475 Value::Cell(c) => c.data.iter().all(value_is_nonzero_length_text),
476 _ => false,
477 }
478}
479
480pub fn value_is_scalar_or_empty(value: &Value) -> bool {
481 let (rows, cols) = value_shape_2d(value);
482 (rows == 1 && cols == 1) || rows == 0 || cols == 0
483}
484
485pub fn value_is_real(value: &Value) -> bool {
486 match value {
487 Value::Complex(_, im) => *im == 0.0,
488 Value::ComplexTensor(t) => t.data.iter().all(|(_, im)| *im == 0.0),
489 _ => true,
490 }
491}
492
493pub fn value_is_integer(value: &Value) -> bool {
494 match value {
495 Value::Int(_) | Value::Bool(_) | Value::LogicalArray(_) => true,
496 Value::Num(v) => v.is_finite() && v.fract() == 0.0,
497 Value::Tensor(t) => t.data.iter().all(|v| v.is_finite() && v.fract() == 0.0),
498 Value::SparseTensor(t) => t.values.iter().all(|v| v.is_finite() && v.fract() == 0.0),
499 Value::Complex(re, im) => *im == 0.0 && re.is_finite() && re.fract() == 0.0,
500 Value::ComplexTensor(t) => t
501 .data
502 .iter()
503 .all(|(re, im)| *im == 0.0 && re.is_finite() && re.fract() == 0.0),
504 _ => false,
505 }
506}
507
508pub fn value_is_non_nan(value: &Value) -> bool {
509 match value {
510 Value::Num(v) => !v.is_nan(),
511 Value::Complex(re, im) => !re.is_nan() && !im.is_nan(),
512 Value::Tensor(t) => t.data.iter().all(|v| !v.is_nan()),
513 Value::SparseTensor(t) => t.values.iter().all(|v| !v.is_nan()),
514 Value::ComplexTensor(t) => t.data.iter().all(|(re, im)| !re.is_nan() && !im.is_nan()),
515 Value::Cell(c) => c.data.iter().all(value_is_non_nan),
516 _ => true,
517 }
518}
519
520pub fn value_is_nonmissing(value: &Value) -> bool {
521 value_is_non_nan(value)
522}
523
524pub fn value_is_positive(value: &Value) -> bool {
525 numeric_values_all(value, |v| v.is_finite() && v > 0.0)
526}
527
528pub fn value_is_negative(value: &Value) -> bool {
529 numeric_values_all(value, |v| v.is_finite() && v < 0.0)
530}
531
532pub fn value_is_nonnegative(value: &Value) -> bool {
533 numeric_values_all(value, |v| v.is_finite() && v >= 0.0)
534}
535
536pub fn value_is_nonpositive(value: &Value) -> bool {
537 numeric_values_all(value, |v| v.is_finite() && v <= 0.0)
538}
539
540pub fn value_is_nonzero(value: &Value) -> bool {
541 match value {
542 Value::Complex(re, im) => re.is_finite() && im.is_finite() && (*re != 0.0 || *im != 0.0),
543 Value::ComplexTensor(t) => t
544 .data
545 .iter()
546 .all(|(re, im)| re.is_finite() && im.is_finite() && (*re != 0.0 || *im != 0.0)),
547 _ => numeric_values_all(value, |v| v.is_finite() && v != 0.0),
548 }
549}
550
551pub fn value_is_greater_than_or_equal(value: &Value, threshold: f64) -> bool {
552 numeric_values_all(value, |v| v.is_finite() && v >= threshold)
553}
554
555pub fn value_is_less_than_or_equal(value: &Value, threshold: f64) -> bool {
556 numeric_values_all(value, |v| v.is_finite() && v <= threshold)
557}
558
559pub fn value_is_greater_than(value: &Value, threshold: f64) -> bool {
560 numeric_values_all(value, |v| v.is_finite() && v > threshold)
561}
562
563pub fn value_is_less_than(value: &Value, threshold: f64) -> bool {
564 numeric_values_all(value, |v| v.is_finite() && v < threshold)
565}
566
567pub fn value_is_in_range(
568 value: &Value,
569 lower: f64,
570 upper: f64,
571 inclusivity: RangeInclusivity,
572) -> bool {
573 numeric_values_all(value, |v| {
574 v.is_finite()
575 && if inclusivity.lower {
576 v >= lower
577 } else {
578 v > lower
579 }
580 && if inclusivity.upper {
581 v <= upper
582 } else {
583 v < upper
584 }
585 })
586}
587
588pub fn value_is_column(value: &Value) -> bool {
589 let (_, cols) = value_shape_2d(value);
590 cols == 1
591}
592
593pub fn value_is_vector(value: &Value) -> Result<bool, RuntimeError> {
594 let (rows, cols) = value_shape_2d(value);
595 Ok((rows == 1 || cols == 1) && !(rows == 0 && cols > 1) && !(cols == 0 && rows > 1))
596}
597
598pub fn value_matches_class(value: &Value, class_name: &str) -> bool {
599 let requested = class_name.trim();
600 if requested.is_empty() {
601 return false;
602 }
603 match requested.to_ascii_lowercase().as_str() {
604 "numeric" => value_is_numeric(value),
605 "float" => value_is_float(value),
606 "integer" => matches!(value, Value::Int(_)),
607 "logical" => matches!(value, Value::Bool(_) | Value::LogicalArray(_)),
608 "char" => matches!(value, Value::CharArray(_)),
609 "string" => matches!(value, Value::String(_) | Value::StringArray(_)),
610 "cell" => matches!(value, Value::Cell(_)),
611 "struct" => matches!(value, Value::Struct(_)),
612 "sparse" => matches!(value, Value::SparseTensor(_)),
613 "double" => {
614 matches!(
615 value,
616 Value::Num(_) | Value::Complex(_, _) | Value::SparseTensor(_)
617 ) || matches!(value, Value::Tensor(t) if t.dtype == NumericDType::F64)
618 || matches!(value, Value::ComplexTensor(_))
619 }
620 "single" => matches!(value, Value::Tensor(t) if t.dtype == NumericDType::F32),
621 "gpuarray" => matches!(value, Value::GpuTensor(_)),
622 _ => class_name_for_value(value).eq_ignore_ascii_case(requested),
623 }
624}
625
626pub fn must_be_a(value: &Value, class_names: Vec<String>) -> Result<bool, RuntimeError> {
627 Ok(class_names
628 .iter()
629 .any(|class_name| value_matches_class(value, class_name)))
630}
631
632pub fn value_underlying_type_matches(
633 value: &Value,
634 class_names: Vec<String>,
635) -> Result<bool, RuntimeError> {
636 Ok(class_names
637 .iter()
638 .any(|class_name| underlying_type_matches(value, class_name)))
639}
640
641pub fn value_is_member(value: &Value, set: &Value) -> Result<bool, RuntimeError> {
642 let values = atoms(value)?;
643 let allowed = atoms(set)?;
644 value_is_member_atoms_inner(&values, &allowed)
645}
646
647pub fn value_is_member_atoms(
648 value: &Value,
649 allowed: &[ValidationAtom],
650) -> Result<bool, RuntimeError> {
651 let values = atoms(value)?;
652 value_is_member_atoms_inner(&values, allowed)
653}
654
655fn value_is_member_atoms_inner(
656 values: &[ValidationAtom],
657 allowed: &[ValidationAtom],
658) -> Result<bool, RuntimeError> {
659 Ok(values
660 .iter()
661 .all(|value| allowed.iter().any(|allowed| atom_eq(value, allowed))))
662}
663
664pub fn atoms(value: &Value) -> Result<Vec<ValidationAtom>, RuntimeError> {
665 match value {
666 Value::Num(v) => Ok(vec![ValidationAtom::Number(*v)]),
667 Value::Int(v) => Ok(vec![ValidationAtom::Number(v.to_f64())]),
668 Value::Bool(v) => Ok(vec![ValidationAtom::Bool(*v)]),
669 Value::String(s) => Ok(vec![ValidationAtom::Text(s.clone())]),
670 Value::CharArray(c) if c.rows == 1 => Ok(vec![ValidationAtom::Text(chars_to_string(c))]),
671 Value::Tensor(t) => Ok(t.data.iter().copied().map(ValidationAtom::Number).collect()),
672 Value::SparseTensor(t) => sparse_atoms(t),
673 Value::LogicalArray(a) => Ok(a
674 .data
675 .iter()
676 .map(|v| ValidationAtom::Bool(*v != 0))
677 .collect()),
678 Value::StringArray(s) => Ok(s.data.iter().cloned().map(ValidationAtom::Text).collect()),
679 Value::Cell(c) => {
680 let mut out = Vec::new();
681 for entry in &c.data {
682 out.extend(atoms(entry)?);
683 }
684 Ok(out)
685 }
686 _ => Err(invalid_argument_error(
687 "mustBeMember",
688 "unsupported member value type",
689 )),
690 }
691}
692
693fn sparse_atoms(t: &SparseTensor) -> Result<Vec<ValidationAtom>, RuntimeError> {
694 let numel = t.rows.saturating_mul(t.cols);
695 let mut out = Vec::with_capacity(numel.min(t.values.len().saturating_add(1)));
696 out.extend(t.values.iter().copied().map(ValidationAtom::Number));
697 if t.values.len() < numel {
698 out.push(ValidationAtom::Number(0.0));
699 }
700 Ok(out)
701}
702
703fn atom_eq(left: &ValidationAtom, right: &ValidationAtom) -> bool {
704 match (left, right) {
705 (ValidationAtom::Number(a), ValidationAtom::Number(b)) => a == b,
706 (ValidationAtom::Text(a), ValidationAtom::Text(b)) => a == b,
707 (ValidationAtom::Bool(a), ValidationAtom::Bool(b)) => a == b,
708 _ => false,
709 }
710}
711
712fn numeric_values_all(value: &Value, pred: impl Fn(f64) -> bool) -> bool {
713 match value {
714 Value::Num(v) => pred(*v),
715 Value::Int(v) => pred(v.to_f64()),
716 Value::Bool(v) => pred(if *v { 1.0 } else { 0.0 }),
717 Value::LogicalArray(a) => a.data.iter().map(|v| f64::from(*v != 0)).all(pred),
718 Value::Tensor(t) => t.data.iter().copied().all(pred),
719 Value::SparseTensor(t) => {
720 let numel = t.rows.saturating_mul(t.cols);
721 t.values.iter().copied().all(&pred) && (t.values.len() >= numel || pred(0.0))
722 }
723 Value::Complex(re, im) => *im == 0.0 && pred(*re),
724 Value::ComplexTensor(t) => t.data.iter().all(|(re, im)| *im == 0.0 && pred(*re)),
725 _ => false,
726 }
727}
728
729fn numeric_arg(args: &[Value], index: usize) -> Result<f64, RuntimeError> {
730 match args.get(index) {
731 Some(Value::Num(v)) => Ok(*v),
732 Some(Value::Int(v)) => Ok(v.to_f64()),
733 Some(Value::Tensor(t)) if t.data.len() == 1 => Ok(t.data[0]),
734 Some(other) => Err(invalid_argument_error(
735 "argumentValidation",
736 format!(
737 "expected numeric scalar argument, got {}",
738 class_name_for_value(other)
739 ),
740 )),
741 None => Err(invalid_argument_error(
742 "argumentValidation",
743 "missing numeric scalar argument",
744 )),
745 }
746}
747
748fn type_names_arg(args: &[Value], index: usize) -> Result<Vec<String>, RuntimeError> {
749 match args.get(index) {
750 Some(value) => value_texts(value),
751 None => Err(invalid_argument_error(
752 "argumentValidation",
753 "missing type name argument",
754 )),
755 }
756}
757
758fn range_inclusivity_arg(builtin: &str, args: &[Value]) -> Result<RangeInclusivity, RuntimeError> {
759 match args {
760 [] => Ok(RangeInclusivity::CLOSED),
761 [flag] => range_inclusivity_single_flag(builtin, text_scalar_arg(builtin, flag)?.as_str()),
762 [lower, upper] => {
763 let lower =
764 range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, lower)?.as_str())?;
765 let upper =
766 range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, upper)?.as_str())?;
767 Ok(RangeInclusivity { lower, upper })
768 }
769 _ => Err(invalid_argument_error(
770 builtin,
771 "invalid range inclusivity flags",
772 )),
773 }
774}
775
776fn range_inclusivity_single_flag(
777 builtin: &str,
778 flag: &str,
779) -> Result<RangeInclusivity, RuntimeError> {
780 match flag.trim().to_ascii_lowercase().as_str() {
781 "inclusive" => Ok(RangeInclusivity::CLOSED),
782 "exclusive" => Ok(RangeInclusivity::OPEN),
783 "exclude-lower" | "openleft" | "open-left" => Ok(RangeInclusivity::OPEN_LEFT),
784 "exclude-upper" | "openright" | "open-right" => Ok(RangeInclusivity::OPEN_RIGHT),
785 _ => Err(invalid_argument_error(
786 builtin,
787 "range flag must be 'inclusive', 'exclusive', 'exclude-lower', or 'exclude-upper'",
788 )),
789 }
790}
791
792fn range_bound_inclusive_flag(builtin: &str, flag: &str) -> Result<bool, RuntimeError> {
793 match flag.trim().to_ascii_lowercase().as_str() {
794 "inclusive" => Ok(true),
795 "exclusive" => Ok(false),
796 _ => Err(invalid_argument_error(
797 builtin,
798 "range bound flag must be 'inclusive' or 'exclusive'",
799 )),
800 }
801}
802
803fn text_scalar_arg(builtin: &str, value: &Value) -> Result<String, RuntimeError> {
804 let texts = value_texts(value)?;
805 match texts.as_slice() {
806 [text] => Ok(text.clone()),
807 _ => Err(invalid_argument_error(builtin, "expected text scalar")),
808 }
809}
810
811fn value_texts(value: &Value) -> Result<Vec<String>, RuntimeError> {
812 match value {
813 Value::String(s) => Ok(vec![s.clone()]),
814 Value::StringArray(s) => Ok(s.data.clone()),
815 Value::CharArray(c) if c.rows == 1 => Ok(vec![chars_to_string(c)]),
816 Value::Cell(c) => {
817 let mut out = Vec::with_capacity(c.data.len());
818 for entry in &c.data {
819 out.extend(value_texts(entry)?);
820 }
821 Ok(out)
822 }
823 other => Err(invalid_argument_error(
824 "argumentValidation",
825 format!("expected text, got {}", class_name_for_value(other)),
826 )),
827 }
828}
829
830fn chars_to_string(chars: &CharArray) -> String {
831 chars.data.iter().collect()
832}
833
834pub fn isvarname_value(value: &Value) -> bool {
835 value_texts(value)
836 .map(|names| names.iter().all(|name| is_valid_varname(name)))
837 .unwrap_or(false)
838}
839
840pub fn namedargs2cell_value(value: Value) -> BuiltinResult<Value> {
841 let Value::Struct(struct_value) = value else {
842 return Err(
843 invalid_argument_error("namedargs2cell", "input must be a scalar struct").into(),
844 );
845 };
846 let mut data = Vec::with_capacity(struct_value.fields.len().saturating_mul(2));
847 for (field, value) in struct_value.fields {
848 data.push(Value::String(field));
849 data.push(value);
850 }
851 let cols = data.len();
852 let cell = CellArray::new(data, 1, cols)
853 .map_err(|err| invalid_argument_error("namedargs2cell", err))?;
854 Ok(Value::Cell(cell))
855}
856
857pub fn validate_function_signatures_json(value: &Value) -> BuiltinResult<()> {
858 for text in value_texts(value)? {
859 serde_json::from_str::<serde_json::Value>(&text).map_err(|err| {
860 invalid_argument_error(
861 "validateFunctionSignaturesJSON",
862 format!("invalid JSON signature payload: {err}"),
863 )
864 })?;
865 }
866 Ok(())
867}
868
869fn bool_type(
870 _: &[runmat_builtins::Type],
871 _: &runmat_builtins::ResolveContext,
872) -> runmat_builtins::Type {
873 runmat_builtins::Type::Bool
874}
875
876fn any_type(
877 _: &[runmat_builtins::Type],
878 _: &runmat_builtins::ResolveContext,
879) -> runmat_builtins::Type {
880 runmat_builtins::Type::Unknown
881}
882
883#[runtime_builtin(
884 name = "isvarname",
885 category = "argument-validation",
886 summary = "Return true when text is a valid MATLAB variable name.",
887 type_resolver(bool_type),
888 descriptor(self::ISVARNAME_DESCRIPTOR),
889 builtin_path = "crate::builtins::common::validation"
890)]
891fn isvarname_builtin(value: Value) -> BuiltinResult<Value> {
892 Ok(Value::Bool(isvarname_value(&value)))
893}
894
895#[runtime_builtin(
896 name = "namedargs2cell",
897 category = "argument-validation",
898 summary = "Convert a scalar name-value struct to an alternating name/value cell row.",
899 type_resolver(any_type),
900 descriptor(self::NAMEDARGS2CELL_DESCRIPTOR),
901 builtin_path = "crate::builtins::common::validation"
902)]
903fn namedargs2cell_builtin(value: Value) -> BuiltinResult<Value> {
904 namedargs2cell_value(value)
905}
906
907macro_rules! validator_builtin {
908 ($func:ident, $name:literal) => {
909 #[runtime_builtin(
910 name = $name,
911 category = "argument-validation",
912 summary = "Validate an input argument and throw if the constraint is not satisfied.",
913 sink = true,
914 suppress_auto_output = true,
915 descriptor(self::VALIDATOR_DESCRIPTOR),
916 builtin_path = "crate::builtins::common::validation"
917 )]
918 fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
919 dispatch_validator($name, args)
920 }
921 };
922}
923
924validator_builtin!(must_be_a_builtin, "mustBeA");
925validator_builtin!(must_be_column_builtin, "mustBeColumn");
926validator_builtin!(must_be_file_builtin, "mustBeFile");
927validator_builtin!(must_be_finite_builtin, "mustBeFinite");
928validator_builtin!(must_be_float_builtin, "mustBeFloat");
929validator_builtin!(must_be_folder_builtin, "mustBeFolder");
930validator_builtin!(must_be_greater_than_builtin, "mustBeGreaterThan");
931validator_builtin!(
932 must_be_greater_than_or_equal_builtin,
933 "mustBeGreaterThanOrEqual"
934);
935validator_builtin!(must_be_in_range_builtin, "mustBeInRange");
936validator_builtin!(must_be_integer_builtin, "mustBeInteger");
937validator_builtin!(must_be_less_than_builtin, "mustBeLessThan");
938validator_builtin!(must_be_less_than_or_equal_builtin, "mustBeLessThanOrEqual");
939validator_builtin!(must_be_member_builtin, "mustBeMember");
940validator_builtin!(must_be_negative_builtin, "mustBeNegative");
941validator_builtin!(must_be_nonempty_builtin, "mustBeNonempty");
942validator_builtin!(must_be_nonmissing_builtin, "mustBeNonmissing");
943validator_builtin!(must_be_non_nan_builtin, "mustBeNonNan");
944validator_builtin!(must_be_nonnegative_builtin, "mustBeNonnegative");
945validator_builtin!(must_be_nonpositive_builtin, "mustBeNonpositive");
946validator_builtin!(must_be_nonsparse_builtin, "mustBeNonsparse");
947validator_builtin!(must_be_nonzero_builtin, "mustBeNonzero");
948validator_builtin!(
949 must_be_nonzero_length_text_builtin,
950 "mustBeNonzeroLengthText"
951);
952validator_builtin!(must_be_numeric_builtin, "mustBeNumeric");
953validator_builtin!(must_be_numeric_or_logical_builtin, "mustBeNumericOrLogical");
954validator_builtin!(must_be_positive_builtin, "mustBePositive");
955validator_builtin!(must_be_real_builtin, "mustBeReal");
956validator_builtin!(must_be_scalar_or_empty_builtin, "mustBeScalarOrEmpty");
957validator_builtin!(must_be_sparse_builtin, "mustBeSparse");
958validator_builtin!(must_be_text_builtin, "mustBeText");
959validator_builtin!(must_be_text_scalar_builtin, "mustBeTextScalar");
960validator_builtin!(must_be_underlying_type_builtin, "mustBeUnderlyingType");
961validator_builtin!(
962 must_be_valid_variable_name_builtin,
963 "mustBeValidVariableName"
964);
965validator_builtin!(must_be_vector_builtin, "mustBeVector");
966validator_builtin!(
967 validate_function_signatures_json_builtin,
968 "validateFunctionSignaturesJSON"
969);
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974 use crate::builtins::common::identifiers::MATLAB_NAME_LENGTH_MAX;
975 use runmat_builtins::{IntValue, LogicalArray, StringArray, StructValue, Tensor};
976
977 fn ok(builtin: &str, args: Vec<Value>) {
978 dispatch_validator(builtin, args).unwrap_or_else(|err| {
979 panic!("{builtin} unexpectedly failed: {err}");
980 });
981 }
982
983 fn err(builtin: &str, args: Vec<Value>) {
984 assert!(
985 dispatch_validator(builtin, args).is_err(),
986 "{builtin} unexpectedly passed"
987 );
988 }
989
990 fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
991 Value::Tensor(Tensor::new_2d(data, rows, cols).unwrap())
992 }
993
994 fn sparse(values: Vec<f64>) -> Value {
995 Value::SparseTensor(SparseTensor::new(2, 2, vec![0, 1, 1], vec![0], values).unwrap())
996 }
997
998 #[test]
999 fn numeric_validators_check_all_elements() {
1000 let ok = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
1001 assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(ok)]).is_ok());
1002
1003 let bad = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1004 assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(bad)]).is_err());
1005 }
1006
1007 #[test]
1008 fn member_validator_accepts_numeric_and_text_sets() {
1009 let allowed = Tensor::new(vec![1.0, 3.0, 5.0], vec![1, 3]).unwrap();
1010 assert!(dispatch_validator(
1011 "mustBeMember",
1012 vec![Value::Num(3.0), Value::Tensor(allowed)]
1013 )
1014 .is_ok());
1015
1016 let allowed = StringArray::new(vec!["on".into(), "off".into()], vec![1, 2]).unwrap();
1017 assert!(dispatch_validator(
1018 "mustBeMember",
1019 vec![Value::String("on".into()), Value::StringArray(allowed)]
1020 )
1021 .is_ok());
1022 }
1023
1024 #[test]
1025 fn text_and_varname_validators_follow_core_shapes() {
1026 assert!(dispatch_validator(
1027 "mustBeNonzeroLengthText",
1028 vec![Value::CharArray(CharArray::new_row("alpha"))]
1029 )
1030 .is_ok());
1031 assert!(isvarname_value(&Value::String("alpha_1".into())));
1032 assert!(!isvarname_value(&Value::String("1alpha".into())));
1033 }
1034
1035 #[test]
1036 fn namedargs2cell_preserves_field_order() {
1037 let mut st = StructValue::new();
1038 st.insert("Name", Value::String("Ada".into()));
1039 st.insert("Value", Value::Num(7.0));
1040 let out = namedargs2cell_value(Value::Struct(st)).expect("namedargs2cell");
1041 let Value::Cell(cell) = out else {
1042 panic!("expected cell");
1043 };
1044 assert_eq!(cell.rows, 1);
1045 assert_eq!(cell.cols, 4);
1046 assert_eq!(cell.data[0], Value::String("Name".into()));
1047 assert_eq!(cell.data[2], Value::String("Value".into()));
1048 }
1049
1050 #[test]
1051 fn validator_surface_accepts_and_rejects_representative_values() {
1052 let temp_dir = tempfile::tempdir().unwrap();
1053 let file_path = temp_dir.path().join("data.txt");
1054 std::fs::write(&file_path, "ok").unwrap();
1055 let dir_text = Value::String(temp_dir.path().to_string_lossy().into_owned());
1056 let file_text = Value::String(file_path.to_string_lossy().into_owned());
1057
1058 ok(
1059 "mustBeA",
1060 vec![Value::Num(1.0), Value::String("double".into())],
1061 );
1062 err(
1063 "mustBeA",
1064 vec![Value::String("x".into()), Value::String("double".into())],
1065 );
1066 ok("mustBeColumn", vec![tensor(vec![1.0, 2.0], 2, 1)]);
1067 err("mustBeColumn", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1068 ok("mustBeFile", vec![file_text.clone()]);
1069 err("mustBeFile", vec![dir_text.clone()]);
1070 ok("mustBeFolder", vec![dir_text.clone()]);
1071 err("mustBeFolder", vec![file_text.clone()]);
1072 ok("mustBeFinite", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1073 err("mustBeFinite", vec![Value::Num(f64::INFINITY)]);
1074 ok("mustBeFloat", vec![Value::Num(1.0)]);
1075 err("mustBeFloat", vec![Value::Int(IntValue::I32(1))]);
1076 ok("mustBeInteger", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1077 ok("mustBeInteger", vec![Value::Bool(true)]);
1078 ok(
1079 "mustBeInteger",
1080 vec![Value::LogicalArray(
1081 LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1082 )],
1083 );
1084 err("mustBeInteger", vec![Value::Num(1.5)]);
1085 ok("mustBeNumeric", vec![Value::Complex(1.0, 2.0)]);
1086 err("mustBeNumeric", vec![Value::String("1".into())]);
1087 ok(
1088 "mustBeNumericOrLogical",
1089 vec![Value::LogicalArray(
1090 LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1091 )],
1092 );
1093 err("mustBeNumericOrLogical", vec![Value::String("true".into())]);
1094 ok("mustBeReal", vec![Value::Complex(1.0, 0.0)]);
1095 err("mustBeReal", vec![Value::Complex(1.0, 1.0)]);
1096 ok("mustBeVector", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1097 err("mustBeVector", vec![tensor(vec![1.0, 2.0, 3.0, 4.0], 2, 2)]);
1098 ok("mustBeScalarOrEmpty", vec![Value::Num(1.0)]);
1099 err("mustBeScalarOrEmpty", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1100 ok("mustBeSparse", vec![sparse(vec![1.0])]);
1101 err("mustBeSparse", vec![Value::Num(1.0)]);
1102 ok("mustBeNonsparse", vec![Value::Num(1.0)]);
1103 err("mustBeNonsparse", vec![sparse(vec![1.0])]);
1104 ok(
1105 "mustBeText",
1106 vec![Value::CharArray(CharArray::new_row("abc"))],
1107 );
1108 err("mustBeText", vec![Value::Num(1.0)]);
1109 ok("mustBeTextScalar", vec![Value::String("abc".into())]);
1110 err(
1111 "mustBeTextScalar",
1112 vec![Value::StringArray(
1113 StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
1114 )],
1115 );
1116 ok(
1117 "mustBeNonzeroLengthText",
1118 vec![Value::StringArray(
1119 StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
1120 )],
1121 );
1122 err(
1123 "mustBeNonzeroLengthText",
1124 vec![Value::String(String::new())],
1125 );
1126 ok("mustBeNonempty", vec![Value::String("x".into())]);
1127 err(
1128 "mustBeNonempty",
1129 vec![Value::StringArray(
1130 StringArray::new_2d(vec![], 0, 0).unwrap(),
1131 )],
1132 );
1133 ok("mustBeNonmissing", vec![Value::Num(1.0)]);
1134 err("mustBeNonmissing", vec![Value::Num(f64::NAN)]);
1135 ok("mustBeNonNan", vec![Value::Complex(1.0, 0.0)]);
1136 err("mustBeNonNan", vec![Value::Complex(f64::NAN, 0.0)]);
1137 ok(
1138 "mustBeUnderlyingType",
1139 vec![Value::Int(IntValue::I16(1)), Value::String("int16".into())],
1140 );
1141 err(
1142 "mustBeUnderlyingType",
1143 vec![Value::Bool(true), Value::String("double".into())],
1144 );
1145 ok(
1146 "mustBeValidVariableName",
1147 vec![Value::String("alpha_1".into())],
1148 );
1149 err(
1150 "mustBeValidVariableName",
1151 vec![Value::String("_alpha".into())],
1152 );
1153 ok(
1154 "mustBeMember",
1155 vec![
1156 Value::String("on".into()),
1157 Value::Cell(
1158 CellArray::new(
1159 vec![Value::String("on".into()), Value::String("off".into())],
1160 1,
1161 2,
1162 )
1163 .unwrap(),
1164 ),
1165 ],
1166 );
1167 err(
1168 "mustBeMember",
1169 vec![
1170 Value::String("bad".into()),
1171 Value::Cell(
1172 CellArray::new(
1173 vec![Value::String("on".into()), Value::String("off".into())],
1174 1,
1175 2,
1176 )
1177 .unwrap(),
1178 ),
1179 ],
1180 );
1181 }
1182
1183 #[test]
1184 fn numeric_threshold_validators_cover_boundaries() {
1185 ok("mustBePositive", vec![Value::Num(1.0)]);
1186 ok("mustBePositive", vec![Value::Bool(true)]);
1187 err("mustBePositive", vec![Value::Num(0.0)]);
1188 ok("mustBeNegative", vec![Value::Num(-1.0)]);
1189 err("mustBeNegative", vec![Value::Num(0.0)]);
1190 ok("mustBeNonnegative", vec![Value::Num(0.0)]);
1191 ok(
1192 "mustBeNonnegative",
1193 vec![Value::LogicalArray(
1194 LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1195 )],
1196 );
1197 err("mustBeNonnegative", vec![Value::Num(-1.0)]);
1198 ok("mustBeNonpositive", vec![Value::Num(0.0)]);
1199 err("mustBeNonpositive", vec![Value::Num(1.0)]);
1200 ok("mustBeNonzero", vec![Value::Complex(0.0, 1.0)]);
1201 err("mustBeNonzero", vec![Value::Num(0.0)]);
1202 ok("mustBeGreaterThan", vec![Value::Num(2.0), Value::Num(1.0)]);
1203 err("mustBeGreaterThan", vec![Value::Num(1.0), Value::Num(1.0)]);
1204 ok(
1205 "mustBeGreaterThanOrEqual",
1206 vec![Value::Num(1.0), Value::Num(1.0)],
1207 );
1208 err(
1209 "mustBeGreaterThanOrEqual",
1210 vec![Value::Num(0.0), Value::Num(1.0)],
1211 );
1212 ok("mustBeLessThan", vec![Value::Num(0.0), Value::Num(1.0)]);
1213 err("mustBeLessThan", vec![Value::Num(1.0), Value::Num(1.0)]);
1214 ok(
1215 "mustBeLessThanOrEqual",
1216 vec![Value::Num(1.0), Value::Num(1.0)],
1217 );
1218 err(
1219 "mustBeLessThanOrEqual",
1220 vec![Value::Num(2.0), Value::Num(1.0)],
1221 );
1222 }
1223
1224 #[test]
1225 fn in_range_supports_interval_flags_and_rejects_extra_inputs() {
1226 ok(
1227 "mustBeInRange",
1228 vec![Value::Num(1.0), Value::Num(1.0), Value::Num(2.0)],
1229 );
1230 err(
1231 "mustBeInRange",
1232 vec![
1233 Value::Num(1.0),
1234 Value::Num(1.0),
1235 Value::Num(2.0),
1236 Value::String("exclusive".into()),
1237 ],
1238 );
1239 ok(
1240 "mustBeInRange",
1241 vec![
1242 Value::Num(1.5),
1243 Value::Num(1.0),
1244 Value::Num(2.0),
1245 Value::String("exclusive".into()),
1246 ],
1247 );
1248 err(
1249 "mustBeInRange",
1250 vec![
1251 Value::Num(1.0),
1252 Value::Num(1.0),
1253 Value::Num(2.0),
1254 Value::String("exclusive".into()),
1255 Value::String("inclusive".into()),
1256 ],
1257 );
1258 ok(
1259 "mustBeInRange",
1260 vec![
1261 Value::Num(2.0),
1262 Value::Num(1.0),
1263 Value::Num(2.0),
1264 Value::String("exclude-lower".into()),
1265 ],
1266 );
1267 err(
1268 "mustBeInRange",
1269 vec![
1270 Value::Num(2.0),
1271 Value::Num(1.0),
1272 Value::Num(2.0),
1273 Value::String("inclusive".into()),
1274 Value::String("exclusive".into()),
1275 Value::String("extra".into()),
1276 ],
1277 );
1278 }
1279
1280 #[test]
1281 fn varname_rules_reject_keywords_underscores_and_overlong_names() {
1282 assert!(isvarname_value(&Value::String("alpha_1".into())));
1283 assert!(!isvarname_value(&Value::String("_alpha".into())));
1284 assert!(!isvarname_value(&Value::String("1alpha".into())));
1285 assert!(!isvarname_value(&Value::String("for".into())));
1286 assert!(!isvarname_value(&Value::String("end".into())));
1287 assert!(isvarname_value(&Value::String(
1288 "a".repeat(MATLAB_NAME_LENGTH_MAX)
1289 )));
1290 assert!(!isvarname_value(&Value::String(
1291 "a".repeat(MATLAB_NAME_LENGTH_MAX + 1)
1292 )));
1293 }
1294
1295 #[test]
1296 fn callable_validators_reject_unexpected_extra_arguments() {
1297 err("mustBePositive", vec![Value::Num(1.0), Value::Num(2.0)]);
1298 err("mustBeMember", vec![Value::String("on".into())]);
1299 err(
1300 "mustBeA",
1301 vec![
1302 Value::Num(1.0),
1303 Value::String("double".into()),
1304 Value::String("extra".into()),
1305 ],
1306 );
1307 }
1308
1309 #[test]
1310 fn validate_function_signatures_json_checks_json_syntax() {
1311 ok(
1312 "validateFunctionSignaturesJSON",
1313 vec![Value::String(r#"{"functions":[]}"#.into())],
1314 );
1315 err(
1316 "validateFunctionSignaturesJSON",
1317 vec![Value::String("{not json}".into())],
1318 );
1319 }
1320}