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 @@ -169,6 +169,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| bic_iso_9362_2014 | Business Identifier Code (ISO 9362:2014) |
| bic | Business Identifier Code (ISO 9362:2022) |
| 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
186 changes: 186 additions & 0 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
Expand Down Expand Up @@ -240,6 +241,7 @@ var (
"iso4217": isIso4217,
"iso4217_numeric": isIso4217Numeric,
"bcp47_language_tag": isBCP47LanguageTag,
"bcp47_strict_language_tag": isBCP47StrictLanguageTag,
"postcode_iso3166_alpha2": isPostcodeByIso3166Alpha2,
"postcode_iso3166_alpha2_field": isPostcodeByIso3166Alpha2Field,
"bic_iso_9362_2014": isIsoBic2014Format,
Expand All @@ -261,6 +263,33 @@ var (
var (
oneofValsCache = map[string][]string{}
oneofValsCacheRWLock = sync.RWMutex{}

// BCP47 language tag
// according to https://www.rfc-editor.org/rfc/bcp/bcp47.txt
bcp47LanguageTagRe = 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})+)?`,
`$`,
}, ""))
)

func parseOneOfParam2(s string) []string {
Expand Down Expand Up @@ -3081,6 +3110,163 @@ func isBCP47LanguageTag(fl FieldLevel) bool {
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 {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}

lower := strings.ToLower(field.String())
lowerTagDash := lower + "-"

m := bcp47LanguageTagRe.FindStringSubmatch(lower)
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 base.String() != baseLang {
return false
}

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

if len(prefixes) > 0 {
found := false
for _, p := range prefixes {
if strings.HasPrefix(lowerTagDash, 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 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, "-") {
_, err := language.ParseVariant(v)
if err != nil {
return false
}

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

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

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

return true
}

// isIsoBic2014Format is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362 2014
func isIsoBic2014Format(fl FieldLevel) bool {
bicString := fl.Field().String()
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 @@ -1442,6 +1442,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 - 2022 standard)

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