sakurs-core 0.2.0

High-performance sentence boundary detection using Delta-Stack Monoid algorithm
Documentation
//! Configuration API for sentence processing

use crate::api::{Error, Language};
use std::str::FromStr;

/// Default configuration constants
pub mod defaults {
    /// Default chunk size in bytes (256KB)
    pub const CHUNK_SIZE: usize = 256 * 1024;
}

/// Processing configuration
#[derive(Debug, Clone)]
pub struct Config {
    pub(crate) language: Language,
    pub(crate) chunk_size: usize,      // in bytes
    pub(crate) threads: Option<usize>, // None = all available threads
}

impl Default for Config {
    fn default() -> Self {
        Self {
            language: Language::default(),
            chunk_size: defaults::CHUNK_SIZE,
            threads: None,
        }
    }
}

impl Config {
    /// Create a configuration builder
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }

    /// Create a configuration optimized for small texts
    pub fn small_text() -> Self {
        Self {
            language: Language::default(),
            chunk_size: 8 * 1024, // 8KB chunks
            threads: None,
        }
    }

    /// Create a configuration optimized for large texts
    pub fn large_text() -> Self {
        Self {
            language: Language::default(),
            chunk_size: 512 * 1024, // 512KB chunks
            threads: None,          // Use all available cores
        }
    }

    /// Create a configuration optimized for streaming
    pub fn streaming() -> Self {
        Self {
            language: Language::default(),
            chunk_size: 32 * 1024, // 32KB chunks
            threads: Some(2),      // Limited parallelism
        }
    }

    /// Validate the configuration
    pub(crate) fn validate(&self) -> Result<(), Error> {
        if self.chunk_size == 0 {
            return Err(Error::Configuration(
                "chunk_size must be greater than 0".into(),
            ));
        }

        if let Some(threads) = self.threads {
            if threads == 0 {
                return Err(Error::Configuration(
                    "threads must be greater than 0".into(),
                ));
            }
        }

        Ok(())
    }
}

/// Fluent builder for configuration
#[derive(Debug, Default)]
pub struct ConfigBuilder {
    language: Option<String>,
    chunk_size: Option<usize>,
    threads: Option<usize>,
}

impl ConfigBuilder {
    /// Create a new configuration builder
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the language by code
    pub fn language(mut self, code: impl Into<String>) -> Result<Self, Error> {
        self.language = Some(code.into());
        Ok(self)
    }

    /// Set the chunk size in bytes
    pub fn chunk_size(mut self, bytes: usize) -> Self {
        self.chunk_size = Some(bytes);
        self
    }

    /// Set the number of threads (None = all available)
    pub fn threads(mut self, count: Option<usize>) -> Self {
        self.threads = count;
        self
    }

    /// Build the configuration
    pub fn build(self) -> Result<Config, Error> {
        let mut config = Config::default();

        if let Some(lang_code) = self.language {
            config.language = Language::from_str(&lang_code)?;
        }

        if let Some(size) = self.chunk_size {
            config.chunk_size = size;
        }

        if self.threads.is_some() {
            config.threads = self.threads;
        }

        config.validate()?;
        Ok(config)
    }
}

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

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert_eq!(config.chunk_size, defaults::CHUNK_SIZE);
        assert!(config.threads.is_none());
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_config_validation() {
        // Invalid chunk size
        let config = Config {
            chunk_size: 0,
            ..Default::default()
        };
        assert!(config.validate().is_err());

        // Invalid thread count
        let config = Config {
            threads: Some(0),
            ..Default::default()
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_preset_configs() {
        // Small text preset
        let small = Config::small_text();
        assert_eq!(small.chunk_size, 8 * 1024);
        assert!(small.validate().is_ok());

        // Large text preset
        let large = Config::large_text();
        assert_eq!(large.chunk_size, 512 * 1024);
        assert!(large.validate().is_ok());

        // Streaming preset
        let streaming = Config::streaming();
        assert_eq!(streaming.chunk_size, 32 * 1024);
        assert_eq!(streaming.threads, Some(2));
        assert!(streaming.validate().is_ok());
    }

    #[test]
    fn test_config_builder_with_new_fields() {
        let config = Config::builder()
            .chunk_size(128 * 1024)
            .threads(Some(4))
            .build()
            .unwrap();

        assert_eq!(config.chunk_size, 128 * 1024);
        assert_eq!(config.threads, Some(4));
    }

    #[test]
    fn test_config_validation_boundary_values() {
        // Test chunk_size at minimum valid value
        let config = Config {
            chunk_size: 1,
            ..Default::default()
        };
        assert!(config.validate().is_ok());

        // Test threads at minimum valid value
        let config = Config {
            threads: Some(1),
            ..Default::default()
        };
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_config_builder_invalid_configurations() {
        // Test building with invalid chunk_size
        let result = Config::builder().chunk_size(0).build();
        assert!(result.is_err());

        // Test building with invalid thread count
        let result = Config::builder().threads(Some(0)).build();
        assert!(result.is_err());
    }

    #[test]
    fn test_config_builder_with_invalid_language() {
        let result = Config::builder()
            .language("invalid_lang")
            .unwrap() // language() returns Ok
            .build(); // Error occurs here during validation

        assert!(result.is_err());
        match result {
            Err(Error::InvalidLanguage(msg)) => {
                assert!(msg.contains("invalid_lang"));
            }
            _ => panic!("Expected InvalidLanguage error"),
        }
    }

    #[test]
    fn test_config_validation_error_messages() {
        // Test chunk_size error message
        let config = Config {
            chunk_size: 0,
            ..Default::default()
        };
        match config.validate() {
            Err(Error::Configuration(msg)) => {
                assert_eq!(msg, "chunk_size must be greater than 0");
            }
            _ => panic!("Expected specific error message"),
        }

        // Test threads error message
        let config = Config {
            threads: Some(0),
            ..Default::default()
        };
        match config.validate() {
            Err(Error::Configuration(msg)) => {
                assert_eq!(msg, "threads must be greater than 0");
            }
            _ => panic!("Expected specific error message"),
        }
    }

    #[test]
    fn test_config_builder_partial_configuration() {
        // Test building with only some fields set
        let config = Config::builder().chunk_size(64 * 1024).build().unwrap();

        assert_eq!(config.chunk_size, 64 * 1024);
        assert!(config.threads.is_none());
    }

    #[test]
    fn test_config_builder_language_setting() {
        // Test setting valid language
        let config = Config::builder().language("en").unwrap().build().unwrap();
        assert_eq!(config.language, Language::English);

        let config = Config::builder().language("ja").unwrap().build().unwrap();
        assert_eq!(config.language, Language::Japanese);
    }

    #[test]
    fn test_large_configuration_values() {
        // Test with very large valid values
        let config = Config {
            chunk_size: usize::MAX / 2,
            threads: Some(1024),
            ..Default::default()
        };
        assert!(config.validate().is_ok());
    }
}