vrl 0.32.0

Vector Remap Language
Documentation
use crate::compiler::prelude::*;
use rand::Rng;
use std::ops::Range;

const INVALID_RANGE_ERR: &str = "max must be greater than min";

fn random_float(min: Value, max: Value) -> Resolved {
    let min = min.try_float()?;
    let max = max.try_float()?;

    if max <= min {
        return Err("max must be greater than min".into());
    }

    let f: f64 = rand::rng().random_range(min..max);

    Ok(Value::Float(NotNan::new(f).expect("always a number")))
}

fn get_range(min: Value, max: Value) -> std::result::Result<Range<f64>, &'static str> {
    let min = min.try_float().expect("min must be a float");
    let max = max.try_float().expect("max must be a float");

    if max <= min {
        return Err(INVALID_RANGE_ERR);
    }

    Ok(min..max)
}

#[derive(Clone, Copy, Debug)]
pub struct RandomFloat;

impl Function for RandomFloat {
    fn identifier(&self) -> &'static str {
        "random_float"
    }

    fn usage(&self) -> &'static str {
        "Returns a random float between [min, max)."
    }

    fn category(&self) -> &'static str {
        Category::Random.as_ref()
    }

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &["`max` is not greater than `min`."]
    }

    fn return_kind(&self) -> u16 {
        kind::FLOAT
    }

    fn parameters(&self) -> &'static [Parameter] {
        const PARAMETERS: &[Parameter] = &[
            Parameter::required("min", kind::FLOAT, "Minimum value (inclusive)."),
            Parameter::required("max", kind::FLOAT, "Maximum value (exclusive)."),
        ];
        PARAMETERS
    }

    fn examples(&self) -> &'static [Example] {
        &[example! {
            title: "Random float from 0.0 to 10.0, not including 10.0",
            source: indoc! {"
                f = random_float(0.0, 10.0)
                f >= 0 && f < 10
            "},
            result: Ok("true"),
        }]
    }

    fn compile(
        &self,
        state: &state::TypeState,
        _ctx: &mut FunctionCompileContext,
        arguments: ArgumentList,
    ) -> Compiled {
        let min = arguments.required("min");
        let max = arguments.required("max");

        if let (Some(min), Some(max)) = (min.resolve_constant(state), max.resolve_constant(state)) {
            // check if range is valid
            let _: Range<f64> =
                get_range(min, max.clone()).map_err(|err| function::Error::InvalidArgument {
                    keyword: "max",
                    value: max,
                    error: err,
                })?;
        }

        Ok(RandomFloatFn { min, max }.as_expr())
    }
}

#[derive(Debug, Clone)]
struct RandomFloatFn {
    min: Box<dyn Expression>,
    max: Box<dyn Expression>,
}

impl FunctionExpression for RandomFloatFn {
    fn resolve(&self, ctx: &mut Context) -> Resolved {
        let min = self.min.resolve(ctx)?;
        let max = self.max.resolve(ctx)?;

        random_float(min, max)
    }

    fn type_def(&self, state: &state::TypeState) -> TypeDef {
        match (
            self.min.resolve_constant(state),
            self.max.resolve_constant(state),
        ) {
            (Some(min), Some(max)) => {
                if get_range(min, max).is_ok() {
                    TypeDef::float().infallible()
                } else {
                    TypeDef::float().fallible()
                }
            }
            _ => TypeDef::float().fallible(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value;
    // positive tests are handled by examples

    test_function![
        random_float => RandomFloat;

        bad_range {
            args: func_args![min: value!(1.0), max: value!(1.0)],
            want: Err("invalid argument"),
            tdef: TypeDef::float().fallible(),
        }
    ];
}