Merge branch 'worktree-agent-a25bd4a44da9f00b3'
# Conflicts: # Cargo.lock # Cargo.toml # src/main.rs
This commit is contained in:
Generated
+218
@@ -11,6 +11,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -67,6 +76,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -125,18 +140,47 @@ version = "2.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.12.1"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.6"
|
version = "4.6.6"
|
||||||
@@ -183,6 +227,12 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -193,6 +243,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -298,6 +354,12 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -333,6 +395,30 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -345,6 +431,17 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.189"
|
version = "0.2.189"
|
||||||
@@ -405,6 +502,15 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -422,7 +528,9 @@ name = "ottyl"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"humantime",
|
||||||
"pest",
|
"pest",
|
||||||
"pest_derive",
|
"pest_derive",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -565,6 +673,12 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -643,6 +757,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -853,12 +973,110 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.128"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.128"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.128"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.128"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ clap = { version = "4", features = ["derive"] }
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
pest = "2"
|
pest = "2"
|
||||||
pest_derive = "2"
|
pest_derive = "2"
|
||||||
|
chrono = "0.4"
|
||||||
|
humantime = "2"
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
//! Datetime parsing for the filter query language's timestamp comparisons.
|
||||||
|
//!
|
||||||
|
//! This module is intentionally standalone: it has no dependency on the engine/index
|
||||||
|
//! types or the filter grammar module, so it can be developed, tested, and reused
|
||||||
|
//! independently of the rest of the filter machinery.
|
||||||
|
//!
|
||||||
|
//! Three input forms are supported, all normalized to a single [`Timestamp`]:
|
||||||
|
//!
|
||||||
|
//! 1. Relative humantime, e.g. `5h`, `-3m` — resolved against a supplied "now" at
|
||||||
|
//! parse/eval time (parsed with the `humantime` crate).
|
||||||
|
//! 2. Absolute time-of-day, e.g. `12:00` — resolves to that time on the current day.
|
||||||
|
//! **This module treats "current day" as the UTC calendar day of `now`, and resolves
|
||||||
|
//! the time-of-day in UTC.** (We use `chrono` for date/time parsing since no other
|
||||||
|
//! datetime crate was already a dependency.)
|
||||||
|
//! 3. Full date/datetime literals, e.g. `2024-01-15` or `2024-01-15T12:00:00Z`
|
||||||
|
//! (RFC3339-ish). A bare date is interpreted as midnight UTC on that date.
|
||||||
|
|
||||||
|
// Not yet wired into the filter grammar/engine (a separate integration ticket); allow
|
||||||
|
// dead code so this standalone module builds warning-free until that lands.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
|
||||||
|
|
||||||
|
use crate::timestamp::Timestamp;
|
||||||
|
|
||||||
|
/// Error returned when a datetime filter literal can't be resolved to a [`Timestamp`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum DateTimeParseError {
|
||||||
|
/// The input matched the shape of a supported format (relative duration,
|
||||||
|
/// time-of-day, or date/datetime) but its value was invalid or ambiguous,
|
||||||
|
/// e.g. an out-of-range time like `25:99`.
|
||||||
|
Invalid { input: String, reason: String },
|
||||||
|
/// The input didn't match any supported format at all.
|
||||||
|
Unparseable(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DateTimeParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DateTimeParseError::Invalid { input, reason } => {
|
||||||
|
write!(f, "invalid datetime literal {input:?}: {reason}")
|
||||||
|
}
|
||||||
|
DateTimeParseError::Unparseable(input) => write!(
|
||||||
|
f,
|
||||||
|
"unparseable datetime literal {input:?}: expected a relative duration \
|
||||||
|
(e.g. \"5h\", \"-3m\"), a time-of-day (e.g. \"12:00\"), or a date/datetime \
|
||||||
|
(e.g. \"2024-01-15\" or \"2024-01-15T12:00:00Z\")"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for DateTimeParseError {}
|
||||||
|
|
||||||
|
/// Parses a filter datetime literal into an absolute [`Timestamp`], resolving any
|
||||||
|
/// relative or day-relative forms against `now`.
|
||||||
|
///
|
||||||
|
/// `now` is taken as a parameter (rather than this function calling `Timestamp::now()`
|
||||||
|
/// internally) so callers can pass a fixed value in tests.
|
||||||
|
pub fn parse_datetime(input: &str, now: Timestamp) -> Result<Timestamp, DateTimeParseError> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(DateTimeParseError::Unparseable(input.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = try_relative(trimmed, now) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = try_full_datetime(trimmed) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = try_time_of_day(trimmed, now) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(DateTimeParseError::Unparseable(input.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to interpret `input` as a signed relative humantime duration (`5h`, `-3m`).
|
||||||
|
///
|
||||||
|
/// Returns `None` if `input` doesn't look like a relative duration at all (so the
|
||||||
|
/// caller can try the other formats); returns `Some(Err(..))` if it looks like one but
|
||||||
|
/// the duration itself is malformed.
|
||||||
|
fn try_relative(input: &str, now: Timestamp) -> Option<Result<Timestamp, DateTimeParseError>> {
|
||||||
|
let (sign, rest) = match input.strip_prefix('-') {
|
||||||
|
Some(rest) => (-1i128, rest),
|
||||||
|
None => match input.strip_prefix('+') {
|
||||||
|
Some(rest) => (1i128, rest),
|
||||||
|
None => (1i128, input),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only treat this as an attempted relative duration if it starts with a digit
|
||||||
|
// followed by (eventually) an alphabetic unit -- this keeps dates like
|
||||||
|
// "2024-01-15" (which start with digits but aren't durations) from being
|
||||||
|
// mis-attempted here and reported as "invalid duration" instead of falling
|
||||||
|
// through to the date parser.
|
||||||
|
let looks_like_duration = rest
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c.is_ascii_digit())
|
||||||
|
&& rest.chars().any(|c| c.is_ascii_alphabetic())
|
||||||
|
&& !rest.contains('-')
|
||||||
|
&& !rest.contains(':');
|
||||||
|
if !looks_like_duration {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match humantime::parse_duration(rest) {
|
||||||
|
Ok(duration) => {
|
||||||
|
let nanos = duration.as_nanos() as i128 * sign;
|
||||||
|
let now_nanos = now.as_millis() as i128 * 1_000_000;
|
||||||
|
let result_millis = ((now_nanos + nanos) / 1_000_000) as i64;
|
||||||
|
Some(Ok(Timestamp::from_millis(result_millis)))
|
||||||
|
}
|
||||||
|
Err(err) => Some(Err(DateTimeParseError::Invalid {
|
||||||
|
input: input.to_string(),
|
||||||
|
reason: format!("not a valid relative duration: {err}"),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to interpret `input` as a full date or RFC3339-ish datetime literal.
|
||||||
|
fn try_full_datetime(input: &str) -> Option<Result<Timestamp, DateTimeParseError>> {
|
||||||
|
// Only attempt this path for inputs shaped like a date (contain a '-' separating
|
||||||
|
// digits, e.g. "2024-01-15"); otherwise let other parsers have a shot.
|
||||||
|
let looks_like_date = input.len() >= 8 && input.as_bytes()[4] == b'-';
|
||||||
|
if !looks_like_date {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
|
||||||
|
return Some(Ok(timestamp_from_datetime(dt.with_timezone(&Utc))));
|
||||||
|
}
|
||||||
|
|
||||||
|
match NaiveDate::parse_from_str(input, "%Y-%m-%d") {
|
||||||
|
Ok(date) => {
|
||||||
|
let dt = date.and_hms_opt(0, 0, 0).expect("midnight is always valid");
|
||||||
|
Some(Ok(timestamp_from_datetime(Utc.from_utc_datetime(&dt))))
|
||||||
|
}
|
||||||
|
Err(_) => Some(Err(DateTimeParseError::Invalid {
|
||||||
|
input: input.to_string(),
|
||||||
|
reason: "not a valid date or RFC3339 datetime (expected e.g. \"2024-01-15\" or \
|
||||||
|
\"2024-01-15T12:00:00Z\")"
|
||||||
|
.to_string(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to interpret `input` as a bare time-of-day (`12:00`, `00:00`, `23:59:59`),
|
||||||
|
/// resolved against the UTC calendar day of `now`.
|
||||||
|
fn try_time_of_day(
|
||||||
|
input: &str,
|
||||||
|
now: Timestamp,
|
||||||
|
) -> Option<Result<Timestamp, DateTimeParseError>> {
|
||||||
|
if !input.contains(':') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = NaiveTime::parse_from_str(input, "%H:%M:%S")
|
||||||
|
.or_else(|_| NaiveTime::parse_from_str(input, "%H:%M"));
|
||||||
|
|
||||||
|
let time = match time {
|
||||||
|
Ok(time) => time,
|
||||||
|
Err(err) => {
|
||||||
|
return Some(Err(DateTimeParseError::Invalid {
|
||||||
|
input: input.to_string(),
|
||||||
|
reason: format!("not a valid time-of-day (expected e.g. \"12:00\"): {err}"),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now_millis = now.as_millis();
|
||||||
|
let now_dt = Utc.timestamp_millis_opt(now_millis).single().unwrap_or_else(|| {
|
||||||
|
// Fall back to the epoch date if `now` is somehow out of range; this should
|
||||||
|
// not happen in practice since `Timestamp` always comes from a valid instant.
|
||||||
|
Utc.timestamp_millis_opt(0).single().expect("epoch is valid")
|
||||||
|
});
|
||||||
|
let today = now_dt.date_naive();
|
||||||
|
let dt = Utc.from_utc_datetime(&today.and_time(time));
|
||||||
|
Some(Ok(timestamp_from_datetime(dt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_from_datetime(dt: DateTime<Utc>) -> Timestamp {
|
||||||
|
Timestamp::from_millis(dt.timestamp_millis())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A fixed "now": 2024-06-15T12:34:56Z.
|
||||||
|
fn fixed_now() -> Timestamp {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2024, 6, 15, 12, 34, 56).unwrap();
|
||||||
|
Timestamp::from_millis(dt.timestamp_millis())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_positive_duration() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("5h", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 17, 34, 56).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_negative_duration() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("-3m", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 12, 31, 56).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_explicit_plus() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("+10s", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 12, 35, 6).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_of_day_resolves_to_today() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("12:00", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_of_day_midnight() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("00:00", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 0, 0, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_of_day_end_of_day() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("23:59", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 23, 59, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_of_day_with_seconds() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("23:59:59", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 23, 59, 59).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_date_only() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("2024-01-15", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_datetime_rfc3339() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("2024-01-15T12:00:00Z", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_datetime_rfc3339_with_offset() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let got = parse_datetime("2024-01-15T12:00:00+02:00", now).expect("should parse");
|
||||||
|
let expected = Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
|
||||||
|
assert_eq!(got.as_millis(), expected.timestamp_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_time_of_day_is_an_error() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let err = parse_datetime("25:99", now).expect_err("should not parse");
|
||||||
|
match err {
|
||||||
|
DateTimeParseError::Invalid { .. } => {}
|
||||||
|
other => panic!("expected Invalid, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_date_is_an_error() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let err = parse_datetime("2024-13-40", now).expect_err("should not parse");
|
||||||
|
match err {
|
||||||
|
DateTimeParseError::Invalid { .. } => {}
|
||||||
|
other => panic!("expected Invalid, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totally_unparseable_input_is_an_error() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let err = parse_datetime("not a date at all", now).expect_err("should not parse");
|
||||||
|
match err {
|
||||||
|
DateTimeParseError::Unparseable(_) => {}
|
||||||
|
other => panic!("expected Unparseable, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_input_is_an_error() {
|
||||||
|
let now = fixed_now();
|
||||||
|
let err = parse_datetime("", now).expect_err("should not parse");
|
||||||
|
assert!(matches!(err, DateTimeParseError::Unparseable(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_panic_on_garbage() {
|
||||||
|
let now = fixed_now();
|
||||||
|
for input in ["-", ":", "--", "::", "5", "h5", "T", "-T:", "🎉"] {
|
||||||
|
let _ = parse_datetime(input, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod api;
|
|||||||
mod data_value;
|
mod data_value;
|
||||||
mod dto;
|
mod dto;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod filter_datetime;
|
||||||
mod filter_parser;
|
mod filter_parser;
|
||||||
mod index_key;
|
mod index_key;
|
||||||
mod ingest;
|
mod ingest;
|
||||||
|
|||||||
Reference in New Issue
Block a user