Skip to content

Commit

Permalink
Introduce Luhn Checksum Validation (#1009)
Browse files Browse the repository at this point in the history
  • Loading branch information
hf-kklein authored Mar 19, 2023
1 parent 0665b95 commit ae3e728
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 17 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
63 changes: 46 additions & 17 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ var (
"semver": isSemverFormat,
"dns_rfc1035_label": isDnsRFC1035LabelFormat,
"credit_card": isCreditCard,
"luhn_checksum": hasLuhnChecksum,
"mongodb": isMongoDB,
"cron": isCron,
}
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit ae3e728

Please sign in to comment.