schema.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package config
  2. import (
  3. "fmt"
  4. "reflect"
  5. "strings"
  6. "time"
  7. )
  8. // ConfigWithDefaults defines an interface for configurations that can apply their own defaults
  9. type ConfigWithDefaults interface {
  10. // ApplySchemaDefaults applies default values using the provided schema
  11. ApplySchemaDefaults(schema *Schema) error
  12. // Validate validates the configuration
  13. Validate() error
  14. }
  15. // FieldType defines the type of a configuration field
  16. type FieldType string
  17. const (
  18. FieldTypeBool FieldType = "bool"
  19. FieldTypeInt FieldType = "int"
  20. FieldTypeDuration FieldType = "duration"
  21. FieldTypeInterval FieldType = "interval"
  22. FieldTypeString FieldType = "string"
  23. FieldTypeFloat FieldType = "float"
  24. )
  25. // FieldUnit defines the unit for display purposes
  26. type FieldUnit string
  27. const (
  28. UnitSeconds FieldUnit = "seconds"
  29. UnitMinutes FieldUnit = "minutes"
  30. UnitHours FieldUnit = "hours"
  31. UnitDays FieldUnit = "days"
  32. UnitCount FieldUnit = "count"
  33. UnitNone FieldUnit = ""
  34. )
  35. // Field defines a configuration field with all its metadata
  36. type Field struct {
  37. // Field identification
  38. Name string `json:"name"`
  39. JSONName string `json:"json_name"`
  40. Type FieldType `json:"type"`
  41. // Default value and validation
  42. DefaultValue interface{} `json:"default_value"`
  43. MinValue interface{} `json:"min_value,omitempty"`
  44. MaxValue interface{} `json:"max_value,omitempty"`
  45. Required bool `json:"required"`
  46. // UI display
  47. DisplayName string `json:"display_name"`
  48. Description string `json:"description"`
  49. HelpText string `json:"help_text"`
  50. Placeholder string `json:"placeholder"`
  51. Unit FieldUnit `json:"unit"`
  52. // Form rendering
  53. InputType string `json:"input_type"` // "checkbox", "number", "text", "interval", etc.
  54. CSSClasses string `json:"css_classes,omitempty"`
  55. }
  56. // GetDisplayValue returns the value formatted for display in the specified unit
  57. func (f *Field) GetDisplayValue(value interface{}) interface{} {
  58. if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds {
  59. if duration, ok := value.(time.Duration); ok {
  60. switch f.Unit {
  61. case UnitMinutes:
  62. return int(duration.Minutes())
  63. case UnitHours:
  64. return int(duration.Hours())
  65. case UnitDays:
  66. return int(duration.Hours() / 24)
  67. }
  68. }
  69. if seconds, ok := value.(int); ok {
  70. switch f.Unit {
  71. case UnitMinutes:
  72. return seconds / 60
  73. case UnitHours:
  74. return seconds / 3600
  75. case UnitDays:
  76. return seconds / (24 * 3600)
  77. }
  78. }
  79. }
  80. return value
  81. }
  82. // GetIntervalDisplayValue returns the value and unit for interval fields
  83. func (f *Field) GetIntervalDisplayValue(value interface{}) (int, string) {
  84. if f.Type != FieldTypeInterval {
  85. return 0, "minutes"
  86. }
  87. seconds := 0
  88. if duration, ok := value.(time.Duration); ok {
  89. seconds = int(duration.Seconds())
  90. } else if s, ok := value.(int); ok {
  91. seconds = s
  92. }
  93. return SecondsToIntervalValueUnit(seconds)
  94. }
  95. // SecondsToIntervalValueUnit converts seconds to the most appropriate interval unit
  96. func SecondsToIntervalValueUnit(totalSeconds int) (int, string) {
  97. if totalSeconds == 0 {
  98. return 0, "minutes"
  99. }
  100. // Check if it's evenly divisible by days
  101. if totalSeconds%(24*3600) == 0 {
  102. return totalSeconds / (24 * 3600), "days"
  103. }
  104. // Check if it's evenly divisible by hours
  105. if totalSeconds%3600 == 0 {
  106. return totalSeconds / 3600, "hours"
  107. }
  108. // Default to minutes
  109. return totalSeconds / 60, "minutes"
  110. }
  111. // IntervalValueUnitToSeconds converts interval value and unit to seconds
  112. func IntervalValueUnitToSeconds(value int, unit string) int {
  113. switch unit {
  114. case "days":
  115. return value * 24 * 3600
  116. case "hours":
  117. return value * 3600
  118. case "minutes":
  119. return value * 60
  120. default:
  121. return value * 60 // Default to minutes
  122. }
  123. }
  124. // ParseDisplayValue converts a display value back to the storage format
  125. func (f *Field) ParseDisplayValue(displayValue interface{}) interface{} {
  126. if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds {
  127. if val, ok := displayValue.(int); ok {
  128. switch f.Unit {
  129. case UnitMinutes:
  130. return val * 60
  131. case UnitHours:
  132. return val * 3600
  133. case UnitDays:
  134. return val * 24 * 3600
  135. }
  136. }
  137. }
  138. return displayValue
  139. }
  140. // ParseIntervalFormData parses form data for interval fields (value + unit)
  141. func (f *Field) ParseIntervalFormData(valueStr, unitStr string) (int, error) {
  142. if f.Type != FieldTypeInterval {
  143. return 0, fmt.Errorf("field %s is not an interval field", f.Name)
  144. }
  145. value := 0
  146. if valueStr != "" {
  147. var err error
  148. value, err = fmt.Sscanf(valueStr, "%d", &value)
  149. if err != nil {
  150. return 0, fmt.Errorf("invalid interval value: %s", valueStr)
  151. }
  152. }
  153. return IntervalValueUnitToSeconds(value, unitStr), nil
  154. }
  155. // ValidateValue validates a value against the field constraints
  156. func (f *Field) ValidateValue(value interface{}) error {
  157. if f.Required && (value == nil || value == "" || value == 0) {
  158. return fmt.Errorf("%s is required", f.DisplayName)
  159. }
  160. if f.MinValue != nil {
  161. if !f.compareValues(value, f.MinValue, ">=") {
  162. return fmt.Errorf("%s must be >= %v", f.DisplayName, f.MinValue)
  163. }
  164. }
  165. if f.MaxValue != nil {
  166. if !f.compareValues(value, f.MaxValue, "<=") {
  167. return fmt.Errorf("%s must be <= %v", f.DisplayName, f.MaxValue)
  168. }
  169. }
  170. return nil
  171. }
  172. // compareValues compares two values based on the operator
  173. func (f *Field) compareValues(a, b interface{}, op string) bool {
  174. switch f.Type {
  175. case FieldTypeInt:
  176. aVal, aOk := a.(int)
  177. bVal, bOk := b.(int)
  178. if !aOk || !bOk {
  179. return false
  180. }
  181. switch op {
  182. case ">=":
  183. return aVal >= bVal
  184. case "<=":
  185. return aVal <= bVal
  186. }
  187. case FieldTypeFloat:
  188. aVal, aOk := a.(float64)
  189. bVal, bOk := b.(float64)
  190. if !aOk || !bOk {
  191. return false
  192. }
  193. switch op {
  194. case ">=":
  195. return aVal >= bVal
  196. case "<=":
  197. return aVal <= bVal
  198. }
  199. }
  200. return true
  201. }
  202. // Schema provides common functionality for configuration schemas
  203. type Schema struct {
  204. Fields []*Field `json:"fields"`
  205. }
  206. // GetFieldByName returns a field by its JSON name
  207. func (s *Schema) GetFieldByName(jsonName string) *Field {
  208. for _, field := range s.Fields {
  209. if field.JSONName == jsonName {
  210. return field
  211. }
  212. }
  213. return nil
  214. }
  215. // ApplyDefaultsToConfig applies defaults to a configuration that implements ConfigWithDefaults
  216. func (s *Schema) ApplyDefaultsToConfig(config ConfigWithDefaults) error {
  217. return config.ApplySchemaDefaults(s)
  218. }
  219. // ApplyDefaultsToProtobuf applies defaults to protobuf types using reflection
  220. func (s *Schema) ApplyDefaultsToProtobuf(config interface{}) error {
  221. return s.applyDefaultsReflection(config)
  222. }
  223. // applyDefaultsReflection applies default values using reflection (internal use only)
  224. // Used for protobuf types and embedded struct handling
  225. func (s *Schema) applyDefaultsReflection(config interface{}) error {
  226. configValue := reflect.ValueOf(config)
  227. if configValue.Kind() == reflect.Ptr {
  228. configValue = configValue.Elem()
  229. }
  230. if configValue.Kind() != reflect.Struct {
  231. return fmt.Errorf("config must be a struct or pointer to struct")
  232. }
  233. configType := configValue.Type()
  234. for i := 0; i < configValue.NumField(); i++ {
  235. field := configValue.Field(i)
  236. fieldType := configType.Field(i)
  237. // Handle embedded structs recursively (before JSON tag check)
  238. if field.Kind() == reflect.Struct && fieldType.Anonymous {
  239. if !field.CanAddr() {
  240. return fmt.Errorf("embedded struct %s is not addressable - config must be a pointer", fieldType.Name)
  241. }
  242. err := s.applyDefaultsReflection(field.Addr().Interface())
  243. if err != nil {
  244. return fmt.Errorf("failed to apply defaults to embedded struct %s: %v", fieldType.Name, err)
  245. }
  246. continue
  247. }
  248. // Get JSON tag name
  249. jsonTag := fieldType.Tag.Get("json")
  250. if jsonTag == "" {
  251. continue
  252. }
  253. // Remove options like ",omitempty"
  254. if commaIdx := strings.Index(jsonTag, ","); commaIdx >= 0 {
  255. jsonTag = jsonTag[:commaIdx]
  256. }
  257. // Find corresponding schema field
  258. schemaField := s.GetFieldByName(jsonTag)
  259. if schemaField == nil {
  260. continue
  261. }
  262. // Apply default if field is zero value
  263. if field.CanSet() && field.IsZero() {
  264. defaultValue := reflect.ValueOf(schemaField.DefaultValue)
  265. if defaultValue.Type().ConvertibleTo(field.Type()) {
  266. field.Set(defaultValue.Convert(field.Type()))
  267. }
  268. }
  269. }
  270. return nil
  271. }
  272. // ValidateConfig validates a configuration against the schema
  273. func (s *Schema) ValidateConfig(config interface{}) []error {
  274. var errors []error
  275. configValue := reflect.ValueOf(config)
  276. if configValue.Kind() == reflect.Ptr {
  277. configValue = configValue.Elem()
  278. }
  279. if configValue.Kind() != reflect.Struct {
  280. errors = append(errors, fmt.Errorf("config must be a struct or pointer to struct"))
  281. return errors
  282. }
  283. configType := configValue.Type()
  284. for i := 0; i < configValue.NumField(); i++ {
  285. field := configValue.Field(i)
  286. fieldType := configType.Field(i)
  287. // Get JSON tag name
  288. jsonTag := fieldType.Tag.Get("json")
  289. if jsonTag == "" {
  290. continue
  291. }
  292. // Remove options like ",omitempty"
  293. if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 {
  294. jsonTag = jsonTag[:commaIdx]
  295. }
  296. // Find corresponding schema field
  297. schemaField := s.GetFieldByName(jsonTag)
  298. if schemaField == nil {
  299. continue
  300. }
  301. // Validate field value
  302. fieldValue := field.Interface()
  303. if err := schemaField.ValidateValue(fieldValue); err != nil {
  304. errors = append(errors, err)
  305. }
  306. }
  307. return errors
  308. }