// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package validator import ( "net/url" "regexp" "slices" "go.probo.inc/probo/pkg/gid" ) var ( domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) ) // URL validates that a string is a valid URL with http or https scheme. func URL() ValidatorFunc { return func(value any) *ValidationError { actualValue, isNil := dereferenceValue(value) if isNil { return nil } str, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidURL, "value must be a string") } if str == "" { return nil } parsedURL, err := url.Parse(str) if err != nil { return newValidationError(ErrorCodeInvalidURL, "invalid URL format") } if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return newValidationError(ErrorCodeInvalidURL, "URL must use http or https scheme") } if parsedURL.Host == "" { return newValidationError(ErrorCodeInvalidURL, "URL must have a host") } return nil } } // HTTPSUrl validates that a string is a valid HTTPS URL (not HTTP). func HTTPSUrl() ValidatorFunc { return func(value any) *ValidationError { actualValue, isNil := dereferenceValue(value) if isNil { return nil } str, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidURL, "value must be a string") } if str == "" { return nil } parsedURL, err := url.Parse(str) if err != nil { return newValidationError(ErrorCodeInvalidURL, "invalid URL format") } if parsedURL.Scheme != "https" { return newValidationError(ErrorCodeInvalidURL, "URL must use https scheme") } if parsedURL.Host == "" { return newValidationError(ErrorCodeInvalidURL, "URL must have a host") } return nil } } // GID validates that a string is a valid GID using gid.ParseGID. // Optionally validates the entity type if provided. // // Example usage: // - GID() validates any GID format // - GID(100) validates GID with entity type 100 // - GID(100, 200) validates GID with entity type 100 or 200 func GID(entityTypes ...uint16) ValidatorFunc { return func(value any) *ValidationError { if value == nil { return nil } var gidValue gid.GID switch v := value.(type) { case gid.GID: gidValue = v case *gid.GID: if v == nil { return nil } gidValue = *v default: return newValidationError(ErrorCodeInvalidGID, "value must be a GID") } if len(entityTypes) > 0 { parsedEntityType := gidValue.EntityType() valid := slices.Contains(entityTypes, parsedEntityType) if !valid { return newValidationError(ErrorCodeInvalidGID, "GID has invalid entity type") } } return nil } } // Origin validates that a string is a valid web origin (scheme + host + optional port). // No path, query, fragment, or userinfo is allowed. func Origin() ValidatorFunc { return func(value any) *ValidationError { actualValue, isNil := dereferenceValue(value) if isNil { return nil } str, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidFormat, "value must be a string") } if str == "" { return nil } parsedURL, err := url.Parse(str) if err != nil { return newValidationError(ErrorCodeInvalidFormat, "must be a valid origin (e.g. https://example.com)") } if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return newValidationError(ErrorCodeInvalidFormat, "must be a valid origin (e.g. https://example.com)") } if parsedURL.Host == "" { return newValidationError(ErrorCodeInvalidFormat, "must be a valid origin (e.g. https://example.com)") } if parsedURL.Path != "" && parsedURL.Path != "/" { return newValidationError(ErrorCodeInvalidFormat, "must be a valid origin (e.g. https://example.com)") } if parsedURL.RawQuery != "" || parsedURL.Fragment != "" || parsedURL.User != nil { return newValidationError(ErrorCodeInvalidFormat, "must be a valid origin (e.g. https://example.com)") } return nil } } // Domain validates that a string is a valid domain name. func Domain() ValidatorFunc { return func(value any) *ValidationError { actualValue, isNil := dereferenceValue(value) if isNil { return nil } str, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidFormat, "value must be a string") } if str == "" { return nil } if len(str) > 253 { return newValidationError(ErrorCodeInvalidFormat, "domain name too long (max 253 characters)") } if !domainRegex.MatchString(str) { return newValidationError(ErrorCodeInvalidFormat, "invalid domain name format") } return nil } }