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::new(
16            format!("assert failed: {}", message),
17            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::new(
115            "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
116            vec![args.source_range],
117        )));
118    }
119
120    if tolerance.is_some() && is_equal_to.is_none() {
121        return Err(KclError::Type(KclErrorDetails::new(
122            "The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
123                .to_owned(),
124            vec![args.source_range],
125        )));
126    }
127
128    let suffix = if let Some(err_string) = error {
129        format!(": {err_string}")
130    } else {
131        Default::default()
132    };
133    let actual = actual.n;
134
135    // Run the checks.
136    if let Some(exp) = is_greater_than {
137        let exp = exp.n;
138        _assert(
139            actual > exp,
140            &format!("Expected {actual} to be greater than {exp} but it wasn't{suffix}"),
141            args,
142        )
143        .await?;
144    }
145    if let Some(exp) = is_less_than {
146        let exp = exp.n;
147        _assert(
148            actual < exp,
149            &format!("Expected {actual} to be less than {exp} but it wasn't{suffix}"),
150            args,
151        )
152        .await?;
153    }
154    if let Some(exp) = is_greater_than_or_equal {
155        let exp = exp.n;
156        _assert(
157            actual >= exp,
158            &format!("Expected {actual} to be greater than or equal to {exp} but it wasn't{suffix}"),
159            args,
160        )
161        .await?;
162    }
163    if let Some(exp) = is_less_than_or_equal {
164        let exp = exp.n;
165        _assert(
166            actual <= exp,
167            &format!("Expected {actual} to be less than or equal to {exp} but it wasn't{suffix}"),
168            args,
169        )
170        .await?;
171    }
172    if let Some(exp) = is_equal_to {
173        let exp = exp.n;
174        let tolerance = tolerance.map(|e| e.n).unwrap_or(0.0000000001);
175        _assert(
176            (actual - exp).abs() < tolerance,
177            &format!("Expected {actual} to be equal to {exp} but it wasn't{suffix}"),
178            args,
179        )
180        .await?;
181    }
182    Ok(())
183}