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:
+1
-1
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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() })
|
||||
}
|
||||
Reference in New Issue
Block a user