From 1adbae211ed9da3fc8d024b193d7e24e0c1414eb Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 29 Aug 2026 00:48:55 +0200 Subject: [PATCH] refactor(validation): move govalidator rules and struct validation into models --- pkg/models/validation.go | 77 ++++++++++++++++++++++++++++++++++++++++ pkg/routes/validation.go | 36 +------------------ 2 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 pkg/models/validation.go diff --git a/pkg/models/validation.go b/pkg/models/validation.go new file mode 100644 index 000000000..fb24839cd --- /dev/null +++ b/pkg/models/validation.go @@ -0,0 +1,77 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "sort" + + "code.vikunja.io/api/pkg/db" + + "github.com/asaskevich/govalidator" +) + +// The `valid:` tag rules live on the models, so the custom validators they +// reference are registered here rather than in the HTTP layer — every entry +// point that validates a model (echo's CustomValidator, /api/v2, MCP) +// imports this package and therefore gets them. +func init() { + govalidator.TagMap["time"] = func(str string) bool { + return govalidator.IsTime(str, "15:04") + } + + // Adapts to the database in use: MySQL TEXT tops out far below what + // PostgreSQL and SQLite accept. + govalidator.TagMap["dbtext"] = func(str string) bool { + maxLength := 65000 + if dialect := db.GetDialect(); dialect == "postgres" || dialect == "sqlite3" { + maxLength = 1048576 + } + return len(str) <= maxLength + } +} + +// ValidateStruct checks a model against its `valid:` struct tags and reports +// every failure as an InvalidFieldError. +func ValidateStruct(i interface{}) error { + return ValidateStructFields(i, nil) +} + +// ValidateStructFields is ValidateStruct restricted to the given field names +// (JSON names, or Go field names for `json:"-"` fields). Callers that build a +// model from a partial payload need this: a `required` rule must not fire for +// a field the caller never sent. A nil set means "report everything". +func ValidateStructFields(i interface{}, only map[string]bool) error { + _, err := govalidator.ValidateStruct(i) + if err == nil { + return nil + } + + var errs []string + for field, e := range govalidator.ErrorsByField(err) { + if only != nil && !only[field] { + continue + } + errs = append(errs, field+": "+e) + } + if len(errs) == 0 { + return nil + } + + // Map iteration order is non-deterministic; sort for a stable errors[]. + sort.Strings(errs) + return InvalidFieldError(errs) +} diff --git a/pkg/routes/validation.go b/pkg/routes/validation.go index 64a8235ca..00d01de1b 100644 --- a/pkg/routes/validation.go +++ b/pkg/routes/validation.go @@ -17,47 +17,13 @@ package routes import ( - "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/models" - - "github.com/asaskevich/govalidator" ) // CustomValidator is a dummy struct to use govalidator with echo type CustomValidator struct{} -func init() { - govalidator.TagMap["time"] = func(str string) bool { - return govalidator.IsTime(str, "15:04") - } - - // Custom validator for database TEXT fields that adapts to the database being used - govalidator.TagMap["dbtext"] = func(str string) bool { - // Get the current database dialect - dialect := db.GetDialect() - - // Default limit for MySQL and unknown databases (65KB safely under TEXT limit) - maxLength := 65000 - - // For databases that support larger text fields - if dialect == "postgres" || dialect == "sqlite3" { - maxLength = 1048576 // ~1MB limit for PostgreSQL and SQLite - } - - return len(str) <= maxLength - } -} - // Validate validates stuff func (cv *CustomValidator) Validate(i interface{}) error { - if _, err := govalidator.ValidateStruct(i); err != nil { - - var errs []string - for field, e := range govalidator.ErrorsByField(err) { - errs = append(errs, field+": "+e) - } - - return models.InvalidFieldError(errs) - } - return nil + return models.ValidateStruct(i) }