Skip to content

Commit 11bffbc

Browse files
feat: Support day unit with edgex-dto-duration validation tag (#1014)
* feat: Support day unit in edgex-dto-duration validation tag Resolves #1013. Support day unit in edgex-dto-duration validation tag. Signed-off-by: Lindsey Cheng <[email protected]> * fix: Handle repeated days in duration string Handle repeated days in duration string. Signed-off-by: Lindsey Cheng <[email protected]> --------- Signed-off-by: Lindsey Cheng <[email protected]>
1 parent 22c22c6 commit 11bffbc

File tree

2 files changed

+209
-16
lines changed

2 files changed

+209
-16
lines changed

common/validator.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"reflect"
1313
"regexp"
14+
"strconv"
1415
"strings"
1516
"time"
1617

@@ -135,40 +136,92 @@ func getErrorMessage(e validator.FieldError) string {
135136
// ex. edgex-dto-duration=10ms0x2C24h - 10ms represents the minimum Duration and 24h represents the maximum Duration
136137
// 0x2c is the UTF-8 hex encoding of comma (,) as the min/max value separator
137138
func ValidateDuration(fl validator.FieldLevel) bool {
138-
duration, err := time.ParseDuration(fl.Field().String())
139-
if err != nil {
139+
durationStr := fl.Field().String()
140+
141+
valid, duration := parseDurationWithDay(durationStr)
142+
if !valid {
140143
return false
141144
}
142145

143146
// if min/max are defined from tag param, check if the duration value is in the duration range
144147
param := fl.Param()
145-
var min, max time.Duration
146148
if param != "" {
147149
params := strings.Split(param, CommaSeparator)
148150
if len(params) > 0 {
149-
min, err = time.ParseDuration(params[0])
150-
if err != nil {
151-
return false
152-
}
153-
if duration < min {
154-
// the duration value is smaller than the min
155-
return false
156-
}
157-
if len(params) > 1 {
158-
max, err = time.ParseDuration(params[1])
159-
if err != nil {
151+
// Check if minimum value is defined from the tag param
152+
if params[0] != "" {
153+
valid, minDuration := parseDurationWithDay(params[0])
154+
if !valid {
160155
return false
161156
}
162-
if duration > max {
163-
// the duration value is larger than the max
157+
if duration < minDuration {
158+
// the duration value is smaller than the min
164159
return false
165160
}
166161
}
162+
163+
if len(params) > 1 {
164+
// Check if maximum value is defined from the tag param
165+
if params[1] != "" {
166+
valid, maxDuration := parseDurationWithDay(params[1])
167+
if !valid {
168+
return false
169+
}
170+
if duration > maxDuration {
171+
// the duration value is larger than the max
172+
return false
173+
}
174+
}
175+
}
167176
}
168177
}
169178
return true
170179
}
171180

181+
// parseDurationWithDay extends duration string parsing to support the "d" (day) unit.
182+
// It returns a boolean indicating whether the string is valid, along with the corresponding Duration value.
183+
func parseDurationWithDay(durationStr string) (bool, time.Duration) {
184+
// Duration string should not be empty
185+
if durationStr == "" {
186+
return false, time.Duration(0)
187+
}
188+
189+
var totalDuration time.Duration
190+
191+
// Regex to find all day fragments like "2.2d", "1.5d", "1d1d", etc.
192+
re := regexp.MustCompile(`([\d.]+)d`)
193+
matches := re.FindAllStringSubmatch(durationStr, -1)
194+
195+
// Sum up all matched day durations
196+
for _, match := range matches {
197+
if len(match) != 2 {
198+
continue
199+
}
200+
day, err := strconv.ParseFloat(match[1], 64)
201+
if err != nil {
202+
return false, 0
203+
}
204+
// Converts days to hours and adds up to the total duration
205+
totalDuration += time.Duration(day * float64(24*time.Hour))
206+
}
207+
208+
// Remove all day fragments from the original string to get the rest
209+
// so we're left with only the remaining duration (e.g., "5h30m")
210+
durationStr = re.ReplaceAllString(durationStr, "")
211+
durationStr = strings.TrimSpace(durationStr)
212+
213+
// Parse the remaining standard duration if any
214+
if durationStr != "" {
215+
remainingDuration, err := time.ParseDuration(durationStr)
216+
if err != nil {
217+
return false, totalDuration
218+
}
219+
220+
totalDuration += remainingDuration
221+
}
222+
return true, totalDuration
223+
}
224+
172225
// ValidateDtoUuid used to check the UpdateDTO uuid pointer value
173226
// Currently, required_without can not correct work with other tag, so write custom tag instead.
174227
// Issue can refer to https://github.com/go-playground/validator/issues/624

common/validator_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package common
77

88
import (
99
"testing"
10+
"time"
1011

12+
"github.com/go-playground/validator/v10"
1113
"github.com/stretchr/testify/require"
1214
)
1315

@@ -61,3 +63,141 @@ func TestValidate(t *testing.T) {
6163
})
6264
}
6365
}
66+
67+
// TestValidateDuration tests the ValidateDuration function
68+
func TestValidateDuration(t *testing.T) {
69+
validate := validator.New()
70+
err := validate.RegisterValidation(dtoDurationTag, ValidateDuration)
71+
require.NoError(t, err)
72+
73+
tests := []struct {
74+
name string
75+
field any
76+
expectedErr bool
77+
}{
78+
{"valid - duration string without day", "10h30m", false},
79+
{"valid - duration string with day", "1d", false},
80+
{"valid - duration string with day and hour", "1d5h", false},
81+
{"valid - duration string with fractional days and hour", "2.5d500ms", false},
82+
{"valid - duration string with hour and day at last", "30m2d", false},
83+
{"valid - duration string with min in ms and max in h", "30ms", false},
84+
{"invalid - duration string with valid day and invalid minute", "1dxxm", true},
85+
{"invalid - duration string with invalid day and valid second", "xxd30s", true},
86+
}
87+
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
err = validate.Var(tt.field, dtoDurationTag)
91+
if tt.expectedErr {
92+
require.Error(t, err)
93+
} else {
94+
require.NoError(t, err)
95+
}
96+
})
97+
}
98+
}
99+
100+
// TestValidateDuration_WithMinMax tests the ValidateDuration function with min or max defined in the edgex-dto-duration annotation tag
101+
func TestValidateDuration_WithMinMax(t *testing.T) {
102+
validate := validator.New()
103+
err := validate.RegisterValidation(dtoDurationTag, ValidateDuration)
104+
require.NoError(t, err)
105+
106+
tests := []struct {
107+
name string
108+
field any
109+
min string
110+
max string
111+
expectedErr bool
112+
}{
113+
{"valid - duration string exceeds min with day", "12h30m", "0.5d", "", false},
114+
{"valid - duration string exceeds min without day", "10h30m", "10h", "", false},
115+
{"valid - duration string with day exceeds min with day", "1d", "0.5d", "", false},
116+
{"valid - duration string with day and hour less than max with day", "1d5h", "", "2d", false},
117+
{"valid - duration string with fractional days and hour less than max with day", "2.5d500ms", "", "3d", false},
118+
{"invalid - duration string with day less than than min with day", "1d5h", "2d", "3d", true},
119+
{"invalid - duration string with day exceeds the max with day", "4d6h", "2d", "3d", true},
120+
{"invalid - duration string without day less than than min without day", "5ms10us", "6ms", "30ms", true},
121+
{"invalid - duration string without day exceeds the max without day", "300ns", "100ns", "200ns", true},
122+
{"invalid - duration string with invalid min", "1d", "xxd", "", true},
123+
{"invalid - duration string with invalid max", "1d", "", "1dxxs", true},
124+
}
125+
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
tagValue := dtoDurationTag + "="
129+
if tt.min != "" {
130+
tagValue += tt.min
131+
}
132+
if tt.max != "" {
133+
tagValue += "0x2C" + tt.max
134+
}
135+
err = validate.Var(tt.field, tagValue)
136+
if tt.expectedErr {
137+
require.Error(t, err)
138+
} else {
139+
require.NoError(t, err)
140+
}
141+
})
142+
}
143+
}
144+
145+
// TestParseDurationWithDay tests the parseDurationWithDay function
146+
func TestParseDurationWithDay(t *testing.T) {
147+
durStr1 := "10h30m"
148+
expectedDur1, err := time.ParseDuration(durStr1)
149+
require.NoError(t, err)
150+
151+
durStr2 := "1d"
152+
expectedDur2, err := time.ParseDuration("24h")
153+
require.NoError(t, err)
154+
155+
durStr3 := "1d5h"
156+
expectedDur3, err := time.ParseDuration("29h")
157+
require.NoError(t, err)
158+
159+
durStr4 := "2.5d500ms"
160+
expectedDur4, err := time.ParseDuration("60h500ms")
161+
require.NoError(t, err)
162+
163+
durStr5 := "30m2d"
164+
expectedDur5, err := time.ParseDuration("30m48h")
165+
require.NoError(t, err)
166+
167+
durStr6 := "30ms"
168+
expectedDur6, err := time.ParseDuration(durStr6)
169+
require.NoError(t, err)
170+
171+
durStr7 := "2.2d1.1d20m3h1h10m"
172+
expectedDur7, err := time.ParseDuration("83.2h30m")
173+
require.NoError(t, err)
174+
175+
tests := []struct {
176+
name string
177+
durString string
178+
expectedResult bool
179+
expectedDuration time.Duration
180+
}{
181+
{"valid - duration string without day", durStr1, true, expectedDur1},
182+
{"valid - duration string with day", durStr2, true, expectedDur2},
183+
{"valid - duration string with day and hour", durStr3, true, expectedDur3},
184+
{"valid - duration string with fractional days and hour", durStr4, true, expectedDur4},
185+
{"valid - duration string with hour and day at last", durStr5, true, expectedDur5},
186+
{"valid - duration string with min in ms and max in h", durStr6, true, expectedDur6},
187+
{"valid - duration string with repeated day and other time units", durStr7, true, expectedDur7},
188+
{"invalid - duration string with valid day and invalid minute", "1dxxm", false, 0},
189+
{"invalid - duration string with invalid day and valid second", "xxd30s", false, 0},
190+
{"invalid duration string", "abc", false, 0},
191+
{"invalid - empty duration string", "", false, 0},
192+
}
193+
194+
for _, tt := range tests {
195+
t.Run(tt.name, func(t *testing.T) {
196+
result, duration := parseDurationWithDay(tt.durString)
197+
require.Equal(t, tt.expectedResult, result)
198+
if tt.expectedResult {
199+
require.Equal(t, tt.expectedDuration, duration)
200+
}
201+
})
202+
}
203+
}

0 commit comments

Comments
 (0)