congen 0.1.0

congen helps you build configuration systems that support partial updates from structured changes and CLI input
Documentation
use core::any::Any;
use std::ffi::OsStr;

use crate::{
    Configuration,
    internal::{
        ChangeVerb, CongenChange, CongenInternal, Description, FieldDescription, NotSupported,
        ParseError, VerbError,
    },
};

pub trait ValueEnumConfiguration: clap::ValueEnum + Clone + 'static {}

impl<T> Configuration for T where T: ValueEnumConfiguration {}
impl<T> CongenInternal for T
where
    T: ValueEnumConfiguration,
{
    type CongenChange = ValueEnumChange<T>;

    fn apply_change_with_inner_default(
        &mut self,
        change: Self::CongenChange,
        _inner_default: Option<fn() -> Box<dyn Any>>,
    ) {
        if let ValueEnumChange::Some(value) = change {
            *self = value;
        }
    }

    fn description(field_name: &'static str) -> Description {
        FieldDescription {
            field_name,
            type_name: Self::type_name(),
            is_flag: false,
            allow_unset: false,
            has_default: false,
            clap_data: None,
        }
        .into()
    }
}

#[derive(Debug, Clone, Default)]
pub enum ValueEnumChange<T> {
    Some(T),
    #[default]
    NoChange,
}

impl<T> CongenChange for ValueEnumChange<T>
where
    T: ValueEnumConfiguration,
{
    type Configuration = T;

    fn empty() -> Self {
        Self::NoChange
    }

    fn parse(input: &OsStr) -> Result<Result<Self, ParseError>, NotSupported> {
        let Some(input) = input.to_str() else {
            return Ok(Err(ParseError("expected utf-8 encoded valued".to_string())));
        };
        match T::from_str(input, false) {
            Ok(value) => Ok(Ok(Self::Some(value))),
            Err(error) => Ok(Err(ParseError(error))),
        }
    }

    fn apply_change(&mut self, change: Self) {
        if let Self::Some(new_change) = change {
            *self = Self::Some(new_change);
        }
    }

    fn from_path_and_verb<'a, P>(mut path: P, verb: ChangeVerb) -> Result<Self, VerbError>
    where
        P: Iterator<Item = &'a str>,
    {
        assert!(path.next().is_none(), "Option<T> implies this is a field");
        match verb {
            ChangeVerb::Set(unparsed) => Ok(Self::parse(&unparsed)??),
            ChangeVerb::SetAny(value) => Ok(Self::Some(
                *value.downcast().map_err(|_| VerbError::DowncastFailed)?,
            )),
            ChangeVerb::UseDefault
            | ChangeVerb::SetFlag
            | ChangeVerb::Unset
            | ChangeVerb::List(_) => Err(VerbError::UnsupportedVerb(verb)),
        }
    }

    fn unwrap_field(self) -> Result<Self::Configuration, Self> {
        match self {
            Self::Some(value) => Ok(value),
            Self::NoChange => Err(Self::NoChange),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::iter::empty;

    extern crate self as congen;

    use super::*;

    #[derive(
        clap::ValueEnum, congen_derive::ValueEnumConfiguration, Debug, Clone, PartialEq, Eq,
    )]
    enum Mode {
        Fast,
        Safe,
    }

    #[test]
    fn value_enum_description_is_field() {
        let desc = Mode::description("mode");
        let Description::Field(field) = desc else {
            panic!("ValueEnum should map to FieldDescription");
        };

        assert_eq!(field.field_name, "mode");
        assert_eq!(field.type_name, Mode::type_name());
        assert!(!field.is_flag);
        assert!(!field.allow_unset);
        assert!(!field.has_default);
    }

    #[test]
    fn value_enum_change_set_parses() {
        let change = <ValueEnumChange<Mode> as CongenChange>::from_path_and_verb(
            empty(),
            ChangeVerb::Set("fast".to_owned().into()),
        )
        .expect("parses enum change");

        assert!(matches!(change, ValueEnumChange::Some(Mode::Fast)));
    }
}