Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| base64rawurl | Base64RawURL String |
| bic | Business Identifier Code (ISO 9362) |
| bcp47_language_tag | Language tag (BCP 47) |
| bcp47_strict_language_tag | Language tag (BCP 47), strictly following RFC 5646 |
| btc_addr | Bitcoin Address |
| btc_addr_bech32 | Bitcoin Bech32 Address (segwit) |
| credit_card | Credit Card Number |
Expand Down
184 changes: 184 additions & 0 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -235,6 +236,7 @@
"iso4217": isIso4217,
"iso4217_numeric": isIso4217Numeric,
"bcp47_language_tag": isBCP47LanguageTag,
"bcp47_strict_language_tag": isBCP47StrictLanguageTag,
"postcode_iso3166_alpha2": isPostcodeByIso3166Alpha2,
"postcode_iso3166_alpha2_field": isPostcodeByIso3166Alpha2Field,
"bic": isIsoBicFormat,
Expand Down Expand Up @@ -301,7 +303,7 @@
default:
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
for i := 0; i < len(vals); i++ {

Check failure on line 306 in baked_in.go

View workflow job for this annotation

GitHub Actions / lint

rangeint: for loop can be modernized using range over int (modernize)
if vals[i] == v {
return true
}
Expand Down Expand Up @@ -654,7 +656,7 @@

factor := []int32{1, 3}

for i = 0; i < 12; i++ {

Check failure on line 659 in baked_in.go

View workflow job for this annotation

GitHub Actions / lint

rangeint: for loop can be modernized using range over int (modernize)
checksum += factor[i%2] * int32(s[i]-'0')
}

Expand All @@ -672,7 +674,7 @@
var checksum int32
var i int32

for i = 0; i < 9; i++ {

Check failure on line 677 in baked_in.go

View workflow job for this annotation

GitHub Actions / lint

rangeint: for loop can be modernized using range over int (modernize)
checksum += (i + 1) * int32(s[i]-'0')
}

Expand Down Expand Up @@ -2943,6 +2945,188 @@
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}

// isBCP47StrictLanguageTag is the validation function for validating if the current field's value is a valid BCP 47 language tag
// according to https://www.rfc-editor.org/rfc/bcp/bcp47.txt
func isBCP47StrictLanguageTag(fl FieldLevel) bool {
field := fl.Field()

if field.Kind() == reflect.String {
var languageTagRe = regexp.MustCompile(strings.Join([]string{
// group 1:
`^(`,
// irregular
`EN-GB-OED|I-AMI|I-BNN|I-DEFAULT|I-ENOCHIAN|I-HAK|I-KLINGON|I-LUX|I-MINGO|I-NAVAJO|I-PWN|I-TAO|I-TAY|I-TSU|`,
`SGN-BE-FR|SGN-BE-NL|SGN-CH-DE|`,
// regular
`ART-LOJBAN|CEL-GAULISH|NO-BOK|NO-NYN|ZH-GUOYU|ZH-HAKKA|ZH-MIN|ZH-MIN-NAN|ZH-XIANG|`,
// privateuse
`X-[A-Z0-9]{1,8}`,
`)$`,

`|`,

// langtag
`^`,
`((?:[A-Z]{2,3}(?:-[A-Z]{3}){0,3})|[A-Z]{4}|[A-Z]{5,8})`, // group 2: language
`(?:-([A-Z]{4}))?`, // group 3: script
`(?:-([A-Z]{2}|[0-9]{3}))?`, // group 4: region
`(?:-((?:[A-Z0-9]{5,8}|[0-9][A-Z0-9]{3})(?:-(?:[A-Z0-9]{5,8}|[0-9][A-Z0-9]{3}))*))?`, // group 5: variant
`(?:-((?:[A-WYZ0-9](?:-[A-Z0-9]{2,8})+)(?:-(?:[A-WYZ0-9](?:-[A-Z0-9]{2,8})+))*))?`, // group 6: extension
`(?:-X(?:-[A-Z0-9]{1,8})+)?`,
`$`,
}, ""))

languageTag := strings.ToUpper(field.String())

m := languageTagRe.FindStringSubmatch(languageTag)
if m == nil {
return false
}

grandfatheredOrPrivateuse := m[1]
lang := m[2]
script := m[3]
region := m[4]
variant := m[5]
extension := m[6]

if grandfatheredOrPrivateuse != "" {
return true
}

// language = 2*3ALPHA ; shortest ISO 639 code
// ["-" extlang] ; sometimes followed by
// ; extended language subtags
// / 4ALPHA ; or reserved for future use
// / 5*8ALPHA ; or registered language subtag
switch n := len(lang); {
// 2*3ALPHA "-" extlang
case strings.Contains(lang, "-"):
parts := strings.Split(lang, "-")

baseLang := parts[0]
base, err := language.ParseBase(baseLang)
if err != nil {
return false
}
// base.String() normalizes the base to the shortest code
// for the language
if strings.ToUpper(base.String()) != baseLang {
return false
}

for _, e := range parts[1:] {
prefixes, ok := iana_subtag_registry_extlangs[strings.ToLower(e)]
if !ok {
return false
}

if len(prefixes) > 0 {
found := false
for _, p := range prefixes {
if strings.HasPrefix(strings.ToLower(languageTag)+"-", strings.ToLower(p)) {
found = true
break
}
}
if !found {
return false
}
}
}
// 2*3ALPHA ; shortest ISO 639 code
case n <= 3:
base, err := language.ParseBase(lang)
if err != nil {
return false
}

// base.String() normalizes the base to the shortest code
// for the language
if strings.ToUpper(base.String()) != lang {
return false
}
// 4ALPHA ; or reserved for future use
case n == 4:
return false
// 5*8ALPHA ; or registered language subtag
default:
// registered language subtag with 5+ characters.
// As of today there aren't any.
// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
return false
}

// script = 4ALPHA ; ISO 15924 code
if script != "" {
_, err := language.ParseScript(script)
if err != nil {
return false
}
}

// region = 2ALPHA ; ISO 3166-1 code
// 3DIGIT ; UN M.49 code
if region != "" {
if len(region) == 2 {
_, err := language.ParseRegion(region)
if err != nil {
return false
}
} else {
// Can't use language.ParseRegion() here because not all
// UN M.49 region codes are allowed, just the subset present
// in the IANA subtag registry.
_, ok := iana_subtag_registry_m49_codes[region]
if !ok {
return false
}
}
}

// variant = 5*8alphanum ; registered variants
// / (DIGIT 3alphanum)
if variant != "" {
for v := range strings.SplitSeq(variant, "-") {
lowerVariant := strings.ToLower(v)
_, err := language.ParseVariant(lowerVariant)
if err != nil {
return false
}

prefixes, ok := iana_subtag_registry_variants[lowerVariant]
if !ok {
return false
}

if len(prefixes) > 0 {
found := false
for _, p := range prefixes {
if strings.HasPrefix(strings.ToLower(languageTag)+"-", strings.ToLower(p)) {
found = true
break
}
}
if !found {
return false
}
}
}
}

if extension != "" {
_, err := language.ParseExtension(extension)
if err != nil {
return false
}
}

return true
}

panic(fmt.Sprintf("Bad field type %s", field.Type()))
}

// isIsoBicFormat is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362
func isIsoBicFormat(fl FieldLevel) bool {
bicString := fl.Field().String()
Expand Down Expand Up @@ -3034,7 +3218,7 @@
func isCreditCard(fl FieldLevel) bool {
val := fl.Field().String()
var creditCard bytes.Buffer
segments := strings.Split(val, " ")

Check failure on line 3221 in baked_in.go

View workflow job for this annotation

GitHub Actions / lint

stringsseq: Ranging over SplitSeq is more efficient (modernize)
for _, segment := range segments {
if len(segment) < 3 {
return false
Expand Down Expand Up @@ -3133,7 +3317,7 @@
case reflect.Bool:
return firstReturnValue.Bool(), nil
case reflect.Interface:
errorType := reflect.TypeOf((*error)(nil)).Elem()

Check failure on line 3320 in baked_in.go

View workflow job for this annotation

GitHub Actions / lint

reflecttypefor: reflect.TypeOf call can be simplified using TypeFor (modernize)

if firstReturnValue.Type().Implements(errorType) {
return firstReturnValue.IsNil(), nil
Expand Down
12 changes: 12 additions & 0 deletions country_codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,3 +1175,15 @@ var iso3166_2 = map[string]struct{}{
"ZW-BU": {}, "ZW-HA": {}, "ZW-MA": {}, "ZW-MC": {}, "ZW-ME": {},
"ZW-MI": {}, "ZW-MN": {}, "ZW-MS": {}, "ZW-MV": {}, "ZW-MW": {},
}

// Subset of UN M.49 region codes present in the IANA Language Subtag Registry:
// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
var iana_subtag_registry_m49_codes = map[string]struct{}{
"001": {}, "002": {}, "003": {}, "005": {}, "009": {},
"011": {}, "013": {}, "014": {}, "015": {}, "017": {},
"018": {}, "019": {}, "021": {}, "029": {}, "030": {},
"034": {}, "035": {}, "039": {}, "053": {}, "054": {},
"057": {}, "061": {}, "142": {}, "143": {}, "145": {},
"150": {}, "151": {}, "154": {}, "155": {}, "202": {},
"419": {},
}
8 changes: 8 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,14 @@ More information on https://pkg.go.dev/golang.org/x/text/language

Usage: bcp47_language_tag

# BCP 47 Strict Language Tag

This validates that a string value is a valid BCP 47 language tag strictly following RFC 5646 rules,
unlike language.Parse which also accepts Unicode extensions.
see https://www.rfc-editor.org/rfc/bcp/bcp47.txt

Usage: bcp47_strict_language_tag

BIC (SWIFT code)

This validates that a string value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362.
Expand Down
Loading
Loading