Skip to content

Commit

Permalink
feat: add from_unix_timestamp function (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
pront committed Jun 27, 2023
1 parent bab67a5 commit 5739fdd
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `ingress_upstreaminfo` log format has been added to `parse_nginx_log` function (https://github.com/vectordotdev/vrl/pull/193)
- fixed type definitions for side-effects inside of queries (https://github.com/vectordotdev/vrl/pull/258)
- replaced `Program::final_type_state` with `Program::final_type_info` to give access to the type definitions of both the target and program result (https://github.com/vectordotdev/vrl/pull/262)
- added `from_unix_timestamp` vrl function (https://github.com/vectordotdev/vrl/pull/277)

## `0.4.0` (2023-05-11)
- consolidated all crates into the root `vrl` crate. The external API stayed the same, with the exception of macros, which are now all exported at the root of the `vrl` crate.
Expand Down
255 changes: 255 additions & 0 deletions src/stdlib/from_unix_timestamp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
use crate::compiler::prelude::*;
use chrono::{TimeZone as _, Utc};
use std::str::FromStr;

fn from_unix_timestamp(value: Value, unit: Unit) -> Resolved {
use Value::Integer;

let value = match value {
Integer(v) => match unit {
Unit::Seconds => {
let t = Utc.timestamp_opt(v, 0).single();
match t {
Some(time) => time.into(),
None => return Err(format!("unable to coerce {v} into timestamp").into()),
}
}
Unit::Milliseconds => {
let t = Utc.timestamp_millis_opt(v).single();
match t {
Some(time) => time.into(),
None => return Err(format!("unable to coerce {v} into timestamp").into()),
}
}
Unit::Nanoseconds => Utc.timestamp_nanos(v).into(),
},
v => return Err(format!("unable to coerce {} into timestamp", v.kind()).into()),
};
Ok(value)
}

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

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

fn parameters(&self) -> &'static [Parameter] {
&[
Parameter {
keyword: "value",
kind: kind::INTEGER,
required: true,
},
Parameter {
keyword: "unit",
kind: kind::BYTES,
required: false,
},
]
}

fn examples(&self) -> &'static [Example] {
&[
Example {
title: "integer as seconds",
source: "to_timestamp!(5)",
result: Ok("t'1970-01-01T00:00:05Z'"),
},
Example {
title: "integer as milliseconds",
source: r#"to_timestamp!(5000, unit: "milliseconds")"#,
result: Ok("t'1970-01-01T00:00:05Z'"),
},
Example {
title: "integer as nanoseconds",
source: r#"from_unix_timestamp!(5000, unit: "nanoseconds")"#,
result: Ok("t'1970-01-01T00:00:00.000005Z'"),
},
]
}

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

let unit = arguments
.optional_enum("unit", Unit::all_value().as_slice())?
.map(|s| {
Unit::from_str(&s.try_bytes_utf8_lossy().expect("unit not bytes"))
.expect("validated enum")
})
.unwrap_or_default();

Ok(FromUnixTimestampFn { value, unit }.as_expr())
}
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
enum Unit {
#[default]
Seconds,
Milliseconds,
Nanoseconds,
}

impl Unit {
fn all_value() -> Vec<Value> {
use Unit::{Milliseconds, Nanoseconds, Seconds};

vec![Seconds, Milliseconds, Nanoseconds]
.into_iter()
.map(|u| u.as_str().into())
.collect::<Vec<_>>()
}

const fn as_str(self) -> &'static str {
use Unit::{Milliseconds, Nanoseconds, Seconds};

match self {
Seconds => "seconds",
Milliseconds => "milliseconds",
Nanoseconds => "nanoseconds",
}
}
}

impl FromStr for Unit {
type Err = &'static str;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
use Unit::{Milliseconds, Nanoseconds, Seconds};

match s {
"seconds" => Ok(Seconds),
"milliseconds" => Ok(Milliseconds),
"nanoseconds" => Ok(Nanoseconds),
_ => Err("unit not recognized"),
}
}
}

#[derive(Debug, Clone)]
struct FromUnixTimestampFn {
value: Box<dyn Expression>,
unit: Unit,
}

impl FunctionExpression for FromUnixTimestampFn {
fn resolve(&self, ctx: &mut Context) -> Resolved {
let value = self.value.resolve(ctx)?;
let unit = self.unit;
from_unix_timestamp(value, unit)
}

fn type_def(&self, _state: &state::TypeState) -> TypeDef {
TypeDef::timestamp().fallible()
}
}

#[cfg(test)]
#[allow(overflowing_literals)]
mod tests {
use super::*;
use crate::compiler::expression::Literal;
use crate::compiler::TimeZone;
use crate::value;
use regex::Regex;
use std::collections::BTreeMap;

#[test]
fn out_of_range_integer() {
let mut object: Value = BTreeMap::new().into();
let mut runtime_state = state::RuntimeState::default();
let tz = TimeZone::default();
let mut ctx = Context::new(&mut object, &mut runtime_state, &tz);
let f = FromUnixTimestampFn {
value: Box::new(Literal::Integer(9_999_999_999_999)),
unit: Unit::default(),
};
let string = f.resolve(&mut ctx).err().unwrap().message();
assert_eq!(string, r#"unable to coerce 9999999999999 into timestamp"#)
}

test_function![
from_unix_timestamp => FromUnixTimestamp;

integer {
args: func_args![value: 1_431_648_000],
want: Ok(chrono::Utc.ymd(2015, 5, 15).and_hms_opt(0, 0, 0).expect("invalid timestamp")),
tdef: TypeDef::timestamp().fallible(),
}

integer_seconds {
args: func_args![value: 1_609_459_200_i64, unit: "seconds"],
want: Ok(chrono::Utc.ymd(2021, 1, 1).and_hms_milli(0,0,0,0)),
tdef: TypeDef::timestamp().fallible(),
}

integer_milliseconds {
args: func_args![value: 1_609_459_200_000_i64, unit: "milliseconds"],
want: Ok(chrono::Utc.ymd(2021, 1, 1).and_hms_milli(0,0,0,0)),
tdef: TypeDef::timestamp().fallible(),
}

integer_nanoseconds {
args: func_args![value: 1_609_459_200_000_000_000_i64, unit: "nanoseconds"],
want: Ok(chrono::Utc.ymd(2021, 1, 1).and_hms_milli(0,0,0,0)),
tdef: TypeDef::timestamp().fallible(),
}

float_type_invalid {
args: func_args![value: 5.123],
want: Err("unable to coerce float into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

float_type_invalid_milliseconds {
args: func_args![value: 5.123, unit: "milliseconds"],
want: Err("unable to coerce float into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

timestamp_type_invalid {
args: func_args![value: chrono::Utc.ymd(2021, 1, 1).and_hms_milli(0,0,0,0)],
want: Err("unable to coerce timestamp into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

boolean_type_invalid {
args: func_args![value: true],
want: Err("unable to coerce boolean into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

null_type_invalid {
args: func_args![value: value!(null)],
want: Err("unable to coerce null into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

array_type_invalid {
args: func_args![value: value!([])],
want: Err("unable to coerce array into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

object_type_invalid {
args: func_args![value: value!({})],
want: Err("unable to coerce object into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}

regex_type_invalid {
args: func_args![value: value!(Regex::new(r"\d+").unwrap())],
want: Err("unable to coerce regex into timestamp"),
tdef: TypeDef::timestamp().fallible(),
}
];
}
4 changes: 4 additions & 0 deletions src/stdlib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

mod util;
mod wasm_unsupported_function;

use crate::compiler::Function;
pub use wasm_unsupported_function::WasmUnsupportedFunction;

Expand Down Expand Up @@ -75,6 +76,7 @@ cfg_if::cfg_if! {
mod format_int;
mod format_number;
mod format_timestamp;
mod from_unix_timestamp;
mod get;
mod get_env_var;
mod get_hostname;
Expand Down Expand Up @@ -234,6 +236,7 @@ cfg_if::cfg_if! {
pub use format_int::FormatInt;
pub use format_number::FormatNumber;
pub use format_timestamp::FormatTimestamp;
pub use from_unix_timestamp::FromUnixTimestamp;
pub use get::Get;
pub use get_env_var::GetEnvVar;
pub use get_hostname::GetHostname;
Expand Down Expand Up @@ -396,6 +399,7 @@ pub fn all() -> Vec<Box<dyn Function>> {
Box::new(FormatInt),
Box::new(FormatNumber),
Box::new(FormatTimestamp),
Box::new(FromUnixTimestamp),
Box::new(Get),
Box::new(GetEnvVar),
Box::new(GetHostname),
Expand Down

0 comments on commit 5739fdd

Please sign in to comment.