use crate::config::ConfigError;
use service_builder::builder;
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ConfigField {
pub name: String,
pub field_type: String,
pub required: bool,
pub default_value: Option<String>,
pub description: Option<String>,
pub validation_rules: Vec<String>,
}
impl ConfigField {
pub fn new(name: impl Into<String>, field_type: impl Into<String>) -> Self {
Self {
name: name.into(),
field_type: field_type.into(),
required: false,
default_value: None,
description: None,
validation_rules: Vec::new(),
}
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn with_default(mut self, default: impl Into<String>) -> Self {
self.default_value = Some(default.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn add_validation(mut self, rule: impl Into<String>) -> Self {
self.validation_rules.push(rule.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ConfigSchema {
pub name: String,
pub fields: Vec<ConfigField>,
}
impl ConfigSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
fields: Vec::new(),
}
}
pub fn add_field(mut self, field: ConfigField) -> Self {
self.fields.push(field);
self
}
pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
self.fields.iter().find(|f| f.name == name)
}
pub fn required_fields(&self) -> Vec<&ConfigField> {
self.fields.iter().filter(|f| f.required).collect()
}
pub fn validate_config(&self, config: &HashMap<String, String>) -> Result<(), ConfigError> {
for field in &self.fields {
if field.required && !config.contains_key(&field.name) {
return Err(ConfigError::missing_required(
&field.name,
field
.description
.as_deref()
.unwrap_or("This field is required"),
));
}
}
for (key, value) in config {
if let Some(field) = self.get_field(key) {
self.validate_field_value(field, value)?;
}
}
Ok(())
}
fn validate_field_value(&self, field: &ConfigField, value: &str) -> Result<(), ConfigError> {
match field.field_type.as_str() {
"integer" | "int" => {
value
.parse::<i64>()
.map_err(|_| ConfigError::invalid_value(&field.name, value, "valid integer"))?;
}
"float" | "number" => {
value
.parse::<f64>()
.map_err(|_| ConfigError::invalid_value(&field.name, value, "valid number"))?;
}
"boolean" | "bool" => {
value
.parse::<bool>()
.map_err(|_| ConfigError::invalid_value(&field.name, value, "true or false"))?;
}
"url" => {
if !value.starts_with("http://") && !value.starts_with("https://") {
return Err(ConfigError::invalid_value(&field.name, value, "valid URL"));
}
}
_ => {
}
}
for rule in &field.validation_rules {
self.apply_validation_rule(field, value, rule)?;
}
Ok(())
}
fn apply_validation_rule(
&self,
field: &ConfigField,
value: &str,
rule: &str,
) -> Result<(), ConfigError> {
if rule.starts_with("min_length:") {
let min_len: usize = rule
.strip_prefix("min_length:")
.unwrap()
.parse()
.map_err(|_| ConfigError::validation_failed("Invalid min_length rule"))?;
if value.len() < min_len {
return Err(ConfigError::invalid_value(
&field.name,
value,
format!("at least {} characters", min_len),
));
}
} else if rule.starts_with("max_length:") {
let max_len: usize = rule
.strip_prefix("max_length:")
.unwrap()
.parse()
.map_err(|_| ConfigError::validation_failed("Invalid max_length rule"))?;
if value.len() > max_len {
return Err(ConfigError::invalid_value(
&field.name,
value,
format!("at most {} characters", max_len),
));
}
} else if rule.starts_with("pattern:") {
let pattern = rule.strip_prefix("pattern:").unwrap();
if !value.contains(pattern) {
return Err(ConfigError::invalid_value(
&field.name,
value,
format!("matching pattern: {}", pattern),
));
}
}
Ok(())
}
}
#[builder]
pub struct ConfigBuilder<T> {
#[builder(default)]
pub fields: Vec<ConfigField>,
#[builder(optional)]
pub name: Option<String>,
#[builder(default)]
pub _phantom: std::marker::PhantomData<T>,
}
impl<T> std::fmt::Debug for ConfigBuilder<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigBuilder")
.field("fields_count", &self.fields.len())
.field("name", &self.name)
.finish()
}
}
impl<T> ConfigBuilder<T> {
pub fn build_schema(self) -> ConfigSchema {
ConfigSchema {
name: self.name.unwrap_or_else(|| "DefaultConfig".to_string()),
fields: self.fields,
}
}
}
impl<T> ConfigBuilderBuilder<T> {
pub fn add_field(self, field: ConfigField) -> Self {
let mut fields_vec = self.fields.unwrap_or_default();
fields_vec.push(field);
ConfigBuilderBuilder {
fields: Some(fields_vec),
name: self.name,
_phantom: self._phantom,
}
}
pub fn add_string_field(self, name: impl Into<String>) -> Self {
self.add_field(ConfigField::new(name, "string"))
}
pub fn add_required_string_field(self, name: impl Into<String>) -> Self {
self.add_field(ConfigField::new(name, "string").required())
}
pub fn add_int_field(self, name: impl Into<String>) -> Self {
self.add_field(ConfigField::new(name, "integer"))
}
pub fn add_bool_field(self, name: impl Into<String>) -> Self {
self.add_field(ConfigField::new(name, "boolean"))
}
pub fn add_url_field(self, name: impl Into<String>) -> Self {
self.add_field(ConfigField::new(name, "url"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_schema() {
let schema = ConfigSchema::new("test_config")
.add_field(ConfigField::new("name", "string").required())
.add_field(ConfigField::new("port", "integer").with_default("3000"))
.add_field(ConfigField::new("debug", "boolean").with_default("false"));
assert_eq!(schema.name, "test_config");
assert_eq!(schema.fields.len(), 3);
assert_eq!(schema.required_fields().len(), 1);
}
#[test]
fn test_config_validation() {
let schema = ConfigSchema::new("test_config")
.add_field(ConfigField::new("name", "string").required())
.add_field(ConfigField::new("port", "integer"));
let mut config = HashMap::new();
config.insert("name".to_string(), "test".to_string());
config.insert("port".to_string(), "3000".to_string());
assert!(schema.validate_config(&config).is_ok());
config.remove("name");
assert!(schema.validate_config(&config).is_err());
}
}