use crate::{PluginError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSchema {
pub version: String,
pub properties: HashMap<String, ConfigProperty>,
pub required: Vec<String>,
}
impl Default for ConfigSchema {
fn default() -> Self {
Self::new()
}
}
impl ConfigSchema {
pub fn new() -> Self {
Self {
version: "1.0".to_string(),
properties: HashMap::new(),
required: Vec::new(),
}
}
pub fn add_property(&mut self, name: String, property: ConfigProperty) {
self.properties.insert(name, property);
}
pub fn require(&mut self, name: &str) {
if !self.required.contains(&name.to_string()) {
self.required.push(name.to_string());
}
}
pub fn validate(&self) -> Result<()> {
if self.version != "1.0" {
return Err(PluginError::config_error(&format!(
"Unsupported schema version: {}",
self.version
)));
}
for (name, property) in &self.properties {
property.validate(name)?;
}
for required_name in &self.required {
if !self.properties.contains_key(required_name) {
return Err(PluginError::config_error(&format!(
"Required property '{}' not defined in schema",
required_name
)));
}
}
Ok(())
}
pub fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
if let serde_json::Value::Object(config_obj) = config {
for required_name in &self.required {
if !config_obj.contains_key(required_name) {
return Err(PluginError::config_error(&format!(
"Missing required configuration property: {}",
required_name
)));
}
}
for (key, value) in config_obj {
if let Some(property) = self.properties.get(key) {
property.validate_value(value)?;
} else {
return Err(PluginError::config_error(&format!(
"Unknown configuration property: {}",
key
)));
}
}
Ok(())
} else {
Err(PluginError::config_error("Configuration must be an object"))
}
}
pub fn get_property(&self, name: &str) -> Option<&ConfigProperty> {
self.properties.get(name)
}
pub fn is_required(&self, name: &str) -> bool {
self.required.contains(&name.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigProperty {
#[serde(rename = "type")]
pub property_type: PropertyType,
pub description: Option<String>,
pub default: Option<serde_json::Value>,
pub validation: Option<PropertyValidation>,
pub deprecated: Option<bool>,
}
impl ConfigProperty {
pub fn new(property_type: PropertyType) -> Self {
Self {
property_type,
description: None,
default: None,
validation: None,
deprecated: None,
}
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn with_default(mut self, default: serde_json::Value) -> Self {
self.default = Some(default);
self
}
pub fn with_validation(mut self, validation: PropertyValidation) -> Self {
self.validation = Some(validation);
self
}
pub fn deprecated(mut self) -> Self {
self.deprecated = Some(true);
self
}
pub fn validate(&self, name: &str) -> Result<()> {
if let Some(default) = &self.default {
self.validate_value(default).map_err(|e| {
PluginError::config_error(&format!(
"Invalid default value for property '{}': {}",
name, e
))
})?;
}
Ok(())
}
pub fn validate_value(&self, value: &serde_json::Value) -> Result<()> {
match &self.property_type {
PropertyType::String => {
if !value.is_string() {
return Err(PluginError::config_error("Expected string value"));
}
}
PropertyType::Number => {
if !value.is_number() {
return Err(PluginError::config_error("Expected number value"));
}
}
PropertyType::Boolean => {
if !value.is_boolean() {
return Err(PluginError::config_error("Expected boolean value"));
}
}
PropertyType::Array => {
if !value.is_array() {
return Err(PluginError::config_error("Expected array value"));
}
}
PropertyType::Object => {
if !value.is_object() {
return Err(PluginError::config_error("Expected object value"));
}
}
}
if let Some(validation) = &self.validation {
validation.validate_value(value)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PropertyType {
#[serde(rename = "string")]
String,
#[serde(rename = "number")]
Number,
#[serde(rename = "boolean")]
Boolean,
#[serde(rename = "array")]
Array,
#[serde(rename = "object")]
Object,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyValidation {
pub min: Option<f64>,
pub max: Option<f64>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub enum_values: Option<Vec<serde_json::Value>>,
}
impl Default for PropertyValidation {
fn default() -> Self {
Self::new()
}
}
impl PropertyValidation {
pub fn new() -> Self {
Self {
min: None,
max: None,
min_length: None,
max_length: None,
pattern: None,
enum_values: None,
}
}
pub fn validate_value(&self, value: &serde_json::Value) -> Result<()> {
if let Some(num) = value.as_f64() {
if let Some(min) = self.min {
if num < min {
return Err(PluginError::config_error(&format!(
"Value {} is less than minimum {}",
num, min
)));
}
}
if let Some(max) = self.max {
if num > max {
return Err(PluginError::config_error(&format!(
"Value {} is greater than maximum {}",
num, max
)));
}
}
}
if let Some(s) = value.as_str() {
if let Some(min_len) = self.min_length {
if s.len() < min_len {
return Err(PluginError::config_error(&format!(
"String length {} is less than minimum {}",
s.len(),
min_len
)));
}
}
if let Some(max_len) = self.max_length {
if s.len() > max_len {
return Err(PluginError::config_error(&format!(
"String length {} is greater than maximum {}",
s.len(),
max_len
)));
}
}
if let Some(pattern) = &self.pattern {
let regex = regex::Regex::new(pattern).map_err(|e| {
PluginError::config_error(&format!("Invalid regex pattern: {}", e))
})?;
if !regex.is_match(s) {
return Err(PluginError::config_error(&format!(
"String '{}' does not match pattern '{}'",
s, pattern
)));
}
}
}
if let Some(arr) = value.as_array() {
if let Some(min_len) = self.min_length {
if arr.len() < min_len {
return Err(PluginError::config_error(&format!(
"Array length {} is less than minimum {}",
arr.len(),
min_len
)));
}
}
if let Some(max_len) = self.max_length {
if arr.len() > max_len {
return Err(PluginError::config_error(&format!(
"Array length {} is greater than maximum {}",
arr.len(),
max_len
)));
}
}
}
if let Some(enum_values) = &self.enum_values {
let mut found = false;
for allowed_value in enum_values {
if value == allowed_value {
found = true;
break;
}
}
if !found {
return Err(PluginError::config_error(&format!(
"Value {} is not in allowed values",
value
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_compiles() {
}
}