use super::buf::ReadoutBuf;
use super::context::ReadoutContext;
use crate::lifecycle::SubjectKind;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Lod {
Compact,
Labeled,
Expanded,
}
impl Default for Lod {
fn default() -> Self {
Lod::Labeled
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ContentMode {
Value,
Explanation,
}
#[derive(Default, Clone, Debug)]
pub struct ReadoutOptions {
kv: Vec<(String, OptionValue)>,
}
#[derive(Debug, Clone)]
pub struct OptionTypeMismatch {
pub key: String,
pub expected: &'static str,
pub actual: OptionValue,
}
impl std::fmt::Display for OptionTypeMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"option {key:?} expected {exp}, got {act:?}",
key = self.key,
exp = self.expected,
act = self.actual
)
}
}
impl std::error::Error for OptionTypeMismatch {}
impl ReadoutOptions {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, value: OptionValue) {
let key = key.into();
if let Some(slot) = self.kv.iter_mut().find(|(k, _)| *k == key) {
slot.1 = value;
} else {
self.kv.push((key, value));
}
}
pub fn get(&self, key: &str) -> Option<&OptionValue> {
self.kv.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
pub fn get_str(&self, key: &str) -> Option<&str> {
match self.get(key)? {
OptionValue::Str(s) => Some(s),
_ => None,
}
}
pub fn get_int(&self, key: &str) -> Option<i64> {
match self.get(key)? {
OptionValue::Int(n) => Some(*n),
_ => None,
}
}
pub fn get_float(&self, key: &str) -> Option<f64> {
match self.get(key)? {
OptionValue::Float(f) => Some(*f),
OptionValue::Int(n) => Some(*n as f64),
_ => None,
}
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
match self.get(key)? {
OptionValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn try_get_str(&self, key: &str) -> Result<Option<&str>, OptionTypeMismatch> {
match self.get(key) {
None => Ok(None),
Some(OptionValue::Str(s)) => Ok(Some(s)),
Some(other) => Err(OptionTypeMismatch {
key: key.to_string(),
expected: "string",
actual: other.clone(),
}),
}
}
pub fn try_get_int(&self, key: &str) -> Result<Option<i64>, OptionTypeMismatch> {
match self.get(key) {
None => Ok(None),
Some(OptionValue::Int(n)) => Ok(Some(*n)),
Some(other) => Err(OptionTypeMismatch {
key: key.to_string(),
expected: "integer",
actual: other.clone(),
}),
}
}
pub fn try_get_float(&self, key: &str) -> Result<Option<f64>, OptionTypeMismatch> {
match self.get(key) {
None => Ok(None),
Some(OptionValue::Float(f)) => Ok(Some(*f)),
Some(OptionValue::Int(n)) => Ok(Some(*n as f64)),
Some(other) => Err(OptionTypeMismatch {
key: key.to_string(),
expected: "number",
actual: other.clone(),
}),
}
}
pub fn try_get_bool(&self, key: &str) -> Result<Option<bool>, OptionTypeMismatch> {
match self.get(key) {
None => Ok(None),
Some(OptionValue::Bool(b)) => Ok(Some(*b)),
Some(other) => Err(OptionTypeMismatch {
key: key.to_string(),
expected: "bool",
actual: other.clone(),
}),
}
}
pub fn is_empty(&self) -> bool {
self.kv.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &OptionValue)> {
self.kv.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[derive(Clone, Debug)]
pub enum OptionValue {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
}
pub trait Readout: Send + Sync {
fn name(&self) -> &'static str;
fn accepts(&self) -> &'static [SubjectKind];
fn render(
&self,
ctx: &dyn ReadoutContext,
lod: Lod,
mode: ContentMode,
opts: &ReadoutOptions,
out: &mut dyn ReadoutBuf,
) -> usize;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_get_str_distinguishes_missing_from_wrong_type() {
let mut opts = ReadoutOptions::new();
opts.set("present", OptionValue::Str("hi".into()));
opts.set("wrong_type", OptionValue::Int(7));
assert!(matches!(opts.try_get_str("absent"), Ok(None)));
assert!(matches!(opts.try_get_str("present"), Ok(Some("hi"))));
assert!(opts.try_get_str("wrong_type").is_err());
}
#[test]
fn try_get_int_rejects_string_typo() {
let mut opts = ReadoutOptions::new();
opts.set("precision", OptionValue::Str("3".into()));
let err = opts.try_get_int("precision").unwrap_err();
assert_eq!(err.expected, "integer");
assert_eq!(err.key, "precision");
}
#[test]
fn try_get_float_accepts_int() {
let mut opts = ReadoutOptions::new();
opts.set("ratio", OptionValue::Int(2));
assert_eq!(opts.try_get_float("ratio").unwrap(), Some(2.0));
}
}