more-config 3.0.0

Provides support for configuration
Documentation
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]

mod builder;
mod cfg;
pub(crate) mod context;
mod error;
mod file;
mod merge;
mod provider;
mod reloadable;
mod section;
mod settings;

/// Contains chained configuration support.
#[cfg(feature = "chained")]
pub mod chained;

/// Contains command line configuration support.
#[cfg(feature = "cmd")]
pub mod cmd;

/// Contains strongly-typed configuration deserialization support.
pub mod de;

/// Contains environment variable configuration support.
#[cfg(feature = "env")]
pub mod env;

/// Contains `*.ini` file configuration support.
#[cfg(feature = "ini")]
pub mod ini;

/// Contains `*.json` file configuration support.
#[cfg(feature = "json")]
pub mod json;

/// Contains in-memory configuration support.
#[cfg(feature = "mem")]
pub mod mem;

/// Contains configuration serialization support.
pub mod ser;

/// Contains strongly-typed configuration support.
#[cfg(feature = "typed")]
pub mod typed;

/// Provides configuration path utilities.
pub mod path;

/// Contains library prelude.
pub mod prelude;

/// Contains `*.xml` file configuration support.
#[cfg(feature = "xml")]
pub mod xml;

/// Contains `*.yaml` and `*.yml` file configuration support.
#[cfg(feature = "yaml")]
pub mod yaml;

pub use builder::Builder;
pub use cfg::{Configuration, ReloadableConfiguration};
pub use error::Error;
pub use file::{FileSource, FileSourceBuilder};
pub use merge::Merge;
pub use provider::Provider;
pub use reloadable::Reloadable;
pub use section::{OwnedSection, Section};
pub use settings::Settings;

#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use config_derive::Deserialize;

/// Represents a configuration result.
pub type Result<T = ()> = std::result::Result<T, Error>;

/// Creates and returns a new [configuration builder](Builder)
#[inline]
pub fn builder() -> Builder {
    Builder::default()
}

/// Converts the specified text into Pascal Case.
///
/// # Arguments
///
/// * `text` - the input text to convert
///
/// # Remarks
///
/// This function supports converting the following input forms:
///
/// - Pascal Case (`HelloWorld → HelloWorld`)
/// - Camel Case (`helloWorld → HelloWorld`)
/// - Snake Case (`hello_world → HelloWorld`)
/// - Screaming Snake Case (`HELLO_WORLD → HelloWorld`)
/// - Kebab Case (`hello-world → HelloWorld`)
/// - Screaming Kebab Case (`HELLO-WORLD → HelloWorld`)
///
/// The characters `' '`, `'_'`, and `'-'` are considered word boundaries. Alphabetic characters following these
/// characters will be capitalized.
///
/// # Remarks
///
/// This function does not handle Unicode word boundaries.
pub fn pascal_case(text: &str) -> String {
    let mut converted = String::with_capacity(text.len());
    let mut next_is_upper = true;
    let mut last_was_lower = false;

    for ch in text.chars() {
        if ch == ' ' || ch == '_' || ch == '-' || ch == ':' {
            next_is_upper = true;
            last_was_lower = false;

            if ch == ':' {
                converted.push(ch);
            }
        } else if ch.is_alphabetic() && (next_is_upper || (last_was_lower && ch.is_ascii_uppercase())) {
            converted.push(ch.to_ascii_uppercase());
            next_is_upper = false;
            last_was_lower = ch.is_ascii_lowercase();
        } else if ch.is_alphabetic() {
            converted.push(ch.to_ascii_lowercase());
            last_was_lower = ch.is_ascii_lowercase();
        } else {
            converted.push(ch);
            next_is_upper = true;
            last_was_lower = false;
        }
    }

    converted
}

// this function is intentionally located here for tracing::trace! to capture the desired location info
#[inline(never)]
fn overridden(mut id: u8, names: &[String], providers: u8, key: &str, old: &str, new: &str) {
    use tracing::trace;

    const UNKNOWN: &str = "Unknown";

    let mut i = (id as u32).saturating_sub(1);
    let current = if i < u8::BITS && (i as usize) < names.len() {
        &names[i as usize]
    } else {
        UNKNOWN
    };
    let last = loop {
        if id > 0 {
            id >>= 1;
            i = (id as u32).saturating_sub(1);

            if providers & id != 0 {
                if i < u8::BITS && (i as usize) < names.len() {
                    break names[i as usize].as_str();
                } else {
                    break UNKNOWN;
                }
            }
        } else {
            break UNKNOWN;
        }
    };

    trace!("key '{key}' with value '{old}' ({last}) has been overridden with value '{new}' ({current})");
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_case::test_case;

    #[test_case(""; "if empty")]
    #[test_case("HelloWorld"; "in pascal case")]
    #[test_case("Hello.World"; "with a period")]
    #[test_case("Hello:World"; "with a colon")]
    fn pascal_case_should_not_change_text(expected: &str) {
        // arrange

        // act
        let actual = pascal_case(expected);

        // assert
        assert_eq!(actual, expected);
    }

    #[test_case("hello world"; "from lower title case")]
    #[test_case("Hello World"; "from upper title case")]
    #[test_case("helloWorld"; "from camel case")]
    #[test_case("hello_world"; "from snake case")]
    #[test_case("HELLO_WORLD"; "from screaming snake case")]
    #[test_case("hello-world"; "from kebab case")]
    #[test_case("HELLO-WORLD"; "from screaming kebab case")]
    fn pascal_case_should_convert_text(text: &str) {
        // arrange

        // act
        let actual = pascal_case(text);

        // assert
        assert_eq!(actual, "HelloWorld");
    }

    #[test]
    fn pascal_case_should_convert_capitalized_with_colon() {
        // arrange
        let expected = "Hello:World";

        // act
        let actual = pascal_case("HELLO:WORLD");

        // assert
        assert_eq!(actual, expected);
    }
}