use std::{fmt, marker::PhantomData, str::FromStr, time::Duration};
#[cfg(feature = "rusqlite")]
use rusqlite::{
ToSql,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}
};
use crate::{Controller, StrVal, err::Error};
#[derive(Clone, Debug)]
pub struct AnyDur;
impl Controller for AnyDur {
type Type = Duration;
fn def() -> String {
String::from("0")
}
fn validate(_val: &Self::Type) -> Result<(), Error> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Dur<C = AnyDur> {
sval: String,
val: Duration,
_marker: PhantomData<C>
}
impl<C> Default for Dur<C> {
fn default() -> Self {
Self {
sval: "0".into(),
val: Duration::default(),
_marker: PhantomData
}
}
}
impl<C> StrVal for Dur<C>
where
C: Controller<Type = Duration>
{
type Type = Duration;
fn set(&mut self, sval: &str) -> Result<Self::Type, Error> {
let dv = sval.parse::<Self>()?;
C::validate(&dv.val)?;
self.sval = sval.to_string();
self.val = dv.val;
Ok(dv.val)
}
fn get(&self) -> Self::Type {
self.val
}
fn val_str(&self) -> Option<String> {
None
}
fn get_str_vals(&self) -> (String, Option<String>) {
(self.sval.clone(), None)
}
}
impl<C> AsRef<str> for Dur<C> {
fn as_ref(&self) -> &str {
&self.sval
}
}
impl<C> fmt::Display for Dur<C> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.sval)
}
}
impl<C> FromStr for Dur<C>
where
C: Controller<Type = Duration>
{
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let dur = s
.parse::<jiff::SignedDuration>()
.map_err(|e| Error::Invalid(e.to_string()))?;
let dur = dur.unsigned_abs();
C::validate(&dur)?;
Ok(Self {
sval: s.to_string(),
val: dur,
_marker: PhantomData
})
}
}
#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> ToSql for Dur<C> {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::from(self.as_ref()))
}
}
#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> FromSql for Dur<C>
where
C: Controller<Type = Duration>
{
#[inline]
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let s = String::column_result(value)?;
s.parse::<Self>()
.map_err(|e| FromSqlError::Other(Box::new(e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rusqlite")]
use rusqlite::{Connection, params};
struct SomeDurBound {}
impl Controller for SomeDurBound {
type Type = Duration;
fn def() -> String {
String::from("0s")
}
fn validate(val: &Self::Type) -> Result<(), Error> {
const MIN: Duration = Duration::from_secs(1);
const MAX: Duration = Duration::from_secs(60);
let range = MIN..MAX;
if range.contains(val) {
Ok(())
} else {
Err(Error::OutOfBounds(format!("{range:?}")))
}
}
}
#[test]
fn default1() {
#[allow(clippy::default_trait_access)]
let mut val: Dur = Default::default();
let v = val.set("1s").unwrap();
assert_eq!(v, Duration::from_secs(1));
assert_eq!(val.get(), Duration::from_secs(1));
}
#[test]
fn default2() {
let mut val = <Dur>::default();
let v = val.set("1ms").unwrap();
assert_eq!(v, Duration::from_millis(1));
assert_eq!(val.get(), Duration::from_millis(1));
}
#[test]
fn set_get() {
let mut val = Dur::<SomeDurBound>::default();
let v = val.set("1s 250ms").unwrap();
assert_eq!(v, Duration::from_millis(1250));
assert_eq!(val.get(), Duration::from_millis(1250));
}
#[test]
#[should_panic(expected = "OutOfBounds(\"1s..60s\")")]
fn oob() {
let mut val = Dur::<SomeDurBound>::default();
let _v = val.set("100ms").unwrap();
}
#[test]
#[should_panic(expected = "OutOfBounds(\"1s..60s\")")]
fn oob_parsed() {
let _val = "1ms".parse::<Dur<SomeDurBound>>().unwrap();
}
#[cfg(feature = "rusqlite")]
fn memdb() -> Result<Connection, rusqlite::Error> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(
"CREATE TABLE tbl (id INTEGER PRIMARY KEY, txtval TEXT)"
)?;
Ok(conn)
}
#[cfg(feature = "rusqlite")]
#[test]
fn insert_query() {
let conn = memdb().unwrap();
let v = "1s".parse::<Dur>().unwrap();
conn
.execute("INSERT INTO tbl (id, txtval) VALUES (?, ?);", params![1, v])
.unwrap();
let v = "2 ms".parse::<Dur>().unwrap();
conn
.execute("INSERT INTO tbl (id, txtval) VALUES (?, ?);", params![2, v])
.unwrap();
let mut stmt = conn.prepare("SELECT txtval FROM tbl WHERE id=?;").unwrap();
let v: Dur = stmt.query_one([1], |row| row.get(0)).unwrap();
assert_eq!(v.get(), Duration::from_secs(1));
let v: Dur = stmt.query_one([2], |row| row.get(0)).unwrap();
assert_eq!(v.get(), Duration::from_millis(2));
}
#[cfg(feature = "rusqlite")]
#[test]
#[should_panic(expected = "FromSqlConversionFailure(0, Text, \
Invalid(\"failed to parse input in the \
\\\"friendly\\\" duration format: expected \
duration to start with a unit value (a decimal \
integer) after an optional sign, but no \
integer was found\"))")]
fn bad_query() {
let conn = memdb().unwrap();
conn
.execute(
"INSERT INTO tbl (id, txtval) VALUES (?, ?);",
params![1, "notaduration"]
)
.unwrap();
let mut stmt = conn.prepare("SELECT txtval FROM tbl WHERE id=?;").unwrap();
let _v: Dur = stmt.query_one([1], |row| row.get(0)).unwrap();
}
}