Introduce Luhn Checksum Validation (#1009)

pull/1081/head
konstantin 1 year ago committed by GitHub
parent 0665b9518a
commit ae3e728763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      README.md
  2. 63
      baked_in.go
  3. 8
      doc.go
  4. 45
      validator_test.go

@ -180,6 +180,7 @@ Baked-in Validations
| jwt | JSON Web Token (JWT) |
| latitude | Latitude |
| longitude | Longitude |
| luhn_checksum | Luhn Algorithm Checksum (for strings and (u)int) |
| postcode_iso3166_alpha2 | Postcode |
| postcode_iso3166_alpha2_field | Postcode |
| rgb | RGB String |

@ -223,6 +223,7 @@ var (
"semver": isSemverFormat,
"dns_rfc1035_label": isDnsRFC1035LabelFormat,
"credit_card": isCreditCard,
"luhn_checksum": hasLuhnChecksum,
"mongodb": isMongoDB,
"cron": isCron,
}
@ -2681,6 +2682,29 @@ func isDnsRFC1035LabelFormat(fl FieldLevel) bool {
return dnsRegexRFC1035Label.MatchString(val)
}
// digitsHaveLuhnChecksum returns true if and only if the last element of the given digits slice is the Luhn checksum of the previous elements
func digitsHaveLuhnChecksum(digits []string) bool {
size := len(digits)
sum := 0
for i, digit := range digits {
value, err := strconv.Atoi(digit)
if err != nil {
return false
}
if size%2 == 0 && i%2 == 0 || size%2 == 1 && i%2 == 1 {
v := value * 2
if v >= 10 {
sum += 1 + (v % 10)
} else {
sum += v
}
} else {
sum += value
}
}
return (sum % 10) == 0
}
// isMongoDB is the validation function for validating if the current field's value is valid mongoDB objectID
func isMongoDB(fl FieldLevel) bool {
val := fl.Field().String()
@ -2705,24 +2729,29 @@ func isCreditCard(fl FieldLevel) bool {
return false
}
sum := 0
for i, digit := range ccDigits {
value, err := strconv.Atoi(digit)
if err != nil {
return false
}
if size%2 == 0 && i%2 == 0 || size%2 == 1 && i%2 == 1 {
v := value * 2
if v >= 10 {
sum += 1 + (v % 10)
} else {
sum += v
}
} else {
sum += value
}
return digitsHaveLuhnChecksum(ccDigits)
}
// hasLuhnChecksum is the validation for validating if the current field's value has a valid Luhn checksum
func hasLuhnChecksum(fl FieldLevel) bool {
field := fl.Field()
var str string // convert to a string which will then be split into single digits; easier and more readable than shifting/extracting single digits from a number
switch field.Kind() {
case reflect.String:
str = field.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
str = strconv.FormatInt(field.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
str = strconv.FormatUint(field.Uint(), 10)
default:
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}
return (sum % 10) == 0
size := len(str)
if size < 2 { // there has to be at least one digit that carries a meaning + the checksum
return false
}
digits := strings.Split(str, "")
return digitsHaveLuhnChecksum(digits)
}
// isCron is the validation function for validating if the current field's value is a valid cron expression

@ -1357,6 +1357,13 @@ This validates that a string value contains a valid credit card number using Luh
Usage: credit_card
# Luhn Checksum
Usage: luhn_checksum
This validates that a string or (u)int value contains a valid checksum using the Luhn algorithm.
#MongoDb ObjectID
This validates that a string is a valid 24 character hexadecimal string.
@ -1372,6 +1379,7 @@ This validates that a string value contains a valid cron expression.
Alias Validators and Tags
Alias Validators and Tags
NOTE: When returning an error, the tag returned in "FieldError" will be
the alias tag unless the dive tag is part of the alias. Everything after the
dive tag is not reported as the alias tag. Also, the "ActualTag" in the before

@ -12692,6 +12692,51 @@ func TestCreditCardFormatValidation(t *testing.T) {
}
}
func TestLuhnChecksumValidation(t *testing.T) {
testsUint := []struct {
value interface{} `validate:"luhn_checksum"` // the type is interface{} because the luhn_checksum works on both strings and numbers
tag string
expected bool
}{
{uint64(586824160825533338), "luhn_checksum", true}, // credit card numbers are just special cases of numbers with luhn checksum
{586824160825533338, "luhn_checksum", true},
{"586824160825533338", "luhn_checksum", true},
{uint64(586824160825533328), "luhn_checksum", false},
{586824160825533328, "luhn_checksum", false},
{"586824160825533328", "luhn_checksum", false},
{10000000116, "luhn_checksum", true}, // but there may be shorter numbers (11 digits)
{"10000000116", "luhn_checksum", true},
{10000000117, "luhn_checksum", false},
{"10000000117", "luhn_checksum", false},
{uint64(12345678123456789011), "luhn_checksum", true}, // or longer numbers (19 digits)
{"12345678123456789011", "luhn_checksum", true},
{1, "luhn_checksum", false}, // single digits (checksum only) are not allowed
{"1", "luhn_checksum", false},
{-10, "luhn_checksum", false}, // negative ints are not allowed
{"abcdefghijklmnop", "luhn_checksum", false},
}
validate := New()
for i, test := range testsUint {
errs := validate.Var(test.value, test.tag)
if test.expected {
if !IsEqual(errs, nil) {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
}
} else {
if IsEqual(errs, nil) {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
} else {
val := getError(errs, "", "")
if val.Tag() != "luhn_checksum" {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
}
}
}
}
}
func TestMultiOrOperatorGroup(t *testing.T) {
tests := []struct {
Value int `validate:"eq=1|gte=5,eq=1|lt=7"`

Loading…
Cancel
Save