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    unlabeled_first = true,
53    args = {
54        actual = { docs = "Value to check. If this is the boolean value true, assert passes. Otherwise it fails." },
55        error = { docs = "If the value was false, the program will terminate with this error message" },
56    }
57}]
58async fn inner_assert_is(actual: bool, error: Option<String>, args: &Args) -> Result<(), KclError> {
59    let error_msg = match &error {
60        Some(x) => x,
61        None => "should have been true, but it was not",
62    };
63    _assert(actual, error_msg, args).await
64}
65
66/// Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met.
67/// If you provide multiple conditions, they will all be checked and all must be met.
68///
69/// ```no_run
70/// n = 10
71/// assert(n, isEqualTo = 10)
72/// assert(n, isGreaterThanOrEqual = 0, isLessThan = 100, error = "number should be between 0 and 100")
73/// assert(1.0000000000012, isEqualTo = 1, tolerance = 0.0001, error = "number should be almost exactly 1")
74/// ```
75#[stdlib {
76    name = "assert",
77    unlabeled_first = true,
78    args = {
79        actual = { docs = "Value to check. It will be compared with one of the comparison arguments." },
80        is_greater_than = { docs = "Comparison argument. If given, checks the `actual` value is greater than this." },
81        is_less_than = { docs = "Comparison argument. If given, checks the `actual` value is less than this." },
82        is_greater_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is greater than or equal to this." },
83        is_less_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this." },
84        is_equal_to = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this.", include_in_snippet = true },
85        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." },
86        error = { docs = "If the value was false, the program will terminate with this error message" },
87    }
88}]
89#[allow(clippy::too_many_arguments)]
90async fn inner_assert(
91    actual: TyF64,
92    is_greater_than: Option<TyF64>,
93    is_less_than: Option<TyF64>,
94    is_greater_than_or_equal: Option<TyF64>,
95    is_less_than_or_equal: Option<TyF64>,
96    is_equal_to: Option<TyF64>,
97    tolerance: Option<TyF64>,
98    error: Option<String>,
99    args: &Args,
100) -> Result<(), KclError> {
101    // Validate the args
102    let no_condition_given = [
103        &is_greater_than,
104        &is_less_than,
105        &is_greater_than_or_equal,
106        &is_less_than_or_equal,
107        &is_equal_to,
108    ]
109    .iter()
110    .all(|cond| cond.is_none());
111    if no_condition_given {
112        return Err(KclError::Type(KclErrorDetails::new(
113            "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
114            vec![args.source_range],
115        )));
116    }
117
118    if tolerance.is_some() && is_equal_to.is_none() {
119        return Err(KclError::Type(KclErrorDetails::new(
120            "The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
121                .to_owned(),
122            vec![args.source_range],
123        )));
124    }
125
126    let suffix = if let Some(err_string) = error {
127        format!(": {err_string}")
128    } else {
129        Default::default()
130    };
131    let actual = actual.n;
132
133    // Run the checks.
134    if let Some(exp) = is_greater_than {
135        let exp = exp.n;
136        _assert(
137            actual > exp,
138            &format!("Expected {actual} to be greater than {exp} but it wasn't{suffix}"),
139            args,
140        )
141        .await?;
142    }
143    if let Some(exp) = is_less_than {
144        let exp = exp.n;
145        _assert(
146            actual < exp,
147            &format!("Expected {actual} to be less than {exp} but it wasn't{suffix}"),
148            args,
149        )
150        .await?;
151    }
152    if let Some(exp) = is_greater_than_or_equal {
153        let exp = exp.n;
154        _assert(
155            actual >= exp,
156            &format!("Expected {actual} to be greater than or equal to {exp} but it wasn't{suffix}"),
157            args,
158        )
159        .await?;
160    }
161    if let Some(exp) = is_less_than_or_equal {
162        let exp = exp.n;
163        _assert(
164            actual <= exp,
165            &format!("Expected {actual} to be less than or equal to {exp} but it wasn't{suffix}"),
166            args,
167        )
168        .await?;
169    }
170    if let Some(exp) = is_equal_to {
171        let exp = exp.n;
172        let tolerance = tolerance.map(|e| e.n).unwrap_or(0.0000000001);
173        _assert(
174            (actual - exp).abs() < tolerance,
175            &format!("Expected {actual} to be equal to {exp} but it wasn't{suffix}"),
176            args,
177        )
178        .await?;
179    }
180    Ok(())
181}