diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 8e82f43..d6bfd55 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -41,8 +41,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: 1.16 + - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: version: v1.45.2 diff --git a/README.md b/README.md index f25649a..1c89266 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ Baked-in Validations | bcp47_language_tag | Language tag (BCP 47) | | btc_addr | Bitcoin Address | | btc_addr_bech32 | Bitcoin Bech32 Address (segwit) | +| credit_card | Credit Card Number | | datetime | Datetime | | e164 | e164 formatted phone number | | email | E-mail String diff --git a/baked_in.go b/baked_in.go index 36a410b..7833f24 100644 --- a/baked_in.go +++ b/baked_in.go @@ -203,6 +203,7 @@ var ( "bic": isIsoBicFormat, "semver": isSemverFormat, "dns_rfc1035_label": isDnsRFC1035LabelFormat, + "credit_card": isCreditCard, } ) @@ -2469,3 +2470,41 @@ func isDnsRFC1035LabelFormat(fl FieldLevel) bool { val := fl.Field().String() return dnsRegexRFC1035Label.MatchString(val) } + +// isCreditCard is the validation function for validating if the current field's value is a valid credit card number +func isCreditCard(fl FieldLevel) bool { + val := fl.Field().String() + var creditCard bytes.Buffer + segments := strings.Split(val, " ") + for _, segment := range segments { + if len(segment) < 3 { + return false + } + creditCard.WriteString(segment) + } + + ccDigits := strings.Split(creditCard.String(), "") + size := len(ccDigits) + if size < 12 || size > 19 { + 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 (sum % 10) == 0 +} diff --git a/doc.go b/doc.go index 6cf6208..7341c67 100644 --- a/doc.go +++ b/doc.go @@ -1317,6 +1317,12 @@ More information on https://semver.org/ Usage: semver +Credit Card + +This validates that a string value contains a valid credit card number using Luhn algoritm. + + Usage: credit_card + Alias Validators and Tags NOTE: When returning an error, the tag returned in "FieldError" will be diff --git a/validator_test.go b/validator_test.go index 4da0de6..a5b1fa2 100644 --- a/validator_test.go +++ b/validator_test.go @@ -11750,3 +11750,41 @@ func TestPostCodeByIso3166Alpha2Field_InvalidKind(t *testing.T) { _ = New().Struct(test{"ABC", 123, false}) t.Errorf("Didn't panic as expected") } + +func TestCreditCardFormatValidation(t *testing.T) { + tests := []struct { + value string `validate:"credit_card"` + tag string + expected bool + }{ + {"586824160825533338", "credit_card", true}, + {"586824160825533328", "credit_card", false}, + {"4624748233249780", "credit_card", true}, + {"4624748233349780", "credit_card", false}, + {"378282246310005", "credit_card", true}, + {"378282146310005", "credit_card", false}, + {"4624 7482 3324 9780", "credit_card", true}, + {"4624 7482 3324 9780", "credit_card", 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 credit_card failed Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d credit_card failed Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "credit_card" { + t.Fatalf("Index: %d credit_card failed Error: %s", i, errs) + } + } + } + } +}