Consolidate backend scanner status, paging, and query-param duplication

Add ActiveStatusCell/PassiveStatusCell wrappers in the scanners common
module so the five per-scanner status.rs files reduce to a single static;
replace parse_parameter_bool/int/string with one generic parse_parameter
over FromStr; extract the shared LIMIT/OFFSET paging clause into
db::apply_paging; and drop a no-op for-loop in the ARP sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-06 09:03:00 -04:00
co-authored by Claude Opus 4.8
parent c022e672d1
commit 8749ff33c1
27 changed files with 189 additions and 260 deletions
+20
View File
@@ -41,6 +41,26 @@ pub fn get_db_connection() -> PooledConnection<SqliteConnectionManager> {
}
}
// Appends the shared `LIMIT ? OFFSET ?` paging clause (and its bound parameters) to a list query
// when both an offset and a limit are supplied. Used by the list endpoints (devices, notifications,
// device_events) so they page identically.
pub fn apply_paging(
sql: &mut String,
params: &mut Vec<rusqlite::types::Value>,
page_offset: Option<i64>,
page_limit: Option<i64>,
) {
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
debug!(
"Adding paging to list with offset={} and limit={}",
page_offset, page_limit
);
sql.push_str(" LIMIT ? OFFSET ?");
params.push(page_limit.into());
params.push(page_offset.into());
}
}
pub async fn init_db() -> Result<(), DbError> {
let mut initialised = INITIALISED.lock().await;
if *initialised {
+1 -9
View File
@@ -89,15 +89,7 @@ pub fn list(
sql_statement.push_str(" ORDER BY created_on DESC, id DESC");
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
debug!(
"Adding paging with offset={} and limit={}",
page_offset, page_limit
);
sql_statement.push_str(" LIMIT ? OFFSET ?");
params.push(page_limit.into());
params.push(page_offset.into());
}
db::apply_paging(&mut sql_statement, &mut params, page_offset, page_limit);
let mut stmt = conn.prepare(sql_statement.as_str())?;
+1 -10
View File
@@ -118,16 +118,7 @@ pub fn list_devices(
));
// Paging
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
debug!(
"Adding paging to list with offset={} and limit={}",
page_offset, page_limit
);
sql_statement.push_str("LIMIT ? OFFSET ?");
params.push(page_limit.into());
params.push(page_offset.into());
};
db::apply_paging(&mut sql_statement, &mut params, page_offset, page_limit);
let mut stmt = conn.prepare(sql_statement.as_str())?;
+1 -10
View File
@@ -32,16 +32,7 @@ pub fn list(
sql_statement.push_str(" ORDER BY created_on DESC");
// Paging
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
debug!(
"Adding paging to list with offset={} and limit={}",
page_offset, page_limit
);
sql_statement.push_str(" LIMIT ? OFFSET ?");
params.push(page_limit.into());
params.push(page_offset.into());
};
db::apply_paging(&mut sql_statement, &mut params, page_offset, page_limit);
let mut stmt = conn.prepare(sql_statement.as_str())?;
+5 -5
View File
@@ -57,11 +57,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
db::init_db().await?;
// Initialize scanner status tracking
scanners::arp::status::init();
scanners::mdns::status::init();
scanners::ssdp::status::init();
scanners::dhcp::status::init();
scanners::snmp::status::init();
scanners::arp::status::STATUS.init();
scanners::mdns::status::STATUS.init();
scanners::ssdp::status::STATUS.init();
scanners::dhcp::status::STATUS.init();
scanners::snmp::status::STATUS.init();
// Start the device scanners, web server, and retention cleaner in parallel
tokio::join!(
+25 -25
View File
@@ -25,35 +25,35 @@ pub async fn send_packet(
continue;
}
trace!("Sending ARP packet to {}", target_ip);
for _ in 0..1 {
//arp packet
let mut arp_buf = [0u8; 28];
let mut arp_packet = MutableArpPacket::new(&mut arp_buf).unwrap();
arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet);
arp_packet.set_protocol_type(EtherTypes::Ipv4);
arp_packet.set_hw_addr_len(6);
arp_packet.set_operation(ArpOperations::Request);
arp_packet.set_proto_addr_len(4);
arp_packet.set_sender_hw_addr(sender_macaddr);
arp_packet.set_sender_proto_addr(sender_ip.ip());
arp_packet.set_target_hw_addr(MacAddr::zero());
arp_packet.set_target_proto_addr(target_ip);
//arp packet
let mut arp_buf = [0u8; 28];
let mut arp_packet = MutableArpPacket::new(&mut arp_buf).unwrap();
//ethernet packet
let mut ethernet_buf = [0u8; 42];
let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buf).unwrap();
arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet);
arp_packet.set_protocol_type(EtherTypes::Ipv4);
arp_packet.set_hw_addr_len(6);
arp_packet.set_operation(ArpOperations::Request);
arp_packet.set_proto_addr_len(4);
arp_packet.set_sender_hw_addr(sender_macaddr);
arp_packet.set_sender_proto_addr(sender_ip.ip());
arp_packet.set_target_hw_addr(MacAddr::zero());
arp_packet.set_target_proto_addr(target_ip);
ethernet_packet.set_destination(MacAddr::broadcast());
ethernet_packet.set_source(sender_macaddr);
ethernet_packet.set_ethertype(EtherTypes::Arp);
ethernet_packet.set_payload(arp_packet.packet_mut());
//ethernet packet
let mut ethernet_buf = [0u8; 42];
let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buf).unwrap();
ethernet_packet.set_destination(MacAddr::broadcast());
ethernet_packet.set_source(sender_macaddr);
ethernet_packet.set_ethertype(EtherTypes::Arp);
ethernet_packet.set_payload(arp_packet.packet_mut());
tx.send_to(
ethernet_packet.to_immutable().packet(),
Some(interface.clone()),
);
tx.send_to(
ethernet_packet.to_immutable().packet(),
Some(interface.clone()),
);
}
count += 1;
// Sleep 1 millisecond every 255 packets
if (count % 255) == 0 {
+3 -3
View File
@@ -14,14 +14,14 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
}
loop {
status::set_running();
status::STATUS.set_running();
// Find online devices via ARP
let devices = finder::find(get_settings().networking.interface.clone()).await?;
info!("Done with ARP probes");
info!("Found {} online devices", devices.len());
status::record_scan(&devices);
status::STATUS.record_scan(&devices);
// Process found devices
for device in devices.iter() {
@@ -35,7 +35,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
"Scan finished. Sleeping for {}",
get_settings().arp_scanner.wait_between_scans
);
status::set_waiting(next_scan_at);
status::STATUS.set_waiting(next_scan_at);
sleep(wait).await;
}
}
+2 -33
View File
@@ -1,34 +1,3 @@
use chrono::{DateTime, Utc};
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::scanners::common::active_status::ActiveStatusCell;
use crate::model::devices::Device;
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
pub fn init() {
STATUS.set(Mutex::new(ActiveStatus::new())).ok();
}
pub fn set_running() {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_running();
}
}
pub fn set_waiting(next_scan_at: DateTime<Utc>) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_waiting(next_scan_at);
}
}
pub fn record_scan(devices: &[Device]) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_scan(devices);
}
}
pub fn get() -> Option<ActiveSnapshot> {
STATUS.get().map(|m| m.lock().unwrap().snapshot())
}
pub static STATUS: ActiveStatusCell = ActiveStatusCell::new();
@@ -1,6 +1,8 @@
use crate::model::devices::Device;
use chrono::{DateTime, Utc};
use once_cell::sync::OnceCell;
use std::collections::HashSet;
use std::sync::Mutex;
/// Status state for the active (polling) scanners — ARP and SNMP. Each scanner owns its own
/// `OnceCell<Mutex<ActiveStatus>>` and delegates to these methods (see e.g.
@@ -67,6 +69,50 @@ impl ActiveStatus {
}
}
/// A lazily-initialised, mutex-guarded [`ActiveStatus`] owned by a single active scanner. Each
/// scanner declares one as a `static` and the API layer reads it back via [`get`](Self::get).
/// All methods are no-ops until [`init`](Self::init) is called (mirroring the previous
/// per-scanner `OnceCell` behaviour).
pub struct ActiveStatusCell(OnceCell<Mutex<ActiveStatus>>);
impl ActiveStatusCell {
pub const fn new() -> Self {
Self(OnceCell::new())
}
pub fn init(&self) {
self.0.set(Mutex::new(ActiveStatus::new())).ok();
}
pub fn set_running(&self) {
if let Some(m) = self.0.get() {
m.lock().unwrap().set_running();
}
}
pub fn set_waiting(&self, next_scan_at: DateTime<Utc>) {
if let Some(m) = self.0.get() {
m.lock().unwrap().set_waiting(next_scan_at);
}
}
pub fn record_scan(&self, devices: &[Device]) {
if let Some(m) = self.0.get() {
m.lock().unwrap().record_scan(devices);
}
}
pub fn get(&self) -> Option<ActiveSnapshot> {
self.0.get().map(|m| m.lock().unwrap().snapshot())
}
}
impl Default for ActiveStatusCell {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1,6 +1,8 @@
use std::collections::HashMap;
use std::sync::Mutex;
use chrono::{DateTime, Duration, Utc};
use once_cell::sync::OnceCell;
/// A device counts as "seen" only if its most recent sighting falls within this rolling window.
const RECENT_WINDOW_SECONDS: i64 = 3600;
@@ -58,6 +60,44 @@ impl PassiveStatus {
}
}
/// A lazily-initialised, mutex-guarded [`PassiveStatus`] owned by a single passive scanner. Each
/// scanner declares one as a `static` and the API layer reads it back via [`get`](Self::get).
/// All methods are no-ops until [`init`](Self::init) is called (mirroring the previous
/// per-scanner `OnceCell` behaviour).
pub struct PassiveStatusCell(OnceCell<Mutex<PassiveStatus>>);
impl PassiveStatusCell {
pub const fn new() -> Self {
Self(OnceCell::new())
}
pub fn init(&self) {
self.0.set(Mutex::new(PassiveStatus::new())).ok();
}
pub fn set_listening(&self) {
if let Some(m) = self.0.get() {
m.lock().unwrap().set_listening();
}
}
pub fn record_discovery(&self, mac: &str) {
if let Some(m) = self.0.get() {
m.lock().unwrap().record_discovery(mac);
}
}
pub fn get(&self) -> Option<PassiveSnapshot> {
self.0.get().map(|m| m.lock().unwrap().snapshot())
}
}
impl Default for PassiveStatusCell {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
+2 -2
View File
@@ -18,7 +18,7 @@ pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
}
let socket = finder::open_socket()?;
status::set_listening();
status::STATUS.set_listening();
info!("DHCP scanner listening for client requests");
let mut buf = [0u8; 1500];
@@ -54,5 +54,5 @@ fn process_discovery(discovery: finder::DhcpDiscovery) {
let device = build_device(mac.clone(), ipv4, &[], discovery.hostname);
pipeline::record_sighting(device, DeviceEventScanner::Dhcp);
status::record_discovery(&mac);
status::STATUS.record_discovery(&mac);
}
+2 -25
View File
@@ -1,26 +1,3 @@
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::scanners::common::passive_status::PassiveStatusCell;
use crate::scanners::common::passive_status::{PassiveSnapshot, PassiveStatus};
static STATUS: OnceCell<Mutex<PassiveStatus>> = OnceCell::new();
pub fn init() {
STATUS.set(Mutex::new(PassiveStatus::new())).ok();
}
pub fn set_listening() {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_listening();
}
}
pub fn record_discovery(mac: &str) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_discovery(mac);
}
}
pub fn get() -> Option<PassiveSnapshot> {
STATUS.get().map(|m| m.lock().unwrap().snapshot())
}
pub static STATUS: PassiveStatusCell = PassiveStatusCell::new();
+2 -2
View File
@@ -20,7 +20,7 @@ pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
let interface = get_settings().networking.interface.clone();
let socket = finder::open_socket(interface.clone())?;
status::set_listening();
status::STATUS.set_listening();
info!("mDNS scanner listening for announcements");
let probe_timeout = Duration::from(get_settings().mdns_scanner.probe_timeout);
@@ -83,5 +83,5 @@ async fn process_announcement(
);
pipeline::record_sighting(device, DeviceEventScanner::Mdns);
status::record_discovery(&mac);
status::STATUS.record_discovery(&mac);
}
+2 -25
View File
@@ -1,26 +1,3 @@
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::scanners::common::passive_status::PassiveStatusCell;
use crate::scanners::common::passive_status::{PassiveSnapshot, PassiveStatus};
static STATUS: OnceCell<Mutex<PassiveStatus>> = OnceCell::new();
pub fn init() {
STATUS.set(Mutex::new(PassiveStatus::new())).ok();
}
pub fn set_listening() {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_listening();
}
}
pub fn record_discovery(mac: &str) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_discovery(mac);
}
}
pub fn get() -> Option<PassiveSnapshot> {
STATUS.get().map(|m| m.lock().unwrap().snapshot())
}
pub static STATUS: PassiveStatusCell = PassiveStatusCell::new();
+3 -3
View File
@@ -25,14 +25,14 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
info!("SNMP scanner polling agent at {}", config.target);
loop {
status::set_running();
status::STATUS.set_running();
// A failed poll (unreachable agent, timeout, bad community) must not stop the loop;
// log it and try again next cycle.
match finder::find(config).await {
Ok(devices) => {
info!("SNMP poll found {} devices in the ARP cache", devices.len());
status::record_scan(&devices);
status::STATUS.record_scan(&devices);
for device in devices.iter() {
pipeline::record_sighting(device.clone(), DeviceEventScanner::Snmp);
}
@@ -46,7 +46,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
"SNMP scan finished. Sleeping for {}",
config.wait_between_scans
);
status::set_waiting(next_scan_at);
status::STATUS.set_waiting(next_scan_at);
sleep(wait).await;
}
}
+2 -33
View File
@@ -1,34 +1,3 @@
use chrono::{DateTime, Utc};
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::scanners::common::active_status::ActiveStatusCell;
use crate::model::devices::Device;
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
pub fn init() {
STATUS.set(Mutex::new(ActiveStatus::new())).ok();
}
pub fn set_running() {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_running();
}
}
pub fn set_waiting(next_scan_at: DateTime<Utc>) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_waiting(next_scan_at);
}
}
pub fn record_scan(devices: &[Device]) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_scan(devices);
}
}
pub fn get() -> Option<ActiveSnapshot> {
STATUS.get().map(|m| m.lock().unwrap().snapshot())
}
pub static STATUS: ActiveStatusCell = ActiveStatusCell::new();
+2 -2
View File
@@ -20,7 +20,7 @@ pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
let interface = get_settings().networking.interface.clone();
let socket = finder::open_socket(interface.clone())?;
status::set_listening();
status::STATUS.set_listening();
info!("SSDP scanner listening for announcements");
let probe_timeout = Duration::from(get_settings().ssdp_scanner.probe_timeout);
@@ -77,5 +77,5 @@ async fn process_announcement(
let device = build_device(mac.clone(), src_ip.to_string(), &device_types, server_hint);
pipeline::record_sighting(device, DeviceEventScanner::Ssdp);
status::record_discovery(&mac);
status::STATUS.record_discovery(&mac);
}
+2 -25
View File
@@ -1,26 +1,3 @@
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::scanners::common::passive_status::PassiveStatusCell;
use crate::scanners::common::passive_status::{PassiveSnapshot, PassiveStatus};
static STATUS: OnceCell<Mutex<PassiveStatus>> = OnceCell::new();
pub fn init() {
STATUS.set(Mutex::new(PassiveStatus::new())).ok();
}
pub fn set_listening() {
if let Some(m) = STATUS.get() {
m.lock().unwrap().set_listening();
}
}
pub fn record_discovery(mac: &str) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_discovery(mac);
}
}
pub fn get() -> Option<PassiveSnapshot> {
STATUS.get().map(|m| m.lock().unwrap().snapshot())
}
pub static STATUS: PassiveStatusCell = PassiveStatusCell::new();
+1 -1
View File
@@ -13,5 +13,5 @@ use crate::web_server::scanner_status::{ActiveScannerStatusResponse, active_resp
security(("bearer_auth" = []))
)]
pub async fn status() -> Result<Json<ActiveScannerStatusResponse>, StatusCode> {
active_response(crate::scanners::arp::status::get()).map(Json)
active_response(crate::scanners::arp::status::STATUS.get()).map(Json)
}
+2 -2
View File
@@ -30,8 +30,8 @@ pub async fn list(
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<Vec<DeviceEvent>>, StatusCode> {
let created_from = utils::parse_parameter_date(&params, "created_from");
let page_offset: Option<i64> = utils::parse_parameter_int(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter_int(&params, "page_limit");
let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit");
match db::device_events::list(Some(mac_address), created_from, page_offset, page_limit) {
Ok(value) => Ok(Json(value)),
+8 -8
View File
@@ -40,17 +40,17 @@ use crate::web_server::utils;
pub async fn list(
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<DeviceListResponse>, StatusCode> {
let is_registered: Option<bool> = utils::parse_parameter_bool(&params, "is_registered");
let is_registered: Option<bool> = utils::parse_parameter(&params, "is_registered");
let last_seen_from: Option<DateTime<Utc>> =
utils::parse_parameter_date(&params, "last_seen_from");
let last_seen_to: Option<DateTime<Utc>> = utils::parse_parameter_date(&params, "last_seen_to");
let owner: Option<String> = utils::parse_parameter_string(&params, "owner");
let device_type: Option<String> = utils::parse_parameter_string(&params, "device_type");
let vendor: Option<String> = utils::parse_parameter_string(&params, "vendor");
let sort_by: Option<String> = utils::parse_parameter_string(&params, "sort_by");
let sort_order: Option<String> = utils::parse_parameter_string(&params, "sort_order");
let page_offset: Option<i64> = utils::parse_parameter_int(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter_int(&params, "page_limit");
let owner: Option<String> = utils::parse_parameter(&params, "owner");
let device_type: Option<String> = utils::parse_parameter(&params, "device_type");
let vendor: Option<String> = utils::parse_parameter(&params, "vendor");
let sort_by: Option<String> = utils::parse_parameter(&params, "sort_by");
let sort_order: Option<String> = utils::parse_parameter(&params, "sort_order");
let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit");
let items = match db::devices::list_devices(
is_registered,
+1 -1
View File
@@ -13,5 +13,5 @@ use crate::web_server::scanner_status::{PassiveScannerStatusResponse, passive_re
security(("bearer_auth" = []))
)]
pub async fn status() -> Result<Json<PassiveScannerStatusResponse>, StatusCode> {
passive_response(crate::scanners::dhcp::status::get()).map(Json)
passive_response(crate::scanners::dhcp::status::STATUS.get()).map(Json)
}
+1 -1
View File
@@ -13,5 +13,5 @@ use crate::web_server::scanner_status::{PassiveScannerStatusResponse, passive_re
security(("bearer_auth" = []))
)]
pub async fn status() -> Result<Json<PassiveScannerStatusResponse>, StatusCode> {
passive_response(crate::scanners::mdns::status::get()).map(Json)
passive_response(crate::scanners::mdns::status::STATUS.get()).map(Json)
}
+3 -3
View File
@@ -130,9 +130,9 @@ pub async fn mark_all_as_old() -> impl IntoResponse {
pub async fn list(
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<NotificationListResponse>, StatusCode> {
let is_new: Option<bool> = utils::parse_parameter_bool(&params, "is_new");
let page_offset: Option<i64> = utils::parse_parameter_int(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter_int(&params, "page_limit");
let is_new: Option<bool> = utils::parse_parameter(&params, "is_new");
let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit");
let items = match db::notifications::list(is_new, page_offset, page_limit) {
Ok(value) => value,
+1 -1
View File
@@ -13,5 +13,5 @@ use crate::web_server::scanner_status::{ActiveScannerStatusResponse, active_resp
security(("bearer_auth" = []))
)]
pub async fn status() -> Result<Json<ActiveScannerStatusResponse>, StatusCode> {
active_response(crate::scanners::snmp::status::get()).map(Json)
active_response(crate::scanners::snmp::status::STATUS.get()).map(Json)
}
+1 -1
View File
@@ -13,5 +13,5 @@ use crate::web_server::scanner_status::{PassiveScannerStatusResponse, passive_re
security(("bearer_auth" = []))
)]
pub async fn status() -> Result<Json<PassiveScannerStatusResponse>, StatusCode> {
passive_response(crate::scanners::ssdp::status::get()).map(Json)
passive_response(crate::scanners::ssdp::status::STATUS.get()).map(Json)
}
+10 -30
View File
@@ -1,41 +1,21 @@
use std::collections::HashMap;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use log::{debug, warn};
pub fn parse_parameter_bool(params: &HashMap<String, String>, name: &str) -> Option<bool> {
if params.contains_key(name) {
let param_value = params.get(name).unwrap().as_str();
debug!("Found parameter {name} with value {}", param_value);
param_value.parse::<bool>().ok()
} else {
None
}
}
pub fn parse_parameter_int(params: &HashMap<String, String>, name: &str) -> Option<i64> {
if params.contains_key(name) {
let param_value = params.get(name).unwrap().as_str();
debug!("Found parameter {name} with value {}", param_value);
param_value.parse::<i64>().ok()
} else {
None
}
}
pub fn parse_parameter_string(params: &HashMap<String, String>, name: &str) -> Option<String> {
if params.contains_key(name) {
let param_value = params.get(name).unwrap().as_str();
debug!("Found parameter {name} with value {}", param_value);
Some(param_value.to_string())
} else {
None
}
// Parses a query parameter into any `FromStr` type (e.g. bool, i64, String). Returns `None` when
// the parameter is absent or fails to parse. The target type is inferred from the call site, so
// callers annotate the binding: `let limit: Option<i64> = parse_parameter(&params, "page_limit");`
pub fn parse_parameter<T: FromStr>(params: &HashMap<String, String>, name: &str) -> Option<T> {
let param_value = params.get(name)?;
debug!("Found parameter {name} with value {param_value}");
param_value.parse::<T>().ok()
}
pub fn parse_parameter_date(params: &HashMap<String, String>, name: &str) -> Option<DateTime<Utc>> {
if params.contains_key(name) {
let mut param_value = params.get(name).unwrap().to_ascii_uppercase();
if let Some(value) = params.get(name) {
let mut param_value = value.to_ascii_uppercase();
if !param_value.ends_with("Z") {
param_value.push('Z');
}