diff --git a/README.md b/README.md index c5bb81d..db38321 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ Baked-in Validations | uuid5 | Universally Unique Identifier UUID v5 | | uuid5_rfc4122 | Universally Unique Identifier UUID v5 RFC4122 | | uuid_rfc4122 | Universally Unique Identifier UUID RFC4122 | +| semver | Semantic Versioning 2.0.0 | | ulid | Universally Unique Lexicographically Sortable Identifier ULID | ### Comparisons: diff --git a/baked_in.go b/baked_in.go index 4b82abb..7868b66 100644 --- a/baked_in.go +++ b/baked_in.go @@ -199,6 +199,7 @@ var ( "postcode_iso3166_alpha2": isPostcodeByIso3166Alpha2, "postcode_iso3166_alpha2_field": isPostcodeByIso3166Alpha2Field, "bic": isIsoBicFormat, + "semver": isSemverFormat, "dns_rfc1035_label": isDnsRFC1035LabelFormat, } ) @@ -2421,6 +2422,13 @@ func isIsoBicFormat(fl FieldLevel) bool { return bicRegex.MatchString(bicString) } +// isSemverFormat is the validation function for validating if the current field's value is a valid semver version, defined in Semantic Versioning 2.0.0 +func isSemverFormat(fl FieldLevel) bool { + semverString := fl.Field().String() + + return semverRegex.MatchString(semverString) +} + // isDnsRFC1035LabelFormat is the validation function // for validating if the current field's value is // a valid dns RFC 1035 label, defined in RFC 1035. diff --git a/doc.go b/doc.go index 1df38e7..b284c37 100644 --- a/doc.go +++ b/doc.go @@ -1276,6 +1276,12 @@ More information on https://golang.org/pkg/time/#LoadLocation Usage: timezone +Semantic Version + +This validates that a string value is a valid semver version, defined in Semantic Versioning 2.0.0. +More information on https://semver.org/ + + Usage: semver Alias Validators and Tags diff --git a/regexes.go b/regexes.go index 58d8766..48e51d5 100644 --- a/regexes.go +++ b/regexes.go @@ -52,6 +52,7 @@ const ( jWTRegexString = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$" splitParamsRegexString = `'[^']*'|\S+` bicRegexString = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$` + semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ dnsRegexStringRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9]){0,62}$" ) @@ -105,5 +106,6 @@ var ( jWTRegex = regexp.MustCompile(jWTRegexString) splitParamsRegex = regexp.MustCompile(splitParamsRegexString) bicRegex = regexp.MustCompile(bicRegexString) + semverRegex = regexp.MustCompile(semverRegexString) dnsRegexRFC1035Label = regexp.MustCompile(dnsRegexStringRFC1035Label) ) diff --git a/validator_test.go b/validator_test.go index c86f8da..3730fb9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -11383,6 +11383,84 @@ func TestBicIsoFormatValidation(t *testing.T) { } } +func TestSemverFormatValidation(t *testing.T) { + tests := []struct { + value string `validate:"semver"` + tag string + expected bool + }{ + {"1.2.3", "semver", true}, + {"10.20.30", "semver", true}, + {"1.1.2-prerelease+meta", "semver", true}, + {"1.1.2+meta", "semver", true}, + {"1.1.2+meta-valid", "semver", true}, + {"1.0.0-alpha", "semver", true}, + {"1.0.0-alpha.1", "semver", true}, + {"1.0.0-alpha.beta", "semver", true}, + {"1.0.0-alpha.beta.1", "semver", true}, + {"1.0.0-alpha0.valid", "semver", true}, + {"1.0.0-alpha.0valid", "semver", true}, + {"1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", "semver", true}, + {"1.0.0-rc.1+build.1", "semver", true}, + {"1.0.0-rc.1+build.123", "semver", true}, + {"1.2.3-beta", "semver", true}, + {"1.2.3-DEV-SNAPSHOT", "semver", true}, + {"1.2.3-SNAPSHOT-123", "semver", true}, + {"2.0.0+build.1848", "semver", true}, + {"2.0.1-alpha.1227", "semver", true}, + {"1.0.0-alpha+beta", "semver", true}, + {"1.2.3----RC-SNAPSHOT.12.9.1--.12+788", "semver", true}, + {"1.2.3----R-S.12.9.1--.12+meta", "semver", true}, + {"1.2.3----RC-SNAPSHOT.12.9.1--.12", "semver", true}, + {"1.0.0+0.build.1-rc.10000aaa-kk-0.1", "semver", true}, + {"99999999999999999999999.999999999999999999.99999999999999999", "semver", true}, + {"1.0.0-0A.is.legal", "semver", true}, + {"1", "semver", false}, + {"1.2", "semver", false}, + {"1.2.3-0123", "semver", false}, + {"1.2.3-0123.0123", "semver", false}, + {"1.1.2+.123", "semver", false}, + {"+invalid", "semver", false}, + {"-invalid", "semver", false}, + {"-invalid+invalid", "semver", false}, + {"alpha", "semver", false}, + {"alpha.beta.1", "semver", false}, + {"alpha.1", "semver", false}, + {"1.0.0-alpha_beta", "semver", false}, + {"1.0.0-alpha_beta", "semver", false}, + {"1.0.0-alpha...1", "semver", false}, + {"01.1.1", "semver", false}, + {"1.01.1", "semver", false}, + {"1.1.01", "semver", false}, + {"1.2", "semver", false}, + {"1.2.Dev", "semver", false}, + {"1.2.3.Dev", "semver", false}, + {"1.2-SNAPSHOT", "semver", false}, + } + + validate := New() + + for i, test := range tests { + + errs := validate.Var(test.value, test.tag) + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d semver failed Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d semver failed Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "semver" { + t.Fatalf("Index: %d semver failed Error: %s", i, errs) + } + } + } + } +} + func TestRFC1035LabelFormatValidation(t *testing.T) { tests := []struct { value string `validate:"dns_rfc1035_label"`