package dashboard import ( "encoding/json" "errors" "fmt" "regexp" ) const CurrentSchemaVersion = 2 var slugPattern = regexp.MustCompile("^[a-z0-9]+(?:-[a-z0-9]+)*$") type Document map[string]any func Validate(document Document) error { if document == nil { return errors.New("dashboard document is required") } if version, ok := number(document["schemaVersion"]); !ok || version < 1 || version > CurrentSchemaVersion { return errors.New("unsupported dashboard schema version") } for _, field := range []string{"id", "slug", "name", "scope", "variables", "widgets", "settings"} { if _, ok := document[field]; !ok { return fmt.Errorf("dashboard field %q is required", field) } } slug, ok := document["slug"].(string) if !ok || !slugPattern.MatchString(slug) || len(slug) > 80 { return errors.New("invalid dashboard slug") } name, ok := document["name"].(string) if !ok || name == "" || len(name) > 120 { return errors.New("invalid dashboard name") } scope, ok := document["scope"].(string) if !ok || scope != "personal" && scope != "shared" && scope != "system" { return errors.New("invalid dashboard scope") } if _, ok := document["widgets"].([]any); !ok { return errors.New("dashboard widgets must be an array") } if _, ok := document["variables"].([]any); !ok { return errors.New("dashboard variables must be an array") } if _, ok := document["settings"].(map[string]any); !ok { return errors.New("dashboard settings must be an object") } return nil } func Migrate(document Document) (Document, error) { if err := Validate(document); err != nil { return nil, err } version, _ := number(document["schemaVersion"]) if version == CurrentSchemaVersion { return clone(document), nil } migrated := clone(document) migrated["schemaVersion"] = CurrentSchemaVersion settings := migrated["settings"].(map[string]any) if _, ok := settings["live"]; !ok { settings["live"] = false } if _, ok := settings["refreshSeconds"]; !ok { settings["refreshSeconds"] = 30 } return migrated, nil } func clone(document Document) Document { encoded, _ := json.Marshal(document) var result Document _ = json.Unmarshal(encoded, &result) return result } func number(value any) (int, bool) { switch v := value.(type) { case int: return v, true case float64: return int(v), v == float64(int(v)) default: return 0, false } }