strval 0.5.0

Parse strings into values
Documentation
//! Relative or absolute limit.

use std::{fmt, marker::PhantomData, str::FromStr};

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

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


#[derive(Clone, Debug)]
pub enum Lim {
  Rel(f32),
  Abs(u64)
}

impl Default for Lim {
  fn default() -> Self {
    Self::Rel(1.0)
  }
}


#[derive(Clone, Debug)]
pub struct AnyLim;
impl Controller for AnyLim {
  type Type = Lim;
  fn def() -> String {
    String::from("100%")
  }
  fn validate(val: &Self::Type) -> Result<(), Error> {
    match val {
      Lim::Rel(f) => {
        let range = 0.0..=100.0;
        if range.contains(f) {
          Ok(())
        } else {
          Err(Error::OutOfBounds(format!("{range:?}")))
        }
      }
      Lim::Abs(_a) => Ok(())
    }
  }
}


/// A relative or absolute limit.
///
/// If setting this to a string ending with a `%` it will set a relative limit.
/// Otherwise sets an absolute limit.
#[derive(Debug, Clone)]
pub struct RelAbsLim<C = AnyLim> {
  sval: String,
  val: Lim,
  _marker: PhantomData<C>
}

impl<C> Default for RelAbsLim<C> {
  fn default() -> Self {
    Self {
      sval: "100%".into(),
      val: Lim::default(),
      _marker: PhantomData
    }
  }
}

impl<C> StrVal for RelAbsLim<C>
where
  C: Controller<Type = Lim>
{
  type Type = Lim;

  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.clone();
    Ok(dv.val)
  }

  fn get(&self) -> Self::Type {
    self.val.clone()
  }

  fn val_str(&self) -> Option<String> {
    match self.val {
      Lim::Rel(f) => Some(f.to_string()),
      Lim::Abs(v) => Some(v.to_string())
    }
  }

  fn get_str_vals(&self) -> (String, Option<String>) {
    (self.sval.clone(), None)
  }
}

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

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

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

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    if let Some(s) = s.strip_suffix("%") {
      let f = s.parse::<f32>().unwrap();
      let f = f / 100.0;
      let val = Lim::Rel(f);
      C::validate(&val)?;
      Ok(Self {
        sval: s.to_string(),
        val,
        _marker: PhantomData
      })
    } else {
      let val = parse_size::Config::default()
        .with_binary()
        .parse_size(s)
        .map_err(|e| Error::Invalid(e.to_string()))?;
      let val = Lim::Abs(val);
      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 RelAbsLim<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 RelAbsLim<C>
where
  C: Controller<Type = Lim>
{
  #[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::*;

  #[test]
  #[allow(clippy::float_cmp)]
  fn percent() {
    let v = "100%".parse::<RelAbsLim>().unwrap();

    let Lim::Rel(f) = v.get() else {
      panic!("Unexpectedly not Lim::Rel");
    };
    assert_eq!(f, 1.0);
  }

  #[test]
  fn abs() {
    let v = "1000".parse::<RelAbsLim>().unwrap();

    let Lim::Abs(a) = v.get() else {
      panic!("Unexpectedly not Lim::Abs");
    };
    assert_eq!(a, 1000);
  }

  #[test]
  fn abs2() {
    let v = "64K".parse::<RelAbsLim>().unwrap();

    let Lim::Abs(a) = v.get() else {
      panic!("Unexpectedly not Lim::Abs");
    };
    assert_eq!(a, 65536);
  }
}

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