Skip to content

Commit e1fd661

Browse files
authored
feat: Add ability to mark property value as secret (validaiton) (#194)
* initial commit * add propertyRules test * revert changes * revert changes * change * to hidden
1 parent 0f00ce0 commit e1fd661

File tree

5 files changed

+102
-24
lines changed

5 files changed

+102
-24
lines changed

validation/comparable.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"golang.org/x/exp/constraints"
88
)
99

10-
func EqualTo[T comparable](compared T) Rule[T] {
10+
func EqualTo[T comparable](compared T) SingleRule[T] {
1111
return NewSingleRule(func(v T) error {
1212
if v != compared {
1313
return errors.Errorf(comparisonFmt, cmpEqualTo, compared)
@@ -16,7 +16,7 @@ func EqualTo[T comparable](compared T) Rule[T] {
1616
}).WithErrorCode(ErrorCodeEqualTo)
1717
}
1818

19-
func NotEqualTo[T comparable](compared T) Rule[T] {
19+
func NotEqualTo[T comparable](compared T) SingleRule[T] {
2020
return NewSingleRule(func(v T) error {
2121
if v == compared {
2222
return errors.Errorf(comparisonFmt, cmpNotEqualTo, compared)
@@ -25,22 +25,22 @@ func NotEqualTo[T comparable](compared T) Rule[T] {
2525
}).WithErrorCode(ErrorCodeNotEqualTo)
2626
}
2727

28-
func GreaterThan[T constraints.Ordered](n T) Rule[T] {
28+
func GreaterThan[T constraints.Ordered](n T) SingleRule[T] {
2929
return NewSingleRule(orderedComparisonRule(cmpGreaterThan, n)).
3030
WithErrorCode(ErrorCodeGreaterThan)
3131
}
3232

33-
func GreaterThanOrEqualTo[T constraints.Ordered](n T) Rule[T] {
33+
func GreaterThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T] {
3434
return NewSingleRule(orderedComparisonRule(cmpGreaterThanOrEqual, n)).
3535
WithErrorCode(ErrorCodeGreaterThanOrEqualTo)
3636
}
3737

38-
func LessThan[T constraints.Ordered](n T) Rule[T] {
38+
func LessThan[T constraints.Ordered](n T) SingleRule[T] {
3939
return NewSingleRule(orderedComparisonRule(cmpLessThan, n)).
4040
WithErrorCode(ErrorCodeLessThan)
4141
}
4242

43-
func LessThanOrEqualTo[T constraints.Ordered](n T) Rule[T] {
43+
func LessThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T] {
4444
return NewSingleRule(orderedComparisonRule(cmpLessThanOrEqual, n)).
4545
WithErrorCode(ErrorCodeLessThanOrEqualTo)
4646
}

validation/errors.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ func (e PropertyErrors) Error() string {
4141
return b.String()
4242
}
4343

44+
func (e PropertyErrors) HideValue() PropertyErrors {
45+
for _, err := range e {
46+
_ = err.HideValue()
47+
}
48+
return e
49+
}
50+
4451
func NewPropertyError(propertyName string, propertyValue interface{}, errs ...error) *PropertyError {
4552
return &PropertyError{
4653
PropertyName: propertyName,
@@ -70,13 +77,26 @@ func (e *PropertyError) Error() string {
7077
return b.String()
7178
}
7279

73-
const propertyNameSeparator = "."
80+
const (
81+
propertyNameSeparator = "."
82+
hiddenValue = "[hidden]"
83+
)
7484

7585
func (e *PropertyError) PrependPropertyName(name string) *PropertyError {
7686
e.PropertyName = concatStrings(name, e.PropertyName, propertyNameSeparator)
7787
return e
7888
}
7989

90+
// HideValue hides the property value from [PropertyError.Error] and also hides it from.
91+
func (e *PropertyError) HideValue() *PropertyError {
92+
sv := propertyValueString(e.PropertyValue)
93+
e.PropertyValue = ""
94+
for _, err := range e.Errors {
95+
_ = err.HideValue(sv)
96+
}
97+
return e
98+
}
99+
80100
// NewRuleError creates a new [RuleError] with the given message and optional error codes.
81101
// Error codes are added according to the rules defined by [RuleError.AddCode].
82102
func NewRuleError(message string, codes ...ErrorCode) *RuleError {
@@ -110,6 +130,12 @@ func (r *RuleError) AddCode(code ErrorCode) *RuleError {
110130
return r
111131
}
112132

133+
// HideValue replaces all occurrences of stringValue in the [RuleError.Message] with an '*' characters.
134+
func (r *RuleError) HideValue(stringValue string) *RuleError {
135+
r.Message = strings.ReplaceAll(r.Message, stringValue, hiddenValue)
136+
return r
137+
}
138+
113139
func concatStrings(pre, post, sep string) string {
114140
if pre == "" {
115141
return post

validation/rules.go

+41-17
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ func ForPointer[T, S any](getter PropertyGetter[*T, S]) PropertyRules[T, S] {
2727
// [Transformer] is only called if [PropertyGetter] returns a non-zero value.
2828
func Transform[T, N, S any](getter PropertyGetter[T, S], transform Transformer[T, N]) PropertyRules[N, S] {
2929
return PropertyRules[N, S]{
30-
getter: func(s S) (transformed N, err error) {
30+
transformGetter: func(s S) (transformed N, original any, err error) {
3131
v := getter(s)
3232
if err != nil {
33-
return transformed, err
33+
return transformed, nil, err
3434
}
3535
if isEmptyFunc(v) {
36-
return transformed, emptyErr{}
36+
return transformed, nil, emptyErr{}
3737
}
3838
transformed, err = transform(v)
3939
if err != nil {
40-
return transformed, NewPropertyError("", v, NewRuleError(err.Error(), ErrorCodeTransform))
40+
return transformed, v, NewRuleError(err.Error(), ErrorCodeTransform)
4141
}
42-
return transformed, nil
42+
return transformed, v, nil
4343
},
4444
}
4545
}
@@ -56,18 +56,21 @@ type Predicate[S any] func(S) bool
5656
type PropertyGetter[T, S any] func(S) T
5757

5858
type internalPropertyGetter[T, S any] func(S) (v T, err error)
59+
type internalTransformPropertyGetter[T, S any] func(S) (transformed T, original any, err error)
5960
type emptyErr struct{}
6061

6162
func (emptyErr) Error() string { return "" }
6263

6364
// PropertyRules is responsible for validating a single property.
6465
type PropertyRules[T, S any] struct {
65-
name string
66-
getter internalPropertyGetter[T, S]
67-
steps []interface{}
68-
required bool
69-
omitEmpty bool
70-
isPointer bool
66+
name string
67+
getter internalPropertyGetter[T, S]
68+
transformGetter internalTransformPropertyGetter[T, S]
69+
steps []interface{}
70+
required bool
71+
omitEmpty bool
72+
hideValue bool
73+
isPointer bool
7174
}
7275

7376
// Validate validates the property value using provided rules.
@@ -80,6 +83,9 @@ func (r PropertyRules[T, S]) Validate(st S) PropertyErrors {
8083
)
8184
propValue, skip, err := r.getValue(st)
8285
if err != nil {
86+
if r.hideValue {
87+
err = err.HideValue()
88+
}
8389
return err
8490
}
8591
if skip {
@@ -122,6 +128,9 @@ loop:
122128
allErrors = append(allErrors, NewPropertyError(r.name, propValue, ruleErrors...))
123129
}
124130
if len(allErrors) > 0 {
131+
if r.hideValue {
132+
allErrors = allErrors.HideValue()
133+
}
125134
return allErrors
126135
}
127136
return nil
@@ -157,6 +166,11 @@ func (r PropertyRules[T, S]) OmitEmpty() PropertyRules[T, S] {
157166
return r
158167
}
159168

169+
func (r PropertyRules[T, S]) HideValue() PropertyRules[T, S] {
170+
r.hideValue = true
171+
return r
172+
}
173+
160174
type stopOnErrorStep uint8
161175

162176
func (r PropertyRules[T, S]) StopOnError() PropertyRules[T, S] {
@@ -172,16 +186,26 @@ func appendSteps[T any](slice []interface{}, steps []T) []interface{} {
172186
}
173187

174188
func (r PropertyRules[T, S]) getValue(st S) (v T, skip bool, errs PropertyErrors) {
175-
v, err := r.getter(st)
189+
var (
190+
err error
191+
originalValue any
192+
)
193+
if r.transformGetter != nil {
194+
v, originalValue, err = r.transformGetter(st)
195+
} else {
196+
v, err = r.getter(st)
197+
}
176198
_, isEmptyError := err.(emptyErr)
177199
// Any error other than [emptyErr] is considered critical, we don't proceed with validation.
178200
if err != nil && !isEmptyError {
179-
if propErr, ok := err.(*PropertyError); ok {
180-
// Make sure the name is set to the current property name.
181-
propErr.PropertyName = r.name
182-
return v, false, PropertyErrors{propErr}
201+
// If the value was transformed, we need to set the property value to the original, pre-transformed one.
202+
var propValue interface{}
203+
if HasErrorCode(err, ErrorCodeTransform) {
204+
propValue = originalValue
205+
} else {
206+
propValue = v
183207
}
184-
return v, false, PropertyErrors{NewPropertyError(r.name, nil, err)}
208+
return v, false, PropertyErrors{NewPropertyError(r.name, propValue, err)}
185209
}
186210
isEmpty := isEmptyError || (!r.isPointer && isEmptyFunc(v))
187211
if r.required && isEmpty {

validation/rules_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,21 @@ func TestPropertyRules(t *testing.T) {
145145
Errors: []*RuleError{{Message: expectedErrs.Error()}},
146146
}, errs[0])
147147
})
148+
149+
t.Run("hide value", func(t *testing.T) {
150+
expectedErr := errors.New("oh no! here's the value: 'secret'")
151+
r := For(func(m mockStruct) string { return "secret" }).
152+
WithName("test.path").
153+
HideValue().
154+
Rules(NewSingleRule(func(v string) error { return expectedErr }))
155+
errs := r.Validate(mockStruct{})
156+
require.Len(t, errs, 1)
157+
assert.Equal(t, &PropertyError{
158+
PropertyName: "test.path",
159+
PropertyValue: "",
160+
Errors: []*RuleError{{Message: "oh no! here's the value: '[hidden]'"}},
161+
}, errs[0])
162+
})
148163
}
149164

150165
func TestForPointer(t *testing.T) {
@@ -260,6 +275,17 @@ func TestTransform(t *testing.T) {
260275
assert.EqualError(t, errs, expectedErrorOutput(t, "property_error_transform.txt"))
261276
assert.True(t, HasErrorCode(errs, ErrorCodeTransform))
262277
})
278+
t.Run("fail transformation with hidden value", func(t *testing.T) {
279+
getter := func(s string) string { return s }
280+
transformed := Transform(getter, strconv.Atoi).
281+
WithName("prop").
282+
HideValue().
283+
Rules(GreaterThan(123))
284+
errs := transformed.Validate("secret!")
285+
assert.Len(t, errs, 1)
286+
assert.EqualError(t, errs, expectedErrorOutput(t, "property_error_transform_with_hidden_value.txt"))
287+
assert.True(t, HasErrorCode(errs, ErrorCodeTransform))
288+
})
263289
}
264290

265291
func ptr[T any](v T) *T { return &v }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- 'prop':
2+
- strconv.Atoi: parsing "[hidden]": invalid syntax

0 commit comments

Comments
 (0)