Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions pkg/govy/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/govy/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
44 changes: 40 additions & 4 deletions pkg/govy/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"strings"
"sync"
"text/template"

"github.com/nobl9/govy/internal"
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -49,6 +51,7 @@ type Rule[T any] struct {
messageTemplate *template.Template
examples []string
description string
descriptionTpl func() string
planModifiers []RulePlanModifier
}

Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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,
}
Expand All @@ -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
Expand Down
Loading
Loading