Skip to content

Commit

Permalink
current progress
Browse files Browse the repository at this point in the history
  • Loading branch information
nieomylnieja committed Sep 27, 2024
1 parent 58938cd commit 1c13769
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ jobs:
- uses: Slashgear/action-check-pr-title@v4.3.0
with:
regexp: "^(feat|fix|sec|infra|test|chore|doc): .{5,}"
helpMessage: "Example: 'feat: new pr title check BE-143' <- prefix, colon, space, PR title of at least 5 chars (with ticket number strongly suggested, but not mandatory)"
helpMessage: "Example: 'feat: new pr title' <- prefix, colon, space, PR title of at least 5 chars"
6 changes: 3 additions & 3 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- main
# pull_request event is required only for auto-labeler
# pull_request event is required only for autolabeler
# 'edited' event is required to account for initial invalid PR names
pull_request:
types: [opened, reopened, synchronize, edited]
Expand All @@ -17,7 +17,7 @@ jobs:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for auto-labeler
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
Expand All @@ -34,4 +34,4 @@ jobs:
uses: release-drafter/release-drafter@v6
- name: Add release notes to the draft
if: github.event_name == 'push'
run: .github/scripts/release-notes.sh ${{ steps.drafter.outputs.tag_name }}
run: .github/scripts/release-notes.sh ${{ steps.drafter.outputs.tag_name }}
25 changes: 25 additions & 0 deletions pkg/govy/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,31 @@ func (r RuleSetError) Error() string {
return b.String()
}

// NewRuleErrorTemplate creates a new [RuleErrorTemplate] with the given template variables.
// The variables can be of any type, most commonly it would be a struct or a map.
// These variables are then passed to [template.Template.Execute].
// For more details on Go templates see: https://pkg.go.dev/text/template.
// Example:
//
// return govy.NewRuleErrorTemplate(map[string]string{
// "Name": "my-property",
// "MaxLength": 2,
// })
func NewRuleErrorTemplate(vars any) RuleErrorTemplate {
return RuleErrorTemplate{Vars: vars}
}

// RuleErrorTemplate is a container for passing template variables under the guise of an error.
// It's not meant to be used directly as an error but rather
// unpacked by [Rule] in order to create a templated error message.
type RuleErrorTemplate struct {
Vars any
}

func (e RuleErrorTemplate) Error() string {
return fmt.Sprintf("%T should not be used directly", e)
}

// HasErrorCode checks if an error contains given [ErrorCode].
// It supports all govy errors.
func HasErrorCode(err error, code ErrorCode) bool {
Expand Down
41 changes: 32 additions & 9 deletions pkg/govy/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package govy

import (
"fmt"
"text/template"
)

// NewRule creates a new [Rule] instance.
Expand All @@ -13,19 +14,23 @@ func NewRule[T any](validate func(v T) error) Rule[T] {
// It evaluates the provided validation function and enhances it
// with optional [ErrorCode] and arbitrary details.
type Rule[T any] struct {
validate func(v T) error
errorCode ErrorCode
details string
message string
description string
validate func(v T) error
errorCode ErrorCode
details string
message string
messageTemplate *template.Template
description string
}

// Validate runs validation function on the provided value.
// It can handle different types of errors returned by the function:
// - *[RuleError], which details and [ErrorCode] are optionally extended with the ones defined by [Rule].
// - *[PropertyError], for each of its errors their [ErrorCode] is extended with the one defined by [Rule].
// - [*RuleError], which details and [ErrorCode] are optionally extended with the ones defined by [Rule].
// - [*PropertyError], for each of its errors their [ErrorCode] is extended with the one defined by [Rule].
// - [*RuleErrorTemplate], if message template was set with [Rule.WithMessageTemplate] or
// [Rule.WithMessageTemplateString] then the [RuleError.Message] is constructed from the provided template
// using variables passed inside [RuleErrorTemplate.Vars].
//
// By default, it will construct a new RuleError.
// By default, it will construct a new [*RuleError].
func (r Rule[T]) Validate(v T) error {
if err := r.validate(v); err != nil {
switch ev := err.(type) {
Expand All @@ -41,6 +46,7 @@ func (r Rule[T]) Validate(v T) error {
_ = e.AddCode(r.errorCode)
}
return ev
case *RuleErrorTemplate:
default:
msg := ev.Error()
if len(r.message) > 0 {
Expand All @@ -63,8 +69,9 @@ func (r Rule[T]) WithErrorCode(code ErrorCode) Rule[T] {
return r
}

// WithMessage overrides the returned [RuleError] error message with message.
// WithMessage overrides the returned [RuleError] error message.
func (r Rule[T]) WithMessage(format string, a ...any) Rule[T] {
r.messageTemplate = nil
if len(a) == 0 {
r.message = format
} else {
Expand All @@ -73,6 +80,19 @@ func (r Rule[T]) WithMessage(format string, a ...any) Rule[T] {
return r
}

// WithMessageTemplate overrides the returned [RuleError] error message using provided [template.Template].
func (r Rule[T]) WithMessageTemplate(tpl *template.Template) Rule[T] {
r.messageTemplate = tpl
return r
}

// WithMessageTemplateString overrides the returned [RuleError] error message using provided template string.
// The string is parsed into [template.Template], it panics if any error is encountered during parsing.
func (r Rule[T]) WithMessageTemplateString(tplStr string) Rule[T] {
tpl := template.Must(template.New("").Parse(tplStr))
return r.WithMessageTemplate(tpl)
}

// WithDetails adds details to the returned [RuleError] error message.
func (r Rule[T]) WithDetails(format string, a ...any) Rule[T] {
if len(a) == 0 {
Expand All @@ -90,6 +110,9 @@ func (r Rule[T]) WithDescription(description string) Rule[T] {
return r
}

func (r Rule[T]) getErrorMessage(err error) string {
}

Check failure on line 114 in pkg/govy/rule.go

View workflow job for this annotation

GitHub Actions / Coverage

missing return

Check failure on line 114 in pkg/govy/rule.go

View workflow job for this annotation

GitHub Actions / Coverage

missing return

func (r Rule[T]) plan(builder planBuilder) {
builder.rulePlan = RulePlan{
ErrorCode: r.errorCode,
Expand Down
2 changes: 1 addition & 1 deletion pkg/govy/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (emptyErr) Error() string { return "" }

// PropertyRules is responsible for validating a single property.
// It is a collection of rules, predicates, and other properties that define how the property should be validated.
// IT is the middle-level building block of the validation process,
// It is the middle-level building block of the validation process,
// aggregated by [Validator] and aggregating [Rule].
type PropertyRules[T, S any] struct {
name string
Expand Down
23 changes: 12 additions & 11 deletions pkg/rules/length.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,26 @@ import (
)

// StringLength ensures the string's length is between min and max (closed interval).
func StringLength(lower, upper int) govy.Rule[string] {
msg := "length must be between {{ .MinLength }} and {{ .MaxLength }}"
tpl := getMessageTemplate("StringLength", msg)
func StringLength(minLen, maxLen int) govy.Rule[string] {
tpl := getMessageTemplate(stringLengthTemplateKey)

return govy.NewRule(func(v string) error {
length := utf8.RuneCountInString(v)
if length < lower || length > upper {
return returnTemplatedError(tpl, func() templateVariables[string] {
return templateVariables[string]{
PropertyValue: v,
MinLength: lower,
MaxLength: upper,
}
if length < minLen || length > maxLen {
return govy.NewRuleErrorTemplate(ruleErrorTemplateVars[string]{
PropertyValue: v,
MinLength: minLen,
MaxLength: maxLen,
})
}
return nil
}).
WithErrorCode(ErrorCodeStringLength).
WithDescription(msg)
WithMessageTemplate(tpl).
WithDescription(mustExecuteTemplate(tpl, ruleErrorTemplateVars[string]{
MinLength: minLen,
MaxLength: maxLen,
}))
}

// StringMinLength ensures the string's length is greater than or equal to the limit.
Expand Down
48 changes: 32 additions & 16 deletions pkg/rules/message_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,71 @@ package rules

import (
"bytes"
"errors"
"fmt"
"log/slog"
"sync"
"text/template"
)

type templateVariables[T any] struct {
type templateKey int

const (
stringLengthTemplateKey templateKey = iota + 1
)

var rawMessageTemplates = map[templateKey]string{
stringLengthTemplateKey: "length must be between {{ .MinLength }} and {{ .MaxLength }}",
}

// ruleErrorTemplateVars lists all the possible variables that can be used builtin rules' message templates.
type ruleErrorTemplateVars[T any] struct {
PropertyValue T
Error string
MinLength int
MaxLength int
}

func returnTemplatedError[T any](tpl *template.Template, getVars func() templateVariables[T]) error {
func mustExecuteTemplate[T any](tpl *template.Template, vars ruleErrorTemplateVars[T]) string {
var buf bytes.Buffer
if err := tpl.Execute(&buf, getVars()); err != nil {
if err := tpl.Execute(&buf, vars); err != nil {
slog.Error("failed to execute message template",
slog.String("template", tpl.Name()),
slog.String("error", err.Error()))
}
return errors.New(buf.String())
return buf.String()
}

var messageTemplates = messageTemplatesMap{
tmpl: make(map[string]*template.Template),
var messageTemplatesCache = messageTemplatesMap{
tmpl: make(map[templateKey]*template.Template),
mu: sync.RWMutex{},
}

type messageTemplatesMap struct {
tmpl map[string]*template.Template
tmpl map[templateKey]*template.Template
mu sync.RWMutex
}

func (p *messageTemplatesMap) Lookup(name string) *template.Template {
func (p *messageTemplatesMap) Lookup(key templateKey) *template.Template {
p.mu.RLock()
defer p.mu.RUnlock()
return p.tmpl[name]
return p.tmpl[key]
}

func (p *messageTemplatesMap) Register(name string, tpl *template.Template) {
func (p *messageTemplatesMap) Register(key templateKey, tpl *template.Template) {
p.mu.Lock()
p.tmpl[name] = tpl
p.tmpl[key] = tpl
p.mu.Unlock()
}

func getMessageTemplate(name, msg string) *template.Template {
if tpl := messageTemplates.Lookup(name); tpl != nil {
func getMessageTemplate(key templateKey) *template.Template {
if tpl := messageTemplatesCache.Lookup(key); tpl != nil {
return tpl
}
tpl := template.Must(template.New(name).Parse(msg))
messageTemplates.Register(name, tpl)
text, ok := rawMessageTemplates[key]
if !ok {
panic(fmt.Sprintf("message template %q was not found", key))
}
tpl := template.Must(template.New("").Parse(text))
messageTemplatesCache.Register(key, tpl)
return tpl
}

0 comments on commit 1c13769

Please sign in to comment.