kcl_lib/std/
assert.rs

1//! Standard library assert functions.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5
6use super::args::TyF64;
7use crate::{
8    errors::{KclError, KclErrorDetails},
9    execution::{ExecState, KclValue},
10    std::Args,
11};
12
13async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
14    if !value {
15        return Err(KclError::Type(KclErrorDetails {
16            message: format!("assert failed: {}", message),
17            source_ranges: vec![args.source_range],
18        }));
19    }
20    Ok(())
21}
22
23pub async fn assert_is(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
24    let actual = args.get_unlabeled_kw_arg("actual")?;
25    let error = args.get_kw_arg_opt("error")?;
26    inner_assert_is(actual, error, &args).await?;
27    Ok(KclValue::none())
28}
29
30/// Check that the provided value is true, or raise a [KclError]
31/// with the provided description.
32pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33    let actual = args.get_unlabeled_kw_arg("actual")?;
34    let gt = args.get_kw_arg_opt("isGreaterThan")?;
35    let lt = args.get_kw_arg_opt("isLessThan")?;
36    let gte = args.get_kw_arg_opt("isGreaterThanOrEqual")?;
37    let lte = args.get_kw_arg_opt("isLessThanOrEqual")?;
38    let eq = args.get_kw_arg_opt("isEqualTo")?;
39    let tolerance = args.get_kw_arg_opt("tolerance")?;
40    let error = args.get_kw_arg_opt("error")?;
41    inner_assert(actual, gt, lt, gte, lte, eq, tolerance, error, &args).await?;
42    Ok(KclValue::none())
43}
44
45/// Asserts that a value is the boolean value true.
46/// ```no_run
47/// kclIsFun = true
48/// assertIs(kclIsFun)
49/// ```
50#[stdlib{
51    name = "assertIs",
52    keywords = true,
53    unlabeled_first = true,
54    args = {
55        actual = { docs = "Value to check. If this is the boolean value true, assert passes. Otherwise it fails." },
56        error = { docs = "If the value was false, the program will terminate with this error message" },
57    }
58}]
59async fn inner_assert_is(actual: bool, error: Option<String>, args: &Args) -> Result<(), KclError> {
60    let error_msg = match &error {
61        Some(x) => x,
62        None => "should have been true, but it was not",
63    };
64    _assert(actual, error_msg, args).await
65}
66
67/// Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met.
68/// If you provide multiple conditions, they will all be checked and all must be met.
69///
70/// ```no_run
71/// n = 10
72/// assert(n, isEqualTo = 10)
73/// assert(n, isGreaterThanOrEqual = 0, isLessThan = 100, error = "number should be between 0 and 100")
74/// assert(1.0000000000012, isEqualTo = 1, tolerance = 0.0001, error = "number should be almost exactly 1")
75/// ```
76#[stdlib {
77    name = "assert",
78    keywords = true,
79    unlabeled_first = true,
80    args = {
81        actual = { docs = "Value to check. It will be compared with one of the comparison arguments." },
82        is_greater_than = { docs = "Comparison argument. If given, checks the `actual` value is greater than this." },
83        is_less_than = { docs = "Comparison argument. If given, checks the `actual` value is less than this." },
84        is_greater_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is greater than or equal to this." },
85        is_less_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this." },
86        is_equal_to = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this.", include_in_snippet = true },
87        tolerance = { docs = "If `isEqualTo` is used, this is the tolerance to allow for the comparison. This tolerance is used because KCL's number system has some floating-point imprecision when used with very large decimal places." },
88        error = { docs = "If the value was false, the program will terminate with this error message" },
89    }
90}]
91#[allow(clippy::too_many_arguments)]
92async fn inner_assert(
93    actual: TyF64,
94    is_greater_than: Option<TyF64>,
95    is_less_than: Option<TyF64>,
96    is_greater_than_or_equal: Option<TyF64>,
97    is_less_than_or_equal: Option<TyF64>,
98    is_equal_to: Option<TyF64>,
99    tolerance: Option<TyF64>,
100    error: Option<String>,
101    args: &Args,
102) -> Result<(), KclError> {
103    // Validate the args
104    let no_condition_given = [
105        &is_greater_than,
106        &is_less_than,
107        &is_greater_than_or_equal,
108        &is_less_than_or_equal,
109        &is_equal_to,
110    ]
111    .iter()
112    .all(|cond| cond.is_none());
113    if no_condition_given {
114        return Err(KclError::Type(KclErrorDetails {
115            message: "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
116            source_ranges: vec![args.source_range],
117        }));
118    }
119
120    if tolerance.is_some() && is_equal_to.is_none() {
121        return Err(KclError::Type(KclErrorDetails {
122            message:
123                "The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
124                    .to_owned(),
125            source_ranges: vec![args.source_range],
126        }));
127    }
128
129    let suffix = if let Some(err_string) = error {
130        format!(": {err_string}")
131    } else {
132        Default::default()
133    };
134    let actual = actual.n;
135
136    // Run the checks.
137    if let Some(exp) = is_greater_than {
138        let exp = exp.n;
139        _assert(
140            actual > exp,
141            &format!("Expected {actual} to be greater than {exp} but it wasn't{suffix}"),
142            args,
143        )
144        .await?;
145    }
146    if let Some(exp) = is_less_than {
147        let exp = exp.n;
148        _assert(
149            actual < exp,
150            &format!("Expected {actual} to be less than {exp} but it wasn't{suffix}"),
151            args,
152        )
153        .await?;
154    }
155    if let Some(exp) = is_greater_than_or_equal {
156        let exp = exp.n;
157        _assert(
158            actual >= exp,
159            &format!("Expected {actual} to be greater than or equal to {exp} but it wasn't{suffix}"),
160            args,
161        )
162        .await?;
163    }
164    if let Some(exp) = is_less_than_or_equal {
165        let exp = exp.n;
166        _assert(
167            actual <= exp,
168            &format!("Expected {actual} to be less than or equal to {exp} but it wasn't{suffix}"),
169            args,
170        )
171        .await?;
172    }
173    if let Some(exp) = is_equal_to {
174        let exp = exp.n;
175        let tolerance = tolerance.map(|e| e.n).unwrap_or(0.0000000001);
176        _assert(
177            (actual - exp).abs() < tolerance,
178            &format!("Expected {actual} to be equal to {exp} but it wasn't{suffix}"),
179            args,
180        )
181        .await?;
182    }
183    Ok(())
184}