naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SchemaFile {
    pub version: String,
    pub fields: Vec<FieldDefinition>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FieldDefinition {
    pub key: String,
    pub r#type: String,
    pub description: Option<String>,
    pub validation: Option<ValidationRules>,
    pub is_secret: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ValidationRules {
    pub min_length: Option<usize>,
    pub max_length: Option<usize>,
    pub min_value: Option<i64>,
    pub max_value: Option<i64>,
    pub pattern: Option<String>,
}

impl SchemaFile {
    pub fn new(version: &str) -> Self {
        SchemaFile {
            version: version.to_string(),
            fields: Vec::new(),
        }
    }

    pub fn add_field(&mut self, field: FieldDefinition) {
        self.fields.push(field);
    }

    pub fn get_field(&self, key: &str) -> Option<&FieldDefinition> {
        self.fields.iter().find(|f| f.key == key)
    }

    pub fn get_field_mut(&mut self, key: &str) -> Option<&mut FieldDefinition> {
        self.fields.iter_mut().find(|f| f.key == key)
    }

    pub fn remove_field(&mut self, key: &str) -> Option<FieldDefinition> {
        if let Some(pos) = self.fields.iter().position(|f| f.key == key) {
            Some(self.fields.remove(pos))
        } else {
            None
        }
    }

    pub fn is_empty(&self) -> bool {
        self.fields.is_empty()
    }

    pub fn len(&self) -> usize {
        self.fields.len()
    }

    pub fn iter(&self) -> impl Iterator<Item = &FieldDefinition> {
        self.fields.iter()
    }
}

impl FieldDefinition {
    pub fn new(key: &str, r#type: &str) -> Self {
        FieldDefinition {
            key: key.to_string(),
            r#type: r#type.to_string(),
            description: None,
            validation: None,
            is_secret: false,
        }
    }

    pub fn with_description(mut self, description: &str) -> Self {
        self.description = Some(description.to_string());
        self
    }

    pub fn with_validation(mut self, validation: ValidationRules) -> Self {
        self.validation = Some(validation);
        self
    }

    pub fn secret(mut self) -> Self {
        self.is_secret = true;
        self
    }

    pub fn is_string(&self) -> bool {
        self.r#type == "string"
    }

    pub fn is_integer(&self) -> bool {
        self.r#type == "integer"
    }

    pub fn is_boolean(&self) -> bool {
        self.r#type == "boolean"
    }
}

impl ValidationRules {
    pub fn new() -> Self {
        ValidationRules {
            min_length: None,
            max_length: None,
            min_value: None,
            max_value: None,
            pattern: None,
        }
    }

    pub fn with_min_length(mut self, min: usize) -> Self {
        self.min_length = Some(min);
        self
    }

    pub fn with_max_length(mut self, max: usize) -> Self {
        self.max_length = Some(max);
        self
    }

    pub fn with_length_range(mut self, min: usize, max: usize) -> Self {
        self.min_length = Some(min);
        self.max_length = Some(max);
        self
    }

    pub fn with_min_value(mut self, min: i64) -> Self {
        self.min_value = Some(min);
        self
    }

    pub fn with_max_value(mut self, max: i64) -> Self {
        self.max_value = Some(max);
        self
    }

    pub fn with_value_range(mut self, min: i64, max: i64) -> Self {
        self.min_value = Some(min);
        self.max_value = Some(max);
        self
    }

    pub fn with_pattern(mut self, pattern: &str) -> Self {
        self.pattern = Some(pattern.to_string());
        self
    }
}

impl Default for ValidationRules {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_schema_file_new() {
        let schema = SchemaFile::new("1.0.0");
        assert_eq!(schema.version, "1.0.0");
        assert!(schema.fields.is_empty());
    }

    #[test]
    fn test_schema_file_add_field() {
        let mut schema = SchemaFile::new("1.0.0");
        let field = FieldDefinition::new("username", "string");
        schema.add_field(field);
        assert_eq!(schema.len(), 1);
    }

    #[test]
    fn test_schema_file_get_field() {
        let mut schema = SchemaFile::new("1.0.0");
        let field = FieldDefinition::new("username", "string");
        schema.add_field(field);

        assert!(schema.get_field("username").is_some());
        assert!(schema.get_field("nonexistent").is_none());
    }

    #[test]
    fn test_schema_file_remove_field() {
        let mut schema = SchemaFile::new("1.0.0");
        let field = FieldDefinition::new("username", "string");
        schema.add_field(field);

        let removed = schema.remove_field("username");
        assert!(removed.is_some());
        assert!(schema.get_field("username").is_none());
    }

    #[test]
    fn test_field_definition_new() {
        let field = FieldDefinition::new("db_host", "string");
        assert_eq!(field.key, "db_host");
        assert_eq!(field.r#type, "string");
        assert!(!field.is_secret);
    }

    #[test]
    fn test_field_definition_builder() {
        let field = FieldDefinition::new("email", "string")
            .with_description("User email address")
            .with_validation(ValidationRules::new().with_pattern(r"^[\w\.-]+@[\w\.-]+\.\w+$"))
            .secret();

        assert_eq!(field.key, "email");
        assert!(field.description.is_some());
        assert!(field.validation.is_some());
        assert!(field.is_secret);
    }

    #[test]
    fn test_field_type_checkers() {
        let string_field = FieldDefinition::new("name", "string");
        let int_field = FieldDefinition::new("age", "integer");
        let bool_field = FieldDefinition::new("active", "boolean");

        assert!(string_field.is_string());
        assert!(!string_field.is_integer());
        assert!(!string_field.is_boolean());

        assert!(int_field.is_integer());
        assert!(bool_field.is_boolean());
    }

    #[test]
    fn test_validation_rules_builder() {
        let rules = ValidationRules::new()
            .with_min_length(3)
            .with_max_length(10)
            .with_min_value(0)
            .with_max_value(120)
            .with_pattern(r"^[a-z]+$");

        assert_eq!(rules.min_length, Some(3));
        assert_eq!(rules.max_length, Some(10));
        assert_eq!(rules.min_value, Some(0));
        assert_eq!(rules.max_value, Some(120));
        assert!(rules.pattern.is_some());
    }

    #[test]
    fn test_validation_rules_length_range() {
        let rules = ValidationRules::new().with_length_range(5, 15);
        assert_eq!(rules.min_length, Some(5));
        assert_eq!(rules.max_length, Some(15));
    }

    #[test]
    fn test_validation_rules_value_range() {
        let rules = ValidationRules::new().with_value_range(-100, 100);
        assert_eq!(rules.min_value, Some(-100));
        assert_eq!(rules.max_value, Some(100));
    }

    #[test]
    fn test_schema_iter() {
        let mut schema = SchemaFile::new("1.0.0");
        schema.add_field(FieldDefinition::new("field1", "string"));
        schema.add_field(FieldDefinition::new("field2", "integer"));

        let keys: Vec<_> = schema.iter().map(|f| f.key.clone()).collect();
        assert_eq!(keys, vec!["field1", "field2"]);
    }
}