diff --git a/pkg/govy/errors.go b/pkg/govy/errors.go index 39369c2..cc82272 100644 --- a/pkg/govy/errors.go +++ b/pkg/govy/errors.go @@ -266,11 +266,15 @@ func (e RuleErrorTemplate) Error() string { return fmt.Sprintf("%T should not be used directly", e) } -// TemplateVars lists variables available to builtin rule message templates. +// TemplateVars lists variables available to builtin rule message and description templates. // Use the same names for consistent behavior across rules. -// When [PropertyRules.HideValue] applies, it sets [TemplateVars.PropertyValue] to `[hidden]` -// and redacts the property value from [TemplateVars.Error] before template execution. -// It does not change the other fields. +// +// Before executing a message template, [Rule.Validate] sets PropertyValue to the +// validated value and Details and Examples to the rule's configuration. +// When [PropertyRules.HideValue] applies, it sets PropertyValue to `[hidden]` +// and redacts the property value from Error. It does not change the other fields. +// Description templates use the values passed to [Rule.WithDescriptionTemplate] +// without validation-time injection. Their rendered descriptions are cached. type TemplateVars struct { // Common variables which are available for all the rules. PropertyValue any diff --git a/pkg/govy/plan.go b/pkg/govy/plan.go index fe747cd..c35d686 100644 --- a/pkg/govy/plan.go +++ b/pkg/govy/plan.go @@ -71,7 +71,8 @@ type TypeInfo struct { // RulePlan is a validation plan for a single [Rule]. type RulePlan struct { - // Description is the value provided to [Rule.WithDescription]. + // Description is the final rule description. It is usually provided by + // [Rule.WithDescription] or rendered by [Rule.WithDescriptionTemplate]. Description string `json:"description"` // Details is the value provided to [Rule.WithDetails]. Details string `json:"details,omitempty"` diff --git a/pkg/govy/rule.go b/pkg/govy/rule.go index d0f0fd9..39e0870 100644 --- a/pkg/govy/rule.go +++ b/pkg/govy/rule.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "strings" + "sync" "text/template" "github.com/nobl9/govy/internal" @@ -34,6 +35,7 @@ func RuleToPointer[T any](rule Rule[T]) Rule[*T] { messageTemplate: rule.messageTemplate, examples: rule.examples, description: rule.description, + descriptionTpl: rule.descriptionTpl, planModifiers: rule.planModifiers, } } @@ -49,6 +51,7 @@ type Rule[T any] struct { messageTemplate *template.Template examples []string description string + descriptionTpl func() string planModifiers []RulePlanModifier } @@ -73,7 +76,7 @@ func (r Rule[T]) Validate(v T, opts ...ValidationOption) error { if len(r.message) > 0 { ev.Message = createErrorMessage(r.message, r.details, r.examples) } - ev.Description = r.description + ev.Description = r.resolveDescription() _ = ev.AddCode(r.errorCode) if vOpts.hideValue { ev.Message = hideStringValue(ev.Message, v) @@ -111,7 +114,7 @@ func (r Rule[T]) Validate(v T, opts ...ValidationOption) error { return &RuleError{ Message: buf.String(), Code: r.errorCode, - Description: r.description, + Description: r.resolveDescription(), } } msg := err.Error() @@ -121,7 +124,7 @@ func (r Rule[T]) Validate(v T, opts ...ValidationOption) error { ruleErr := &RuleError{ Message: createErrorMessage(msg, r.details, r.examples), Code: r.errorCode, - Description: r.description, + Description: r.resolveDescription(), } if vOpts.hideValue { ruleErr.Message = hideStringValue(ruleErr.Message, v) @@ -195,6 +198,32 @@ func (r Rule[T]) WithPlanModifiers(mods ...RulePlanModifier) Rule[T] { // It is used to enhance the [RulePlan], but otherwise does not appear in standard [RuleError.Error] output. func (r Rule[T]) WithDescription(description string) Rule[T] { r.description = description + r.descriptionTpl = nil + return r +} + +// WithDescriptionTemplate adds a description rendered from [template.Template] and [TemplateVars] to the rule. +// Rendering occurs once, when failed validation needs the description or [Plan] is called. +// Copies of the rule share the rendered description. +// +// WithDescriptionTemplate panics if the template is nil. +// Template execution errors are wrapped, cached, and replayed as panics. +func (r Rule[T]) WithDescriptionTemplate(tpl *template.Template, vars TemplateVars) Rule[T] { + if tpl == nil { + panic("description template must not be nil") + } + r.description = "" + r.descriptionTpl = sync.OnceValue(func() string { + var buf bytes.Buffer + if err := tpl.Execute(&buf, vars); err != nil { + panic(fmt.Errorf( + "failed to execute description template %q: %w", + tpl.Name(), + err, + )) + } + return buf.String() + }) return r } @@ -215,7 +244,7 @@ func (r Rule[T]) plan(builder planBuilder) { rulePlan := RulePlan{ ErrorCode: r.errorCode, Details: r.details, - Description: r.description, + Description: r.resolveDescription(), Conditions: builder.rulePlan.Conditions, Examples: r.examples, } @@ -226,6 +255,13 @@ func (r Rule[T]) plan(builder planBuilder) { *builder.path = append(*builder.path, builder) } +func (r Rule[T]) resolveDescription() string { + if r.descriptionTpl != nil { + return r.descriptionTpl() + } + return r.description +} + func createErrorMessage(message, details string, examples []string) string { if message == "" { return details diff --git a/pkg/govy/rule_test.go b/pkg/govy/rule_test.go index 2b8e502..bc52a7b 100644 --- a/pkg/govy/rule_test.go +++ b/pkg/govy/rule_test.go @@ -2,6 +2,9 @@ package govy_test import ( "errors" + "strings" + "sync" + "sync/atomic" "testing" "text/template" @@ -233,6 +236,256 @@ func TestRule_WithDescription(t *testing.T) { }, err) } +func TestRule_WithDescriptionTemplate(t *testing.T) { + t.Run("nil template panics during configuration", func(t *testing.T) { + defer func() { + if recovered := recover(); recovered != "description template must not be nil" { + t.Fatalf("unexpected panic: %v", recovered) + } + }() + _ = govy.NewRule(func(int) error { return nil }). + WithDescriptionTemplate(nil, govy.TemplateVars{}) + }) + + t.Run("deferred and cached", func(t *testing.T) { + var executions atomic.Int32 + tpl := template.Must(template.New("description"). + Funcs(template.FuncMap{ + "render": func(value string) string { + executions.Add(1) + return value + }, + }). + Parse("must be {{ render .Custom.Requirement }}")) + requirements := map[string]string{"Requirement": "positive"} + vars := govy.TemplateVars{Custom: requirements} + rule := govy.NewRule(func(v int) error { + if v < 0 { + return errors.New("invalid") + } + return nil + }).WithDescriptionTemplate(tpl, vars) + + assert.NoError(t, rule.Validate(1)) + assert.Equal(t, int32(0), executions.Load()) + + err := rule.Validate(-1) + assert.Require(t, assert.Error(t, err)) + assert.Equal(t, "must be positive", err.(*govy.RuleError).Description) + assert.Equal(t, int32(1), executions.Load()) + + err = rule.Validate(-1) + assert.Require(t, assert.Error(t, err)) + assert.Equal(t, "must be positive", err.(*govy.RuleError).Description) + + validator := govy.New( + govy.For(func(value int) int { return value }). + WithName("value"). + Rules(rule), + ) + plan, planErr := govy.Plan(validator) + assert.Require(t, assert.NoError(t, planErr)) + if len(plan.Properties) != 1 || len(plan.Properties[0].Rules) != 1 { + t.Fatalf("unexpected plan shape: %#v", plan) + } + assert.Equal(t, "must be positive", plan.Properties[0].Rules[0].Description) + assert.Equal(t, int32(1), executions.Load()) + }) + + t.Run("execution failure is cached and panics for every consumer", func(t *testing.T) { + var executions atomic.Int32 + renderErr := errors.New("render failed") + tpl := template.Must(template.New("description"). + Funcs(template.FuncMap{ + "fail": func() (string, error) { + executions.Add(1) + return "", renderErr + }, + }). + Parse("partial {{ fail }}")) + rule := govy.NewRule(func(int) error { return errors.New("invalid") }). + WithDescriptionTemplate(tpl, govy.TemplateVars{}) + validator := govy.New( + govy.For(func(value int) int { return value }). + WithName("value"). + Rules(rule), + ) + assertExecutionPanic := func(call func()) { + t.Helper() + defer func() { + recovered := recover() + executionErr, ok := recovered.(error) + if !ok { + t.Fatalf("unexpected panic: %v", recovered) + } + if !errors.Is(executionErr, renderErr) { + t.Fatalf("panic does not wrap the execution error: %v", executionErr) + } + if !strings.Contains( + executionErr.Error(), + `failed to execute description template "description"`, + ) { + t.Fatalf("unexpected panic: %s", executionErr) + } + }() + call() + } + + assertExecutionPanic(func() { + _ = rule.Validate(0) + }) + assertExecutionPanic(func() { + _, _ = govy.Plan(validator) + }) + assert.Equal(t, int32(1), executions.Load()) + }) + + t.Run("last description setter wins", func(t *testing.T) { + var executions atomic.Int32 + tpl := template.Must(template.New("description"). + Funcs(template.FuncMap{ + "render": func() string { + executions.Add(1) + return "templated" + }, + }). + Parse("{{ render }}")) + newRule := func() govy.Rule[int] { + return govy.NewRule(func(int) error { return errors.New("invalid") }) + } + + err := newRule(). + WithDescriptionTemplate(tpl, govy.TemplateVars{}). + WithDescription("eager"). + Validate(0) + assert.Require(t, assert.Error(t, err)) + assert.Equal(t, "eager", err.(*govy.RuleError).Description) + assert.Equal(t, int32(0), executions.Load()) + + err = newRule(). + WithDescription("eager"). + WithDescriptionTemplate(tpl, govy.TemplateVars{}). + Validate(0) + assert.Require(t, assert.Error(t, err)) + assert.Equal(t, "templated", err.(*govy.RuleError).Description) + assert.Equal(t, int32(1), executions.Load()) + }) + + t.Run("plan can resolve first", func(t *testing.T) { + var executions atomic.Int32 + tpl := template.Must(template.New("description"). + Funcs(template.FuncMap{ + "render": func() string { + executions.Add(1) + return "templated" + }, + }). + Parse("{{ render }}")) + rule := govy.NewRule(func(int) error { return errors.New("invalid") }). + WithDescriptionTemplate(tpl, govy.TemplateVars{}) + validator := govy.New( + govy.For(func(value int) int { return value }). + WithName("value"). + Rules(rule), + ) + + plan, planErr := govy.Plan(validator) + assert.Require(t, assert.NoError(t, planErr)) + if len(plan.Properties) != 1 || len(plan.Properties[0].Rules) != 1 { + t.Fatalf("unexpected plan shape: %#v", plan) + } + assert.Equal(t, "templated", plan.Properties[0].Rules[0].Description) + + err := rule.Validate(0) + assert.Require(t, assert.Error(t, err)) + assert.Equal(t, "templated", err.(*govy.RuleError).Description) + assert.Equal(t, int32(1), executions.Load()) + }) + + t.Run("copies share concurrent resolution", func(t *testing.T) { + var executions atomic.Int32 + tpl := template.Must(template.New("description"). + Funcs(template.FuncMap{ + "render": func() string { + executions.Add(1) + return "templated" + }, + }). + Parse("{{ render }}")) + rule := govy.NewRule(func(v int) error { + if v < 0 { + return errors.New("invalid") + } + return nil + }).WithDescriptionTemplate(tpl, govy.TemplateVars{}) + pointerRule := govy.RuleToPointer(rule) + + errs := make([]error, 64) + var wg sync.WaitGroup + for i := range errs { + wg.Go(func() { + if i%2 == 0 { + errs[i] = rule.Validate(-1) + return + } + value := -1 + errs[i] = pointerRule.Validate(&value) + }) + } + wg.Wait() + + for i, err := range errs { + ruleErr, ok := err.(*govy.RuleError) + if !ok { + t.Fatalf("error %d has type %T, expected *govy.RuleError", i, err) + } + assert.Equal(t, "templated", ruleErr.Description) + } + assert.Equal(t, int32(1), executions.Load()) + }) +} + +func TestRule_WithDescriptionTemplateErrorBranches(t *testing.T) { + for _, tc := range []struct { + name string + validate func(int) error + withMessageTemplate bool + }{ + { + name: "RuleError", + validate: func(int) error { return &govy.RuleError{Message: "invalid"} }, + }, + { + name: "RuleErrorTemplate", + validate: func(int) error { + return govy.NewRuleErrorTemplate(govy.TemplateVars{}) + }, + withMessageTemplate: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rule := govy.NewRule(tc.validate) + if tc.withMessageTemplate { + rule = rule.WithMessageTemplate( + template.Must(template.New("message").Parse("invalid")), + ) + } + rule = rule.WithDescriptionTemplate( + template.Must(template.New("description").Parse("templated")), + govy.TemplateVars{}, + ) + + err := rule.Validate(0) + assert.Require(t, assert.Error(t, err)) + ruleErr, ok := err.(*govy.RuleError) + if !ok { + t.Fatalf("expected *govy.RuleError, got %T", err) + } + assert.Equal(t, "templated", ruleErr.Description) + }) + } +} + func TestRule_WithExamples(t *testing.T) { r := govy.NewRule(func(v string) error { if v != "foo" && v != "bar" { diff --git a/pkg/rules/comparable.go b/pkg/rules/comparable.go index 191a1c6..aa8a8de 100644 --- a/pkg/rules/comparable.go +++ b/pkg/rules/comparable.go @@ -27,9 +27,9 @@ func EQ[T comparable](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeEqualTo). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })). + }). WithPlanModifiers(govy.RulePlanModifierValidValues(compared)) } @@ -48,9 +48,9 @@ func NEQ[T comparable](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeNotEqualTo). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })) + }) } // GT ensures the property's value is greater than the compared value. @@ -68,9 +68,9 @@ func GT[T cmp.Ordered](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeGreaterThan). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })) + }) } // GTE ensures the property's value is greater than or equal to the compared value. @@ -88,9 +88,9 @@ func GTE[T cmp.Ordered](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeGreaterThanOrEqualTo). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })) + }) } // LT ensures the property's value is less than the compared value. @@ -108,9 +108,9 @@ func LT[T cmp.Ordered](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeLessThan). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })) + }) } // LTE ensures the property's value is less than or equal to the compared value. @@ -128,9 +128,9 @@ func LTE[T cmp.Ordered](compared T) govy.Rule[T] { }). WithErrorCode(ErrorCodeLessThanOrEqualTo). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: compared, - })) + }) } // ComparisonFunc defines a shape for a function that compares two values. diff --git a/pkg/rules/duration.go b/pkg/rules/duration.go index c42bb07..7984148 100644 --- a/pkg/rules/duration.go +++ b/pkg/rules/duration.go @@ -25,7 +25,7 @@ func DurationPrecision(precision time.Duration) govy.Rule[time.Duration] { }). WithErrorCode(ErrorCodeDurationPrecision). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ PropertyValue: precision, - })) + }) } diff --git a/pkg/rules/forbidden.go b/pkg/rules/forbidden.go index 6c7adae..ecbdb9e 100644 --- a/pkg/rules/forbidden.go +++ b/pkg/rules/forbidden.go @@ -20,5 +20,5 @@ func Forbidden[T any]() govy.Rule[T] { }). WithErrorCode(ErrorCodeForbidden). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } diff --git a/pkg/rules/length.go b/pkg/rules/length.go index daf67b2..daa4172 100644 --- a/pkg/rules/length.go +++ b/pkg/rules/length.go @@ -30,10 +30,10 @@ func StringLength(minLen, maxLen int) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ MinLength: minLen, MaxLength: maxLen, - })) + }) } // StringMinLength ensures the string's length is greater than or equal to the limit. @@ -52,9 +52,9 @@ func StringMinLength(limit int) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMinLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } // StringMaxLength ensures the string's length is less than or equal to the limit. @@ -73,9 +73,9 @@ func StringMaxLength(limit int) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMaxLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } // SliceLength ensures the slice's length is between min and max (closed interval). @@ -100,10 +100,10 @@ func SliceLength[S ~[]E, E any](minLen, maxLen int) govy.Rule[S] { }). WithErrorCode(ErrorCodeSliceLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ MinLength: minLen, MaxLength: maxLen, - })) + }) } // SliceMinLength ensures the slice's length is greater than or equal to the limit. @@ -122,9 +122,9 @@ func SliceMinLength[S ~[]E, E any](limit int) govy.Rule[S] { }). WithErrorCode(ErrorCodeSliceMinLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } // SliceMaxLength ensures the slice's length is less than or equal to the limit. @@ -143,9 +143,9 @@ func SliceMaxLength[S ~[]E, E any](limit int) govy.Rule[S] { }). WithErrorCode(ErrorCodeSliceMaxLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } // MapLength ensures the map's length is between min and max (closed interval). @@ -170,10 +170,10 @@ func MapLength[M ~map[K]V, K comparable, V any](minLen, maxLen int) govy.Rule[M] }). WithErrorCode(ErrorCodeMapLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ MinLength: minLen, MaxLength: maxLen, - })) + }) } // MapMinLength ensures the map's length is greater than or equal to the limit. @@ -192,9 +192,9 @@ func MapMinLength[M ~map[K]V, K comparable, V any](limit int) govy.Rule[M] { }). WithErrorCode(ErrorCodeMapMinLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } // MapMaxLength ensures the map's length is less than or equal to the limit. @@ -213,9 +213,9 @@ func MapMaxLength[M ~map[K]V, K comparable, V any](limit int) govy.Rule[M] { }). WithErrorCode(ErrorCodeMapMaxLength). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: limit, - })) + }) } func enforceMinMaxLength(minLen, maxLen int) { diff --git a/pkg/rules/message_templates.go b/pkg/rules/message_templates.go deleted file mode 100644 index 3149bd0..0000000 --- a/pkg/rules/message_templates.go +++ /dev/null @@ -1,20 +0,0 @@ -package rules - -import ( - "bytes" - "log/slog" - "text/template" - - "github.com/nobl9/govy/internal/logging" - "github.com/nobl9/govy/pkg/govy" -) - -func mustExecuteTemplate(tpl *template.Template, vars govy.TemplateVars) string { - var buf bytes.Buffer - if err := tpl.Execute(&buf, vars); err != nil { - logging.Logger().Error("failed to execute message template", - slog.String("template", tpl.Name()), - slog.String("error", err.Error())) - } - return buf.String() -} diff --git a/pkg/rules/one_of.go b/pkg/rules/one_of.go index 439cd89..81d1e81 100644 --- a/pkg/rules/one_of.go +++ b/pkg/rules/one_of.go @@ -29,9 +29,9 @@ func OneOf[T comparable](values ...T) govy.Rule[T] { }). WithErrorCode(ErrorCodeOneOf). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: values, - })). + }). WithPlanModifiers(govy.RulePlanModifierValidValues(values...)) } @@ -53,15 +53,16 @@ func NotOneOf[T comparable](values ...T) govy.Rule[T] { }). WithErrorCode(ErrorCodeNotOneOf). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: values, - })) + }) } // OneOfProperties checks if at least one of the properties is set. // Property is considered set if its value is not empty (non-zero). func OneOfProperties[T any](getters map[string]func(parent T) any) govy.Rule[T] { tpl := messagetemplates.Get(messagetemplates.OneOfPropertiesTemplate) + descriptionKeys := collections.SortedKeys(getters) return govy.NewRule(func(parent T) error { for _, getter := range getters { @@ -77,9 +78,9 @@ func OneOfProperties[T any](getters map[string]func(parent T) any) govy.Rule[T] }). WithErrorCode(ErrorCodeOneOfProperties). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ - ComparisonValue: collections.SortedKeys(getters), - })) + WithDescriptionTemplate(tpl, govy.TemplateVars{ + ComparisonValue: descriptionKeys, + }) } type mutuallyExclusiveTemplateVars struct { diff --git a/pkg/rules/string.go b/pkg/rules/string.go index 7441c24..bbda235 100644 --- a/pkg/rules/string.go +++ b/pkg/rules/string.go @@ -36,7 +36,7 @@ func StringNotEmpty() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringNotEmpty). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringMatchRegexp ensures the property's value matches the regular expression. @@ -55,9 +55,9 @@ func StringMatchRegexp(re *regexp.Regexp) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMatchRegexp). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: re.String(), - })) + }) } // StringDenyRegexp ensures the property's value does not match the regular expression. @@ -76,9 +76,9 @@ func StringDenyRegexp(re *regexp.Regexp) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringDenyRegexp). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: re.String(), - })) + }) } // StringDNSLabel ensures the property's value is a valid DNS label as defined by [RFC 1123]. @@ -173,7 +173,7 @@ func StringMAC() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMAC). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringIP ensures property's value is a valid IP address. @@ -190,7 +190,7 @@ func StringIP() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringIP). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringIPv4 ensures property's value is a valid IPv4 address. @@ -207,7 +207,7 @@ func StringIPv4() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringIPv4). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringIPv6 ensures property's value is a valid IPv6 address. @@ -224,7 +224,7 @@ func StringIPv6() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringIPv6). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringCIDR ensures property's value is a valid CIDR notation IP address. @@ -241,7 +241,7 @@ func StringCIDR() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringCIDR). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringCIDRv4 ensures property's value is a valid CIDR notation IPv4 address. @@ -258,7 +258,7 @@ func StringCIDRv4() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringCIDRv4). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringCIDRv6 ensures property's value is a valid CIDR notation IPv6 address. @@ -275,7 +275,7 @@ func StringCIDRv6() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringCIDRv6). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringEIN ensures the property's value is a United States Employer Identification Number (EIN) @@ -293,7 +293,7 @@ func StringEIN() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringEIN). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } func isValidEIN(s string) bool { @@ -347,7 +347,7 @@ func StringSSN() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringSSN). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } func isValidSSN(s string) bool { @@ -389,9 +389,9 @@ func StringUUID() govy.Rule[string] { return nil }). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: uuidPattern, - })). + }). WithDetails("expected RFC-4122 compliant UUID string"). WithExamples( "00000000-0000-0000-0000-000000000000", @@ -417,7 +417,7 @@ func StringUUIDRFC4122() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringUUIDRFC4122). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringUUIDv3 ensures the property's value is a version 3 Universally Unique Identifier (UUID) @@ -435,7 +435,7 @@ func StringUUIDv3() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringUUIDv3). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringUUIDv4 ensures the property's value is a version 4 Universally Unique Identifier (UUID) @@ -453,7 +453,7 @@ func StringUUIDv4() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringUUIDv4). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringUUIDv5 ensures the property's value is a version 5 Universally Unique Identifier (UUID) @@ -471,7 +471,7 @@ func StringUUIDv5() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringUUIDv5). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } func isValidUUID(s string) bool { @@ -529,7 +529,7 @@ func StringULID() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringULID). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } func isValidULID(s string) bool { @@ -571,7 +571,7 @@ func StringMongoDBObjectID() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMongoDBObjectID). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringCreditCard ensures the property's value is a plausible digit-only @@ -590,7 +590,7 @@ func StringCreditCard() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringCreditCard). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringLuhnChecksum ensures the property's value is a digit-only string that @@ -608,7 +608,7 @@ func StringLuhnChecksum() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringLuhnChecksum). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringBIC ensures the property's value matches the current Business @@ -626,7 +626,7 @@ func StringBIC() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringBIC). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringBICISO93622014 ensures the property's value matches the ISO 9362:2014 @@ -644,7 +644,7 @@ func StringBICISO93622014() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringBICISO93622014). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringASCII ensures property's value contains only ASCII characters. @@ -666,7 +666,7 @@ func StringJSON() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringJSON). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringE164 ensures the property's value is a valid E.164 phone number. @@ -683,7 +683,7 @@ func StringE164() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringE164). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringSemver ensures the property's value is a valid Semantic Versioning 2.0.0 version. @@ -739,7 +739,7 @@ func StringBase64() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringBase64). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringBase64URL ensures the property's value is a URL-safe padded base64 string. @@ -758,7 +758,7 @@ func StringBase64URL() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringBase64URL). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringBase64RawURL ensures the property's value is a URL-safe base64 string without padding. @@ -777,7 +777,7 @@ func StringBase64RawURL() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringBase64RawURL). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringHexadecimal ensures the property's value is a hexadecimal string. @@ -795,7 +795,7 @@ func StringHexadecimal() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringHexadecimal). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } func decodesBase64(encoding *base64.Encoding, s string) bool { @@ -836,7 +836,7 @@ func StringMD5() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMD5). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringSHA256 ensures the property's value is a lowercase hexadecimal SHA-256 digest. @@ -853,7 +853,7 @@ func StringSHA256() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringSHA256). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringSHA384 ensures the property's value is a lowercase hexadecimal SHA-384 digest. @@ -870,7 +870,7 @@ func StringSHA384() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringSHA384). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringSHA512 ensures the property's value is a lowercase hexadecimal SHA-512 digest. @@ -887,7 +887,7 @@ func StringSHA512() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringSHA512). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringJWT ensures the property's value is a JSON Web Token (JWT) represented @@ -938,9 +938,9 @@ func StringContains(substrings ...string) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringContains). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: substrings, - })) + }) } // StringExcludes ensures the property's value does not contain any of the provided substrings. @@ -960,9 +960,9 @@ func StringExcludes(substrings ...string) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringExcludes). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: substrings, - })) + }) } // StringStartsWith ensures the property's value starts with one of the provided prefixes. @@ -987,9 +987,9 @@ func StringStartsWith(prefixes ...string) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringStartsWith). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: prefixes, - })) + }) } // StringEndsWith ensures the property's value ends with one of the provided suffixes. @@ -1014,9 +1014,9 @@ func StringEndsWith(suffixes ...string) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringEndsWith). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: suffixes, - })) + }) } // StringTitle ensures each word in a string starts with a capital letter. @@ -1044,7 +1044,7 @@ func StringTitle() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringTitle). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } type stringGitRefTemplateVars struct { @@ -1156,7 +1156,7 @@ func StringFileSystemPath() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringFileSystemPath). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringFilePath ensures the property's value is a file system path pointing to an existing file. @@ -1178,7 +1178,7 @@ func StringFilePath() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringFilePath). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringDirPath ensures the property's value is a file system path pointing to an existing directory. @@ -1200,7 +1200,7 @@ func StringDirPath() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringDirPath). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringMatchFileSystemPath ensures the property's value matches the provided file path pattern. @@ -1229,9 +1229,9 @@ func StringMatchFileSystemPath(pattern string) govy.Rule[string] { }). WithErrorCode(ErrorCodeStringMatchFileSystemPath). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: pattern, - })) + }) } // StringRegexp ensures the property's value is a valid regular expression. @@ -1255,7 +1255,7 @@ func StringRegexp() govy.Rule[string] { WithErrorCode(ErrorCodeStringRegexp). WithMessageTemplate(tpl). WithDetails(`the regular expression syntax must comply to RE2, it is described at https://golang.org/s/re2syntax, except for \C; for an overview of the syntax, see https://pkg.go.dev/regexp/syntax`). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringCrontab ensures the property's value is a valid crontab schedule expression. @@ -1277,7 +1277,7 @@ func StringCrontab() govy.Rule[string] { }). WithErrorCode(ErrorCodeStringCrontab). WithMessageTemplate(tpl). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringDateTime ensures the property's value is a valid date and time in the specified layout. @@ -1300,9 +1300,9 @@ func StringDateTime(layout string) govy.Rule[string] { WithErrorCode(ErrorCodeStringDateTime). WithMessageTemplate(tpl). WithDetails("date and time format follows Go's time layout, see https://pkg.go.dev/time#Layout for more details"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{ + WithDescriptionTemplate(tpl, govy.TemplateVars{ ComparisonValue: layout, - })) + }) } // StringTimeZone ensures the property's value is a valid time zone name which @@ -1336,7 +1336,7 @@ func StringTimeZone() govy.Rule[string] { WithErrorCode(ErrorCodeStringTimeZone). WithMessageTemplate(tpl). WithExamples("UTC", "America/New_York", "Europe/Warsaw"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringAlpha ensures the property's value consists only of ASCII letters. diff --git a/pkg/rules/string_locale_geo.go b/pkg/rules/string_locale_geo.go index 450526d..fe1512b 100644 --- a/pkg/rules/string_locale_geo.go +++ b/pkg/rules/string_locale_geo.go @@ -330,7 +330,7 @@ func StringBCP47LanguageTag() govy.Rule[string] { WithErrorCode(ErrorCodeStringBCP47LanguageTag). WithMessageTemplate(tpl). WithExamples("en", "en-US", "zh-Hant-TW"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringBCP47StrictLanguageTag ensures the property's value is a valid canonical BCP 47 language tag. @@ -348,7 +348,7 @@ func StringBCP47StrictLanguageTag() govy.Rule[string] { WithErrorCode(ErrorCodeStringBCP47StrictLanguageTag). WithMessageTemplate(tpl). WithExamples("en", "en-US", "zh-Hant-TW"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringISO3166Alpha2 ensures the property's value is a valid ISO 3166-1 alpha-2 country code. @@ -366,7 +366,7 @@ func StringISO3166Alpha2() govy.Rule[string] { WithErrorCode(ErrorCodeStringISO3166Alpha2). WithMessageTemplate(tpl). WithExamples("US", "PL", "JP"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringISO3166Alpha3 ensures the property's value is a valid ISO 3166-1 alpha-3 country code. @@ -384,7 +384,7 @@ func StringISO3166Alpha3() govy.Rule[string] { WithErrorCode(ErrorCodeStringISO3166Alpha3). WithMessageTemplate(tpl). WithExamples("USA", "POL", "JPN"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringISO3166Numeric ensures the property's value is a valid ISO 3166-1 numeric-3 country code. @@ -402,7 +402,7 @@ func StringISO3166Numeric() govy.Rule[string] { WithErrorCode(ErrorCodeStringISO3166Numeric). WithMessageTemplate(tpl). WithExamples("840", "616", "392"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringISO31662 ensures the property's value is a valid ISO 3166-2 country subdivision code. @@ -420,7 +420,7 @@ func StringISO31662() govy.Rule[string] { WithErrorCode(ErrorCodeStringISO31662). WithMessageTemplate(tpl). WithExamples("US-CA", "GB-ENG", "PL-14"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringISO4217 ensures the property's value is a valid ISO 4217 three-letter alphabetic currency code. @@ -438,7 +438,7 @@ func StringISO4217() govy.Rule[string] { WithErrorCode(ErrorCodeStringISO4217). WithMessageTemplate(tpl). WithExamples("USD", "EUR", "JPY"). - WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) + WithDescriptionTemplate(tpl, govy.TemplateVars{}) } // StringLatitude ensures the property's value is a decimal latitude coordinate between -90 and 90 degrees.