feat: generate fields validation and use it when sending requests to API (#7)

* feat: generate fields validation and use it when issuing requests to API with soft (default) or hard modes

* chore: apply linter fixes

* feat: enable field validation on int fields

* feat: add validation for ^[\w]+$ fields

* feat: add validation for MAC address fields

* fix: trim wrappers for all comments

* feat: add validation for IPv4, IPv6 and IP(IPv4/IPv6) fields

* feat: add validation for numeric, non-zero based fields

* fix: one of validation can contain dot (.) sign in values

* feat: add second notation of MAC address validation

* fix: one of validation can start with ^( and end with )$

* feat: add option to disable validation and use soft validation by default

* chore: fix test

* docs: add readme about client-side validation
This commit is contained in:
Mateusz Filipowicz
2025-02-09 21:08:21 +01:00
committed by GitHub
parent 9f4fe33d07
commit 53bb1a13b9
55 changed files with 1326 additions and 588 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{{- $structName := .StructName }}
{{ define "field" }}
{{ .FieldName }} {{ if .IsArray }}[]{{end}}{{ .FieldType }} `json:"{{ .JSONName }}{{ if .OmitEmpty }},omitempty{{ end }}"` {{ if .FieldValidation }}// {{ .FieldValidation }}{{ end }} {{- end }}
{{ .FieldName }} {{ if .IsArray }}[]{{end}}{{ .FieldType }} `json:"{{ .JSONName }}{{ if .OmitEmpty }},omitempty{{ end }}"{{if .FieldValidation }} {{ .FieldValidation }}{{ end }}` {{ if .FieldValidationComment }}// {{ .FieldValidationComment }}{{ end }} {{- end }}
{{ define "field-customUnmarshalType" }}
{{- if eq .CustomUnmarshalType "" }}{{else}}
{{ .FieldName }} {{ if .IsArray }}[]{{end}}{{ .CustomUnmarshalType }} `json:"{{ .JSONName }}"`{{ end }} {{- end }}
+53 -49
View File
@@ -94,19 +94,20 @@ type Resource struct {
}
type FieldInfo struct {
FieldName string
JSONName string
FieldType string
FieldValidation string
OmitEmpty bool
IsArray bool
Fields map[string]*FieldInfo
CustomUnmarshalType string
CustomUnmarshalFunc string
FieldName string
JSONName string
FieldType string
FieldValidation string
FieldValidationComment string
OmitEmpty bool
IsArray bool
Fields map[string]*FieldInfo
CustomUnmarshalType string
CustomUnmarshalFunc string
}
func NewResource(structName string, resourcePath string) *Resource {
baseType := NewFieldInfo(structName, resourcePath, "struct", "", false, false, "")
baseType := NewFieldInfo(structName, resourcePath, "struct", "", "", false, false, "")
resource := &Resource{
StructName: structName,
ResourcePath: resourcePath,
@@ -122,14 +123,14 @@ func NewResource(structName string, resourcePath string) *Resource {
//
// This hack is here for stability of the generated code, but can be removed if desired.
baseType.Fields = map[string]*FieldInfo{
" ID": NewFieldInfo("ID", "_id", "string", "", true, false, ""),
" SiteID": NewFieldInfo("SiteID", "site_id", "string", "", true, false, ""),
" ID": NewFieldInfo("ID", "_id", "string", "", "", true, false, ""),
" SiteID": NewFieldInfo("SiteID", "site_id", "string", "", "", true, false, ""),
" _Spacer": nil,
" Hidden": NewFieldInfo("Hidden", "attr_hidden", "bool", "", true, false, ""),
" HiddenID": NewFieldInfo("HiddenID", "attr_hidden_id", "string", "", true, false, ""),
" NoDelete": NewFieldInfo("NoDelete", "attr_no_delete", "bool", "", true, false, ""),
" NoEdit": NewFieldInfo("NoEdit", "attr_no_edit", "bool", "", true, false, ""),
" Hidden": NewFieldInfo("Hidden", "attr_hidden", "bool", "", "", true, false, ""),
" HiddenID": NewFieldInfo("HiddenID", "attr_hidden_id", "string", "", "", true, false, ""),
" NoDelete": NewFieldInfo("NoDelete", "attr_no_delete", "bool", "", "", true, false, ""),
" NoEdit": NewFieldInfo("NoEdit", "attr_no_edit", "bool", "", "", true, false, ""),
" _Spacer": nil,
" _Spacer": nil,
@@ -138,38 +139,39 @@ func NewResource(structName string, resourcePath string) *Resource {
switch {
case resource.IsSetting():
resource.ResourcePath = strcase.ToSnake(strings.TrimPrefix(structName, "Setting"))
baseType.Fields[" Key"] = NewFieldInfo("Key", "key", "string", "", false, false, "")
baseType.Fields[" Key"] = NewFieldInfo("Key", "key", "string", "", "", false, false, "")
if resource.StructName == "SettingUsg" {
// Removed in v7, retaining for backwards compatibility
baseType.Fields["MdnsEnabled"] = NewFieldInfo("MdnsEnabled", "mdns_enabled", "bool", "", false, false, "")
baseType.Fields["MdnsEnabled"] = NewFieldInfo("MdnsEnabled", "mdns_enabled", "bool", "", "", false, false, "")
}
case resource.StructName == "Device":
baseType.Fields[" MAC"] = NewFieldInfo("MAC", "mac", "string", "", true, false, "")
baseType.Fields["Adopted"] = NewFieldInfo("Adopted", "adopted", "bool", "", false, false, "")
baseType.Fields["Model"] = NewFieldInfo("Model", "model", "string", "", true, false, "")
baseType.Fields["State"] = NewFieldInfo("State", "state", "DeviceState", "", false, false, "")
baseType.Fields["Type"] = NewFieldInfo("Type", "type", "string", "", true, false, "")
baseType.Fields[" MAC"] = NewFieldInfo("MAC", "mac", "string", createValidations(validation{v: mac}), "", true, false, "")
baseType.Fields["Adopted"] = NewFieldInfo("Adopted", "adopted", "bool", "", "", false, false, "")
baseType.Fields["Model"] = NewFieldInfo("Model", "model", "string", "", "", true, false, "")
baseType.Fields["State"] = NewFieldInfo("State", "state", "DeviceState", "", "", false, false, "")
baseType.Fields["Type"] = NewFieldInfo("Type", "type", "string", "", "", true, false, "")
case resource.StructName == "User":
baseType.Fields[" IP"] = NewFieldInfo("IP", "ip", "string", "non-generated field", true, false, "")
baseType.Fields[" DevIdOverride"] = NewFieldInfo("DevIdOverride", "dev_id_override", "int", "non-generated field", true, false, "")
baseType.Fields[" IP"] = NewFieldInfo("IP", "ip", "string", createValidations(validation{v: ip}), "non-generated field", true, false, "")
baseType.Fields[" DevIdOverride"] = NewFieldInfo("DevIdOverride", "dev_id_override", "int", "", "non-generated field", true, false, "")
case resource.StructName == "WLAN":
// this field removed in v6, retaining for backwards compatibility
baseType.Fields["WLANGroupID"] = NewFieldInfo("WLANGroupID", "wlangroup_id", "string", "", false, false, "")
baseType.Fields["WLANGroupID"] = NewFieldInfo("WLANGroupID", "wlangroup_id", "string", "", "", false, false, "")
}
return resource
}
func NewFieldInfo(fieldName string, jsonName string, fieldType string, fieldValidation string, omitempty bool, isArray bool, customUnmarshalType string) *FieldInfo {
func NewFieldInfo(fieldName, jsonName, fieldType, fieldValidation, fieldValidationComment string, omitempty bool, isArray bool, customUnmarshalType string) *FieldInfo {
return &FieldInfo{
FieldName: fieldName,
JSONName: jsonName,
FieldType: fieldType,
FieldValidation: fieldValidation,
OmitEmpty: omitempty,
IsArray: isArray,
CustomUnmarshalType: customUnmarshalType,
FieldName: fieldName,
JSONName: jsonName,
FieldType: fieldType,
FieldValidation: fieldValidation,
FieldValidationComment: fieldValidationComment,
OmitEmpty: omitempty,
IsArray: isArray,
CustomUnmarshalType: customUnmarshalType,
}
}
@@ -207,7 +209,7 @@ func (r *Resource) fieldInfoFromValidation(name string, validation interface{})
switch validation := validation.(type) {
case []interface{}:
if len(validation) == 0 {
fieldInfo = NewFieldInfo(fieldName, name, "string", "", false, true, "")
fieldInfo = NewFieldInfo(fieldName, name, "string", "", "", false, true, "")
err := r.FieldProcessor(fieldName, fieldInfo)
return fieldInfo, err
}
@@ -229,7 +231,7 @@ func (r *Resource) fieldInfoFromValidation(name string, validation interface{})
case map[string]interface{}:
typeName := r.StructName + fieldName
result := NewFieldInfo(fieldName, name, typeName, "", true, false, "")
result := NewFieldInfo(fieldName, name, typeName, "", "", true, false, "")
result.Fields = make(map[string]*FieldInfo)
for name, fv := range validation {
@@ -246,19 +248,19 @@ func (r *Resource) fieldInfoFromValidation(name string, validation interface{})
return result, err
case string:
fieldValidation := validation
fieldValidationComment := validation
normalized := normalizeValidation(validation)
omitEmpty := false
switch {
case normalized == "falsetrue" || normalized == "truefalse":
fieldInfo = NewFieldInfo(fieldName, name, "bool", "", omitEmpty, false, "")
fieldInfo = NewFieldInfo(fieldName, name, "bool", "", "", omitEmpty, false, "")
return fieldInfo, r.FieldProcessor(fieldName, fieldInfo)
default:
if _, err := strconv.ParseFloat(normalized, 64); err == nil {
if normalized == "09" || normalized == "09.09" {
fieldValidation = ""
fieldValidationComment = ""
}
if strings.Contains(normalized, ".") {
@@ -267,12 +269,13 @@ func (r *Resource) fieldInfoFromValidation(name string, validation interface{})
}
omitEmpty = true
fieldInfo = NewFieldInfo(fieldName, name, "float64", fieldValidation, omitEmpty, false, "")
fieldInfo = NewFieldInfo(fieldName, name, "float64", "", fieldValidationComment, omitEmpty, false, "")
return fieldInfo, r.FieldProcessor(fieldName, fieldInfo)
}
fieldValidation := defineFieldValidation(fieldValidationComment)
omitEmpty = true
fieldInfo = NewFieldInfo(fieldName, name, "int", fieldValidation, omitEmpty, false, "")
fieldInfo = NewFieldInfo(fieldName, name, "int", fieldValidation, fieldValidationComment, omitEmpty, false, "")
fieldInfo.CustomUnmarshalType = "emptyStringInt"
return fieldInfo, r.FieldProcessor(fieldName, fieldInfo)
}
@@ -281,8 +284,9 @@ func (r *Resource) fieldInfoFromValidation(name string, validation interface{})
log.Tracef("normalize %q to %q", validation, normalized)
}
fieldValidation := defineFieldValidation(fieldValidationComment)
omitEmpty = omitEmpty || (!strings.Contains(validation, "^$") && !strings.HasSuffix(fieldName, "ID"))
fieldInfo = NewFieldInfo(fieldName, name, "string", fieldValidation, omitEmpty, false, "")
fieldInfo = NewFieldInfo(fieldName, name, "string", fieldValidation, fieldValidationComment, omitEmpty, false, "")
return fieldInfo, r.FieldProcessor(fieldName, fieldInfo)
}
@@ -444,14 +448,14 @@ func generateCode(fieldsDir string, outDir string) error {
return nil
}
case "SettingMgmt":
sshKeyField := NewFieldInfo(resource.StructName+"XSshKeys", "x_ssh_keys", "struct", "", false, false, "")
sshKeyField := NewFieldInfo(resource.StructName+"XSshKeys", "x_ssh_keys", "struct", "", "", false, false, "")
sshKeyField.Fields = map[string]*FieldInfo{
"name": NewFieldInfo("Name", "name", "string", "", false, false, ""),
"keyType": NewFieldInfo("KeyType", "type", "string", "", false, false, ""),
"key": NewFieldInfo("Key", "key", "string", "", false, false, ""),
"comment": NewFieldInfo("Comment", "comment", "string", "", false, false, ""),
"date": NewFieldInfo("Date", "date", "string", "", false, false, ""),
"fingerprint": NewFieldInfo("Fingerprint", "fingerprint", "string", "", false, false, ""),
"name": NewFieldInfo("Name", "name", "string", "", "", false, false, ""),
"keyType": NewFieldInfo("KeyType", "type", "string", "", "", false, false, ""),
"key": NewFieldInfo("Key", "key", "string", "", "", false, false, ""),
"comment": NewFieldInfo("Comment", "comment", "string", "", "", false, false, ""),
"date": NewFieldInfo("Date", "date", "string", "", "", false, false, ""),
"fingerprint": NewFieldInfo("Fingerprint", "fingerprint", "string", "", "", false, false, ""),
}
resource.Types[sshKeyField.FieldName] = sshKeyField
+33 -33
View File
@@ -50,8 +50,8 @@ func TestFieldInfoFromValidation(t *testing.T) {
if fieldInfo.FieldType != c.expectedType {
t.Fatalf("expected type %q got %q", c.expectedType, fieldInfo.FieldType)
}
if fieldInfo.FieldValidation != c.expectedComment {
t.Fatalf("expected comment %q got %q", c.expectedComment, fieldInfo.FieldValidation)
if fieldInfo.FieldValidationComment != c.expectedComment {
t.Fatalf("expected comment %q got %q", c.expectedComment, fieldInfo.FieldValidationComment)
}
if fieldInfo.OmitEmpty != c.expectedOmitEmpty {
t.Fatalf("expected omitempty %t got %t", c.expectedOmitEmpty, fieldInfo.OmitEmpty)
@@ -79,51 +79,51 @@ func TestResourceTypes(t *testing.T) {
}
`
expectedFields := map[string]*FieldInfo{
"Note": NewFieldInfo("Note", "note", "string", ".{0,1024}", true, false, ""),
"Date": NewFieldInfo("Date", "date", "string", "^$|^(20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])Z?$", false, false, ""),
"MAC": NewFieldInfo("MAC", "mac", "string", "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true, false, ""),
"Number": NewFieldInfo("Number", "number", "int", "", true, false, "emptyStringInt"),
"Boolean": NewFieldInfo("Boolean", "boolean", "bool", "", false, false, ""),
"Note": NewFieldInfo("Note", "note", "string", "validate:\"omitempty,gte=0,lte=1024\"", ".{0,1024}", true, false, ""),
"Date": NewFieldInfo("Date", "date", "string", "", "^$|^(20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])Z?$", false, false, ""),
"MAC": NewFieldInfo("MAC", "mac", "string", "validate:\"omitempty,mac\"", "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true, false, ""),
"Number": NewFieldInfo("Number", "number", "int", "", "", true, false, "emptyStringInt"),
"Boolean": NewFieldInfo("Boolean", "boolean", "bool", "", "", false, false, ""),
"NestedType": {
FieldName: "NestedType",
JSONName: "nested_type",
FieldType: "StructNestedType",
FieldValidation: "",
OmitEmpty: true,
IsArray: false,
FieldName: "NestedType",
JSONName: "nested_type",
FieldType: "StructNestedType",
FieldValidationComment: "",
OmitEmpty: true,
IsArray: false,
Fields: map[string]*FieldInfo{
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "^$", false, false, ""),
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "", "^$", false, false, ""),
},
},
"NestedTypeArray": {
FieldName: "NestedTypeArray",
JSONName: "nested_type_array",
FieldType: "StructNestedTypeArray",
FieldValidation: "",
OmitEmpty: true,
IsArray: true,
FieldName: "NestedTypeArray",
JSONName: "nested_type_array",
FieldType: "StructNestedTypeArray",
FieldValidationComment: "",
OmitEmpty: true,
IsArray: true,
Fields: map[string]*FieldInfo{
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "^$", false, false, ""),
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "", "^$", false, false, ""),
},
},
}
expectedStruct := map[string]*FieldInfo{
"Struct": {
FieldName: "Struct",
JSONName: "path",
FieldType: "struct",
FieldValidation: "",
OmitEmpty: false,
IsArray: false,
FieldName: "Struct",
JSONName: "path",
FieldType: "struct",
FieldValidationComment: "",
OmitEmpty: false,
IsArray: false,
Fields: map[string]*FieldInfo{
" ID": NewFieldInfo("ID", "_id", "string", "", true, false, ""),
" SiteID": NewFieldInfo("SiteID", "site_id", "string", "", true, false, ""),
" ID": NewFieldInfo("ID", "_id", "string", "", "", true, false, ""),
" SiteID": NewFieldInfo("SiteID", "site_id", "string", "", "", true, false, ""),
" _Spacer": nil,
" Hidden": NewFieldInfo("Hidden", "attr_hidden", "bool", "", true, false, ""),
" HiddenID": NewFieldInfo("HiddenID", "attr_hidden_id", "string", "", true, false, ""),
" NoDelete": NewFieldInfo("NoDelete", "attr_no_delete", "bool", "", true, false, ""),
" NoEdit": NewFieldInfo("NoEdit", "attr_no_edit", "bool", "", true, false, ""),
" Hidden": NewFieldInfo("Hidden", "attr_hidden", "bool", "", "", true, false, ""),
" HiddenID": NewFieldInfo("HiddenID", "attr_hidden_id", "string", "", "", true, false, ""),
" NoDelete": NewFieldInfo("NoDelete", "attr_no_delete", "bool", "", "", true, false, ""),
" NoEdit": NewFieldInfo("NoEdit", "attr_no_edit", "bool", "", "", true, false, ""),
" _Spacer": nil,
" _Spacer": nil,
},
+182
View File
@@ -0,0 +1,182 @@
package main
import (
"fmt"
"slices"
"strconv"
"strings"
)
type validator string
type validation struct {
v validator
params []string
}
type validationComment string
type regexSpecialChars string
const (
validateTag = "validate"
mac validator = "mac"
ip validator = "ip"
ipv4 validator = "ipv4"
ipv6 validator = "ipv6"
httpUrl validator = "http_url"
oneOf validator = "oneof"
cidr validator = "cidr"
omitempty validator = "omitempty"
length validator = "len"
gte validator = "gte"
lte validator = "lte"
w_regex validator = "w_regex"
numeric_nonzero validator = "numeric_nonzero"
regexChars regexSpecialChars = "^$*+?()[]{}\\|."
)
func createValidations(validations ...validation) string {
if len(validations) == 0 {
return ""
}
validators := make([]string, len(validations)+1)
validators[0] = createValidator(omitempty)
for i, v := range validations {
validators[i+1] = createValidator(v.v, v.params...)
}
joinedValidators := strings.Join(validators, ",")
return fmt.Sprintf("%s:\"%s\"", validateTag, joinedValidators)
}
func createValidator(v validator, params ...string) string {
var filteredParams []string
for _, p := range params {
if p != "" {
filteredParams = append(filteredParams, p)
}
}
if len(filteredParams) == 0 {
return string(v)
}
return fmt.Sprintf("%s=%s", v, strings.Join(filteredParams, " "))
}
func (r regexSpecialChars) In(s string, excludedChars string) bool {
for _, c := range r {
if strings.ContainsRune(s, c) && !strings.ContainsRune(excludedChars, c) {
return true
}
}
return false
}
func (r regexSpecialChars) NotIn(s string, excludedChars string) bool {
return !r.In(s, excludedChars)
}
func (vc validationComment) HasDefinedLength() bool {
s := string(vc)
formatOk := strings.HasPrefix(s, ".{") && strings.HasSuffix(s, "}") && regexChars.NotIn(s, ".{}")
if formatOk {
sub := s[2 : len(s)-1]
bounds := strings.Split(sub, ",")
if len(bounds) < 1 || len(bounds) > 2 {
return false
}
for _, b := range bounds {
if _, err := strconv.Atoi(b); err != nil {
return false
}
}
return true
}
return false
}
func (vc validationComment) IsOneOf() bool {
s := string(vc)
trimmed := strings.TrimPrefix(strings.TrimSuffix(s, ")$"), "^(")
return strings.Contains(trimmed, "|") && regexChars.NotIn(trimmed, "|.")
}
func (vc validationComment) IsWRegex() bool {
s := string(vc)
return slices.Contains([]string{"[\\d\\w]+", "[\\d\\w]*", "[\\w]+", "[\\w]*"}, s)
}
func (vc validationComment) IsMAC() bool {
s := string(vc)
// there are validations present in both notations, so we need to check for both
return (strings.Contains(s, "([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})") || strings.Contains(s, "([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})")) && regexChars.NotIn(s, "(){}[]^$")
}
func (vc validationComment) IsIPv4() bool {
s := string(vc)
return strings.Contains(s, ipv4Regex) && strings.Count(s, "|") == ipv4RegexGroupsCount // last is sanity check if there are no more validation groups than expected
}
func (vc validationComment) IsIPv6() bool {
s := string(vc)
return strings.Contains(s, ipv6Regex) && strings.Count(s, "|") == ipv6RegexGroupsCount // last is sanity check if there are no more validation groups than expected
}
func (vc validationComment) IsIP() bool {
s := string(vc)
return strings.Contains(s, ipv4Regex) && strings.Contains(s, ipv6Regex) && strings.Count(s, "|") == (ipv4RegexGroupsCount+ipv6RegexGroupsCount+1)
}
func (vc validationComment) IsNumericNonZeroBased() bool {
s := string(vc)
return s == numericNonZeroRegex
}
func trimWrappers(s string) string {
trimmed := strings.TrimSuffix(strings.TrimPrefix(s, "(^$|"), "|^$)") // remove wrapping parenthesis
trimmed = strings.TrimSuffix(strings.TrimPrefix(trimmed, "^$|"), "|^$") // remove ^$ which allows for empty string and is not needed
return trimmed
}
const (
ipv4Regex = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"
ipv6Regex = "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
numericNonZeroRegex = "^[1-9][0-9]*$"
)
var (
ipv4RegexGroupsCount = strings.Count(ipv4Regex, "|")
ipv6RegexGroupsCount = strings.Count(ipv6Regex, "|")
)
func defineFieldValidation(rawValidation string) string {
if rawValidation == "" {
return ""
}
rawValidation = trimWrappers(rawValidation)
vc := validationComment(rawValidation)
if vc.IsOneOf() {
trimmed := strings.TrimPrefix(strings.TrimSuffix(rawValidation, ")$"), "^(")
return createValidations(validation{v: oneOf, params: strings.Split(trimmed, "|")})
} else if vc.HasDefinedLength() {
sub := rawValidation[2 : len(rawValidation)-1]
bounds := strings.Split(sub, ",")
if len(bounds) == 1 {
return createValidations(validation{v: length, params: []string{bounds[0]}})
}
return createValidations(validation{v: gte, params: []string{bounds[0]}}, validation{v: lte, params: []string{bounds[1]}})
} else if vc.IsWRegex() {
return createValidations(validation{v: w_regex})
} else if vc.IsMAC() {
return createValidations(validation{v: mac})
} else if vc.IsIPv4() {
return createValidations(validation{v: ipv4})
} else if vc.IsIPv6() {
return createValidations(validation{v: ipv6})
} else if vc.IsIP() {
return createValidations(validation{v: ip})
} else if vc.IsNumericNonZeroBased() {
return createValidations(validation{v: numeric_nonzero})
}
return ""
}
+360
View File
@@ -0,0 +1,360 @@
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateValidator(t *testing.T) {
t.Parallel()
var testValidator validator = "test"
testCases := []struct {
params []string
expectedValidation string
}{
{[]string{"vpn", "802.1x", "custom"}, "test=vpn 802.1x custom"},
{[]string{"vpn"}, "test=vpn"},
{[]string{}, "test"},
{[]string{"0"}, "test=0"},
{[]string{"2-2"}, "test=2-2"},
{[]string{""}, "test"},
}
for _, tc := range testCases {
t.Run(tc.expectedValidation, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
v := validation{testValidator, tc.params}
result := createValidator(v.v, v.params...)
a.Equal(tc.expectedValidation, result)
})
}
}
func TestCreateValidations(t *testing.T) {
t.Parallel()
var testValidator validator = "test"
testCases := []struct {
params [][]string
expectedValidations string
}{
{[][]string{{"1", "2", "3"}}, "test=1 2 3"},
{[][]string{{"1", "2", "3"}, {"4", "5", "6"}}, "test=1 2 3,test=4 5 6"},
{[][]string{{}}, "test"},
{[][]string{{}, {}}, "test,test"},
{[][]string{{}, {"1"}, {}}, "test,test=1,test"},
{[][]string{}, ""},
}
for _, c := range testCases {
var expectedValidationTag string
if len(c.params) == 0 {
expectedValidationTag = ""
} else {
expectedValidationTag = fmt.Sprintf("validate:\"omitempty,%s\"", c.expectedValidations)
}
t.Run(expectedValidationTag, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
var validations []validation
for _, params := range c.params {
validations = append(validations, validation{testValidator, params})
}
result := createValidations(validations...)
a.Equal(expectedValidationTag, result)
})
}
}
func TestDefineValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
validationComment, expected string
}{
{"a|b", "oneof=a b"},
{"1|2", "oneof=1 2"},
{".{1,2}", "gte=1,lte=2"},
{".{1}", "len=1"},
{".{1}", "len=1"},
{"[\\d\\w]+", "w_regex"},
{"[\\d\\w]*", "w_regex"},
{"[\\w]+", "w_regex"},
{"[\\w]*", "w_regex"},
{"a", ""},
{".{1}|.{5,6}", ""},
{"a|.{5,6}", ""},
}
for _, c := range testCases {
var fullExpected string
if c.expected == "" {
fullExpected = ""
} else {
fullExpected = fmt.Sprintf("validate:\"omitempty,%s\"", c.expected)
}
t.Run(c.expected, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
result := defineFieldValidation(c.validationComment)
a.Equal(fullExpected, result)
})
}
}
func testValidationCommentCheck(t *testing.T, testCases []struct {
validationComment validationComment
expected bool
}, fn func(validationComment) bool,
) {
t.Helper()
for _, c := range testCases {
t.Run(fmt.Sprintf("%s-%t", c.validationComment, c.expected), func(t *testing.T) {
t.Parallel()
a := assert.New(t)
trimmed := trimWrappers(string(c.validationComment))
// trimmed := string(c.validationComment) //trimWrappers(string(c.validationComment))
result := fn(validationComment(trimmed))
a.Equal(c.expected, result)
})
}
}
func TestIsOneOfValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
validationComment validationComment
expected bool
}{
{"", false},
{"a", false},
{"a|b", true},
{"^(a|b)$", true},
{"1-2|2-3", true},
{"1_2|2_3", true},
{"1|2", true},
{"%|#", true},
{".|b", true},
{".|.", true},
{"a|.", true},
{"a|.", true},
{"^a|b", false},
{"a|b$", false},
{"(a)|b", false},
{"[a]|b", false},
{"[a]|b", false},
{"a+|b", false},
{"a*|b", false},
{"[a]*|b", false},
{"a?|b", false},
{"\\w|b", false},
{"{a}|b", false},
{".{0,32}", false},
}
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsOneOf() })
}
func TestStringLengthValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
validationComment validationComment
expected bool
}{
{"", false},
{".{1,2}", true},
{".{0,9999}", true},
{".{9999}", true},
{"{9999}", false},
{"{9999", false},
{"9999}", false},
{".{}", false},
{".{1,2,3}", false},
{"a", false},
{"a,b", false},
{"1,2", false},
{"1", false},
{".{1,b}", false},
{".{a,2}", false},
{".{a,b}", false},
{".{1-2}", false},
{".{1_2,2_3}", false},
{".{%,#}", false},
{".{^1,2}", false},
{".{1,2$", false},
{".{(1),2}", false},
{".{[1],2}", false},
{".{[1],2}", false},
{".{1+,2}", false},
{".{1*,2}", false},
{".{[1]*,2}", false},
{".{1?,2}", false},
{".{\\w,2}", false},
{".{{1},2}", false},
{".{.,2}", false},
}
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.HasDefinedLength() })
}
func TestIsWRegexValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
validationComment validationComment
expected bool
}{
{"[\\d\\w]+", true},
{"[\\d\\w]*", true},
{"[\\w]+", true},
{"[\\w]*", true},
{"", false},
{"a", false},
{"[\\d]+", false},
{"[\\d]*", false},
{"[\\s]+", false},
{"[\\s]*", false},
}
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsWRegex() })
}
func TestIsMACValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
validationComment validationComment
expected bool
}{
{"([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", true},
{"([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true},
{"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true},
{"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true},
{"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", true},
{"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", true},
{"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$", true},
{"^$|^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$", true},
{"^$|^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", true},
{"(^$|^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$)", true},
{"[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2}", false},
{"[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", false},
{"([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2}", false},
{"^[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2}$", false},
{"^$|[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", false},
{"^$|[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$", false},
{"(^$|[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$)", false},
{"^$|[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$)", false},
{"(^$|[0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|^$", false},
{"", false},
{"a", false},
{"([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})|(0-9)", false},
{"[0-9]|([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})", false},
{"[0-9]|([0-9A-Fa-f]{2}:){5}", false},
}
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsMAC() })
}
func (vc validationComment) mutate(prefix, suffix string) validationComment {
return validationComment(fmt.Sprintf("%s%s%s", prefix, string(vc), suffix))
}
func generateTestCasesForFixedRegex(regex string) []struct {
validationComment validationComment
expected bool
} {
base := validationComment(regex)
return []struct {
validationComment validationComment
expected bool
}{
{base, true},
{base.mutate("^", "$"), true},
{base.mutate("^", ""), true},
{base.mutate("", "$"), true},
{base.mutate("(^", "$)"), true},
{base.mutate("^", "$)"), true},
{base.mutate("^(", "$"), true},
{base.mutate("^$|", "|^$"), true},
{base.mutate("^$|", ""), true},
{base.mutate("", "|^$"), true},
{base.mutate("(^$|", "|^$)"), true},
{base.mutate("(^$|", ""), true},
{base.mutate("", "|^$)"), true},
// FIXME how to handle these cases? current implementation is dumb and quick, and might fail..
//{base.mutate("^test", ""), false},
//{base.mutate("^test", "test$"), false},
//{base.mutate("", "test$"), false},
//{base.mutate("test", "test"), false},
//{base.mutate("", "test"), false},
//{base.mutate("test", ""), false},
{base.mutate("^test$|", "|^test$"), false},
{base.mutate("^test|", "|^test$"), false},
{base.mutate("test$|", "|^test$"), false},
{base.mutate("test$|", "|^test$"), false},
{base.mutate("^test$|", "|test$"), false},
{base.mutate("^test|", "|^test"), false},
{base.mutate("test|", "|test"), false},
{base.mutate("(test|", "|test)"), false},
{"test", false},
{"", false},
}
}
func TestIsIPv4Validation(t *testing.T) {
t.Parallel()
testCases := generateTestCasesForFixedRegex(ipv4Regex)
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsIPv4() })
}
func TestIsIPv6Validation(t *testing.T) {
t.Parallel()
testCases := generateTestCasesForFixedRegex(ipv6Regex)
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsIPv6() })
}
func TestIsIPValidation(t *testing.T) {
t.Parallel()
testCases := generateTestCasesForFixedRegex(ipv4Regex + "|" + ipv6Regex)
testCases = append(testCases, generateTestCasesForFixedRegex(ipv6Regex+"|"+ipv4Regex)...)
testCases = append(testCases, generateTestCasesForFixedRegex("("+ipv6Regex+")|("+ipv4Regex+")")...)
testCases = append(testCases, generateTestCasesForFixedRegex("("+ipv4Regex+")|("+ipv6Regex+")")...)
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsIP() })
}
func TestIsNumericNonZeroValidation(t *testing.T) {
t.Parallel()
base := validationComment(numericNonZeroRegex)
testCases := []struct {
validationComment validationComment
expected bool
}{
{base, true},
{base.mutate("^$|", "|^$"), true},
{base.mutate("^$|", ""), true},
{base.mutate("", "|^$"), true},
{base.mutate("(^$|", "|^$)"), true},
{base.mutate("(^$|", ""), true},
{base.mutate("", "|^$)"), true},
{base.mutate("(^", "$)"), false},
{base.mutate("^", "$)"), false},
{base.mutate("^(", "$"), false},
{base.mutate("^test", ""), false},
{base.mutate("^test", "test$"), false},
{base.mutate("", "test$"), false},
{base.mutate("test", "test"), false},
{base.mutate("", "test"), false},
{base.mutate("test", ""), false},
{base.mutate("^test$|", "|^test$"), false},
{base.mutate("^test|", "|^test$"), false},
{base.mutate("test$|", "|^test$"), false},
{base.mutate("test$|", "|^test$"), false},
{base.mutate("^test$|", "|test$"), false},
{base.mutate("^test|", "|^test"), false},
{base.mutate("test|", "|test"), false},
{base.mutate("(test|", "|test)"), false},
{"test", false},
{"", false},
}
testValidationCommentCheck(t, testCases, func(v validationComment) bool { return v.IsNumericNonZeroBased() })
}