strval 0.5.0

Parse strings into values
Documentation
use std::{fmt, marker::PhantomData, str::FromStr};

#[cfg(feature = "rusqlite")]
use rusqlite::{
  ToSql,
  types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}
};

use parse_size::ByteSuffix;

use crate::{AnyUsize, Controller, StrVal, err::Error};


/// A `usize` value that supports suffixes, like `1k`, `2.5k`, etc.
///
/// This variant treats k as 1024.  See [`DecUsizeCount`](super::DecUsizeCount)
/// for a decimal variant.
#[derive(Debug, Clone)]
pub struct BinUsizeCount<C = AnyUsize> {
  sval: String,
  val: usize,
  _marker: PhantomData<C>
}

impl<C> Default for BinUsizeCount<C> {
  fn default() -> Self {
    Self {
      sval: "0".into(),
      val: 0,
      _marker: PhantomData
    }
  }
}

impl<C> StrVal for BinUsizeCount<C>
where
  C: Controller<Type = usize>
{
  type Type = usize;

  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> {
    Some(self.val.to_string())
  }

  fn get_str_vals(&self) -> (String, Option<String>) {
    let eval = self.val.to_string();
    let eval = (self.sval != eval).then_some(eval);
    (self.sval.clone(), eval)
  }
}

impl<C> AsRef<str> for BinUsizeCount<C> {
  fn as_ref(&self) -> &str {
    &self.sval
  }
}

impl<C> fmt::Display for BinUsizeCount<C> {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}", self.sval)
  }
}

impl<C> FromStr for BinUsizeCount<C>
where
  C: Controller<Type = usize>
{
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    let val = parse_size::Config::default()
      .with_binary()
      .with_byte_suffix(ByteSuffix::Deny)
      .parse_size(s)
      .map_err(|e| Error::Invalid(e.to_string()))?;
    let val =
      usize::try_from(val).map_err(|e| Error::OutOfBounds(e.to_string()))?;
    C::validate(&val)?;
    Ok(Self {
      sval: s.to_string(),
      val,
      _marker: PhantomData
    })
  }
}

#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> ToSql for BinUsizeCount<C> {
  fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
    self.sval.to_sql()
  }
}

#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> FromSql for BinUsizeCount<C>
where
  C: Controller<Type = usize>
{
  #[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 SomeUsizeBound {}

  impl Controller for SomeUsizeBound {
    type Type = usize;
    fn def() -> String {
      String::from("10")
    }
    fn validate(val: &Self::Type) -> Result<(), Error> {
      if *val < 10 || *val > 256 * 1024 {
        Err(Error::OutOfBounds("Must be 10 - 256K".into()))
      } else {
        Ok(())
      }
    }
  }

  #[test]
  fn default1() {
    #[allow(clippy::default_trait_access)]
    let mut val: BinUsizeCount = Default::default();

    let v = val.set("64k").unwrap();
    assert_eq!(v, 65536);
    assert_eq!(val.get(), 65536);
  }

  #[test]
  fn default2() {
    let mut val = <BinUsizeCount>::default();

    let v = val.set("64k").unwrap();
    assert_eq!(v, 65536);
    assert_eq!(val.get(), 65536);
  }

  #[test]
  fn set_get() {
    let mut val = BinUsizeCount::<SomeUsizeBound>::default();

    let v = val.set("64k").unwrap();
    assert_eq!(v, 65536);
    assert_eq!(val.get(), 65536);
  }

  #[test]
  #[should_panic(expected = "OutOfBounds(\"Must be 10 - 256K\")")]
  fn oob() {
    let mut val = BinUsizeCount::<SomeUsizeBound>::default();

    let _v = val.set("512k").unwrap();
  }

  #[test]
  #[should_panic(expected = "OutOfBounds(\"Must be 10 - 256K\")")]
  fn oob_parsed() {
    let _val = "512k".parse::<BinUsizeCount<SomeUsizeBound>>().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 = "64k".parse::<BinUsizeCount>().unwrap();
    conn
      .execute("INSERT INTO tbl (id, txtval) VALUES (?, ?);", params![1, v])
      .unwrap();

    let v = "256".parse::<BinUsizeCount>().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: BinUsizeCount = stmt.query_one([1], |row| row.get(0)).unwrap();
    assert_eq!(v.get(), 65536);

    let v: BinUsizeCount = stmt.query_one([2], |row| row.get(0)).unwrap();
    assert_eq!(v.get(), 256);
  }


  #[cfg(feature = "rusqlite")]
  #[test]
  #[should_panic(expected = "FromSqlConversionFailure(0, Text, \
                             Invalid(\"invalid digit found in string\"))")]
  fn bad_query() {
    let conn = memdb().unwrap();

    conn
      .execute(
        "INSERT INTO tbl (id, txtval) VALUES (?, ?);",
        params![1, "notanumber"]
      )
      .unwrap();

    let mut stmt = conn.prepare("SELECT txtval FROM tbl WHERE id=?;").unwrap();

    let _v: BinUsizeCount = stmt.query_one([1], |row| row.get(0)).unwrap();
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :