| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- package config
- import (
- "fmt"
- "reflect"
- "strings"
- "time"
- )
- // ConfigWithDefaults defines an interface for configurations that can apply their own defaults
- type ConfigWithDefaults interface {
- // ApplySchemaDefaults applies default values using the provided schema
- ApplySchemaDefaults(schema *Schema) error
- // Validate validates the configuration
- Validate() error
- }
- // FieldType defines the type of a configuration field
- type FieldType string
- const (
- FieldTypeBool FieldType = "bool"
- FieldTypeInt FieldType = "int"
- FieldTypeDuration FieldType = "duration"
- FieldTypeInterval FieldType = "interval"
- FieldTypeString FieldType = "string"
- FieldTypeFloat FieldType = "float"
- )
- // FieldUnit defines the unit for display purposes
- type FieldUnit string
- const (
- UnitSeconds FieldUnit = "seconds"
- UnitMinutes FieldUnit = "minutes"
- UnitHours FieldUnit = "hours"
- UnitDays FieldUnit = "days"
- UnitCount FieldUnit = "count"
- UnitNone FieldUnit = ""
- )
- // Field defines a configuration field with all its metadata
- type Field struct {
- // Field identification
- Name string `json:"name"`
- JSONName string `json:"json_name"`
- Type FieldType `json:"type"`
- // Default value and validation
- DefaultValue interface{} `json:"default_value"`
- MinValue interface{} `json:"min_value,omitempty"`
- MaxValue interface{} `json:"max_value,omitempty"`
- Required bool `json:"required"`
- // UI display
- DisplayName string `json:"display_name"`
- Description string `json:"description"`
- HelpText string `json:"help_text"`
- Placeholder string `json:"placeholder"`
- Unit FieldUnit `json:"unit"`
- // Form rendering
- InputType string `json:"input_type"` // "checkbox", "number", "text", "interval", etc.
- CSSClasses string `json:"css_classes,omitempty"`
- }
- // GetDisplayValue returns the value formatted for display in the specified unit
- func (f *Field) GetDisplayValue(value interface{}) interface{} {
- if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds {
- if duration, ok := value.(time.Duration); ok {
- switch f.Unit {
- case UnitMinutes:
- return int(duration.Minutes())
- case UnitHours:
- return int(duration.Hours())
- case UnitDays:
- return int(duration.Hours() / 24)
- }
- }
- if seconds, ok := value.(int); ok {
- switch f.Unit {
- case UnitMinutes:
- return seconds / 60
- case UnitHours:
- return seconds / 3600
- case UnitDays:
- return seconds / (24 * 3600)
- }
- }
- }
- return value
- }
- // GetIntervalDisplayValue returns the value and unit for interval fields
- func (f *Field) GetIntervalDisplayValue(value interface{}) (int, string) {
- if f.Type != FieldTypeInterval {
- return 0, "minutes"
- }
- seconds := 0
- if duration, ok := value.(time.Duration); ok {
- seconds = int(duration.Seconds())
- } else if s, ok := value.(int); ok {
- seconds = s
- }
- return SecondsToIntervalValueUnit(seconds)
- }
- // SecondsToIntervalValueUnit converts seconds to the most appropriate interval unit
- func SecondsToIntervalValueUnit(totalSeconds int) (int, string) {
- if totalSeconds == 0 {
- return 0, "minutes"
- }
- // Check if it's evenly divisible by days
- if totalSeconds%(24*3600) == 0 {
- return totalSeconds / (24 * 3600), "days"
- }
- // Check if it's evenly divisible by hours
- if totalSeconds%3600 == 0 {
- return totalSeconds / 3600, "hours"
- }
- // Default to minutes
- return totalSeconds / 60, "minutes"
- }
- // IntervalValueUnitToSeconds converts interval value and unit to seconds
- func IntervalValueUnitToSeconds(value int, unit string) int {
- switch unit {
- case "days":
- return value * 24 * 3600
- case "hours":
- return value * 3600
- case "minutes":
- return value * 60
- default:
- return value * 60 // Default to minutes
- }
- }
- // ParseDisplayValue converts a display value back to the storage format
- func (f *Field) ParseDisplayValue(displayValue interface{}) interface{} {
- if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds {
- if val, ok := displayValue.(int); ok {
- switch f.Unit {
- case UnitMinutes:
- return val * 60
- case UnitHours:
- return val * 3600
- case UnitDays:
- return val * 24 * 3600
- }
- }
- }
- return displayValue
- }
- // ParseIntervalFormData parses form data for interval fields (value + unit)
- func (f *Field) ParseIntervalFormData(valueStr, unitStr string) (int, error) {
- if f.Type != FieldTypeInterval {
- return 0, fmt.Errorf("field %s is not an interval field", f.Name)
- }
- value := 0
- if valueStr != "" {
- var err error
- value, err = fmt.Sscanf(valueStr, "%d", &value)
- if err != nil {
- return 0, fmt.Errorf("invalid interval value: %s", valueStr)
- }
- }
- return IntervalValueUnitToSeconds(value, unitStr), nil
- }
- // ValidateValue validates a value against the field constraints
- func (f *Field) ValidateValue(value interface{}) error {
- if f.Required && (value == nil || value == "" || value == 0) {
- return fmt.Errorf("%s is required", f.DisplayName)
- }
- if f.MinValue != nil {
- if !f.compareValues(value, f.MinValue, ">=") {
- return fmt.Errorf("%s must be >= %v", f.DisplayName, f.MinValue)
- }
- }
- if f.MaxValue != nil {
- if !f.compareValues(value, f.MaxValue, "<=") {
- return fmt.Errorf("%s must be <= %v", f.DisplayName, f.MaxValue)
- }
- }
- return nil
- }
- // compareValues compares two values based on the operator
- func (f *Field) compareValues(a, b interface{}, op string) bool {
- switch f.Type {
- case FieldTypeInt:
- aVal, aOk := a.(int)
- bVal, bOk := b.(int)
- if !aOk || !bOk {
- return false
- }
- switch op {
- case ">=":
- return aVal >= bVal
- case "<=":
- return aVal <= bVal
- }
- case FieldTypeFloat:
- aVal, aOk := a.(float64)
- bVal, bOk := b.(float64)
- if !aOk || !bOk {
- return false
- }
- switch op {
- case ">=":
- return aVal >= bVal
- case "<=":
- return aVal <= bVal
- }
- }
- return true
- }
- // Schema provides common functionality for configuration schemas
- type Schema struct {
- Fields []*Field `json:"fields"`
- }
- // GetFieldByName returns a field by its JSON name
- func (s *Schema) GetFieldByName(jsonName string) *Field {
- for _, field := range s.Fields {
- if field.JSONName == jsonName {
- return field
- }
- }
- return nil
- }
- // ApplyDefaultsToConfig applies defaults to a configuration that implements ConfigWithDefaults
- func (s *Schema) ApplyDefaultsToConfig(config ConfigWithDefaults) error {
- return config.ApplySchemaDefaults(s)
- }
- // ApplyDefaultsToProtobuf applies defaults to protobuf types using reflection
- func (s *Schema) ApplyDefaultsToProtobuf(config interface{}) error {
- return s.applyDefaultsReflection(config)
- }
- // applyDefaultsReflection applies default values using reflection (internal use only)
- // Used for protobuf types and embedded struct handling
- func (s *Schema) applyDefaultsReflection(config interface{}) error {
- configValue := reflect.ValueOf(config)
- if configValue.Kind() == reflect.Ptr {
- configValue = configValue.Elem()
- }
- if configValue.Kind() != reflect.Struct {
- return fmt.Errorf("config must be a struct or pointer to struct")
- }
- configType := configValue.Type()
- for i := 0; i < configValue.NumField(); i++ {
- field := configValue.Field(i)
- fieldType := configType.Field(i)
- // Handle embedded structs recursively (before JSON tag check)
- if field.Kind() == reflect.Struct && fieldType.Anonymous {
- if !field.CanAddr() {
- return fmt.Errorf("embedded struct %s is not addressable - config must be a pointer", fieldType.Name)
- }
- err := s.applyDefaultsReflection(field.Addr().Interface())
- if err != nil {
- return fmt.Errorf("failed to apply defaults to embedded struct %s: %v", fieldType.Name, err)
- }
- continue
- }
- // Get JSON tag name
- jsonTag := fieldType.Tag.Get("json")
- if jsonTag == "" {
- continue
- }
- // Remove options like ",omitempty"
- if commaIdx := strings.Index(jsonTag, ","); commaIdx >= 0 {
- jsonTag = jsonTag[:commaIdx]
- }
- // Find corresponding schema field
- schemaField := s.GetFieldByName(jsonTag)
- if schemaField == nil {
- continue
- }
- // Apply default if field is zero value
- if field.CanSet() && field.IsZero() {
- defaultValue := reflect.ValueOf(schemaField.DefaultValue)
- if defaultValue.Type().ConvertibleTo(field.Type()) {
- field.Set(defaultValue.Convert(field.Type()))
- }
- }
- }
- return nil
- }
- // ValidateConfig validates a configuration against the schema
- func (s *Schema) ValidateConfig(config interface{}) []error {
- var errors []error
- configValue := reflect.ValueOf(config)
- if configValue.Kind() == reflect.Ptr {
- configValue = configValue.Elem()
- }
- if configValue.Kind() != reflect.Struct {
- errors = append(errors, fmt.Errorf("config must be a struct or pointer to struct"))
- return errors
- }
- configType := configValue.Type()
- for i := 0; i < configValue.NumField(); i++ {
- field := configValue.Field(i)
- fieldType := configType.Field(i)
- // Get JSON tag name
- jsonTag := fieldType.Tag.Get("json")
- if jsonTag == "" {
- continue
- }
- // Remove options like ",omitempty"
- if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 {
- jsonTag = jsonTag[:commaIdx]
- }
- // Find corresponding schema field
- schemaField := s.GetFieldByName(jsonTag)
- if schemaField == nil {
- continue
- }
- // Validate field value
- fieldValue := field.Interface()
- if err := schemaField.ValidateValue(fieldValue); err != nil {
- errors = append(errors, err)
- }
- }
- return errors
- }
|