Replace three instances of leftover %T format verbs in logger.ErrorCtx() calls with proper structured logging fields. Add alphabetically-sorted reference documentation for six new contrib/claude/ guides and reorder the existing list. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
4.4 KiB
4.4 KiB
Validation Framework
Custom fluent validation API in pkg/validator/. Used in every service method to validate request structs before processing.
Basic pattern
Create a validator, chain Check() calls for each field, then call Error() to get accumulated errors:
func (req *CreateVendorRequest) Validate() error {
v := validator.New()
v.Check(req.OrganizationID, "organization_id",
validator.Required(),
validator.GID(coredata.OrganizationEntityType),
)
v.Check(req.Name, "name",
validator.Required(),
validator.SafeTextNoNewLine(TitleMaxLength),
)
v.Check(req.Category, "category",
validator.OneOfSlice(coredata.VendorCategories()),
)
return v.Error()
}
Check(value, fieldName, validators...) runs validators sequentially on a value. Multiple Check() calls accumulate all errors. Error() returns nil if clean, or ValidationErrors (which implements error).
Available validators
Common
Required()— value must not be nil, empty string, or empty sliceNotEmpty()— value cannot be empty/nil (only checks content, not presence)
String
MinLen(n)— at least n charactersMaxLen(n)— at most n charactersOneOfSlice[T](allowed)— value must be in allowed list
Numeric
Min(n)— value >= nMax(n)— value <= n
Format
URL()— valid HTTP/HTTPS URL with hostHTTPSUrl()— HTTPS-only URLDomain()— valid RFC-compliant domain nameGID(entityTypes...)— valid GID, optionally restricted to specific entity types
Security
NoHTML()— rejects HTML tagsPrintableText()— rejects invisible/harmful Unicode (control chars, bidi overrides, zero-width)NoNewLine()— rejects\nand\rSafeText(maxLen)— combines NotEmpty + MaxLen + NoHTML + PrintableText (allows newlines)SafeTextNoNewLine(maxLen)— same as SafeText but also rejects newlines (for single-line fields)
Time
After(refTime)— time must be after referenceBefore(refTime)— time must be before referenceRangeDuration(min, max)— duration between min and max inclusive
Pointer handling
The framework automatically dereferences pointers at any level. Nil pointers pass all non-Required validators:
v.Check(stringValue, "field", validator.Required()) // Direct value
v.Check(&stringValue, "field", validator.Required()) // Pointer — auto-dereferenced
v.Check(nilPointer, "field", validator.MinLen(5)) // Nil passes (not Required)
v.Check(nilPointer, "field", validator.Required()) // Nil fails Required
Collection validation
Use CheckEach to validate each item in a slice:
v.CheckEach(ids, "ids", func(index int, item any) {
gidValue := item.(gid.GID)
v.Check(gidValue, fmt.Sprintf("ids[%d]", index),
validator.Required(),
validator.GID(coredata.VendorEntityType),
)
})
Nil or empty slices are silently skipped.
Error types
type ValidationError struct {
Field string // e.g. "email"
Code ErrorCode // e.g. ErrorCodeRequired
Message string // human-readable
Value any // the problematic value
}
Error codes:
| Code | Meaning |
|---|---|
REQUIRED |
Field is missing or empty |
INVALID_FORMAT |
Value does not match expected format |
OUT_OF_RANGE |
Numeric value outside bounds |
TOO_SHORT |
String below minimum length |
TOO_LONG |
String above maximum length |
INVALID_EMAIL |
Invalid email address |
INVALID_URL |
Invalid URL |
INVALID_ENUM |
Value not in allowed set |
INVALID_GID |
Invalid GID or wrong entity type |
UNSAFE_CONTENT |
HTML, control chars, or harmful Unicode |
CUSTOM |
Custom validation error |
ValidationErrors is a slice with query methods:
errs := err.(validator.ValidationErrors)
errs.HasErrors() // bool
errs.Fields() // unique field names
errs.ByField("name") // filter by field
errs.ByCode(ErrorCodeRequired) // filter by code
errs.First() // first error
Error propagation
Validation errors flow naturally through Go's error interface:
- Request struct's
Validate()returnsValidationErrorsornil - Service method checks error before processing
- GraphQL/HTTP handlers convert
ValidationErrorsto appropriate response format
func (s *Service) CreateVendor(ctx context.Context, req CreateVendorRequest) (*coredata.Vendor, error) {
if err := req.Validate(); err != nil {
return nil, err
}
// proceed with business logic
}