Skip to content
Closed
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- BIC / SWIFT validation: `it.IsBIC()` with `CaseInsensitive` and `WithIBAN` (and `WithIBANError` / `WithIBANMessage`), `validate.BIC` with `validate.BICCaseInsensitive` and `validate.BICWithIBAN`, `is.BIC`; `validation.ErrInvalidBIC`, `validation.ErrBICIBANCountryMismatch`, `message.InvalidBIC`, `message.BICNotAssociatedWithIBAN` and English and Russian translations (behavior aligned with Symfony `Bic` / `BicValidator`; country/territory check via `golang.org/x/text/language` regions plus Symfony’s BIC-to-IBAN territory map).
- IBAN validation: `it.IsIBAN()`, `validate.IBAN`, `is.IBAN`, with `validation.ErrInvalidIBAN` / `message.InvalidIBAN` and English and Russian translations (behavior aligned with Symfony `Iban`; country patterns from Symfony 7.2 `IbanValidator`).
- **NoSuspiciousCharacters** (spoofing / homoglyph checks): `it.HasNoSuspiciousCharacters()` with `CheckInvisible` / `CheckMixedNumbers` / `CheckHiddenOverlay`, `WithoutInvisible` / `WithoutMixedNumbers` / `WithoutHiddenOverlay` (bitmask-based), locale and single-script restrictions; `validate.NoSuspiciousCharacters`; errors `validation.ErrSuspiciousInvisible`, `ErrSuspiciousMixedNumbers`, `ErrSuspiciousHiddenOverlay`, `ErrSuspiciousCharactersRestriction` and English/Russian messages (behavior inspired by Symfony `NoSuspiciousCharacters`, implemented without CGO; may differ from ICU in edge cases).
- CIDR notation validation: `it.IsCIDR()` with `IPv4Only`, `IPv6Only`, `WithVersion`, `WithNetmaskRange`, and separate invalid vs netmask-range messages; `validate.CIDR`, `validate.CIDRViolationNetmaskBounds`, `is.CIDR`; `validation.ErrInvalidCIDR` / `validation.ErrCIDRNetmaskOutOfRange` and English and Russian translations (behavior aligned with Symfony `Cidr`). Exported names use the **CIDR** initialism per [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments).
Expand Down
112 changes: 57 additions & 55 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,63 @@ import (
)

var (
ErrInvalidDate = NewError("invalid date", message.InvalidDate)
ErrInvalidDateTime = NewError("invalid datetime", message.InvalidDateTime)
ErrInvalidEAN13 = NewError("invalid EAN-13", message.InvalidEAN13)
ErrInvalidEAN8 = NewError("invalid EAN-8", message.InvalidEAN8)
ErrInvalidEmail = NewError("invalid email", message.InvalidEmail)
ErrInvalidHostname = NewError("invalid hostname", message.InvalidHostname)
ErrInvalidIBAN = NewError("invalid IBAN", message.InvalidIBAN)
ErrInvalidISIN = NewError("invalid ISIN", message.InvalidISIN)
ErrInvalidCIDR = NewError("invalid CIDR", message.InvalidCIDR)
ErrCIDRNetmaskOutOfRange = NewError("CIDR netmask out of range", message.CIDRNetmaskOutOfRange)
ErrInvalidIP = NewError("invalid IP address", message.InvalidIP)
ErrInvalidJSON = NewError("invalid JSON", message.InvalidJSON)
ErrInvalidLUHN = NewError("invalid LUHN", message.InvalidLUHN)
ErrInvalidTime = NewError("invalid time", message.InvalidTime)
ErrInvalidULID = NewError("invalid ULID", message.InvalidULID)
ErrInvalidUPCA = NewError("invalid UPC-A", message.InvalidUPCA)
ErrInvalidUPCE = NewError("invalid UPC-E", message.InvalidUPCE)
ErrInvalidURL = NewError("invalid URL", message.InvalidURL)
ErrInvalidUUID = NewError("invalid UUID", message.InvalidUUID)
ErrIsBlank = NewError("is blank", message.IsBlank)
ErrIsEqual = NewError("is equal", message.IsEqual)
ErrIsNil = NewError("is nil", message.IsNil)
ErrNoSuchChoice = NewError("no such choice", message.NoSuchChoice)
ErrNotBlank = NewError("is not blank", message.NotBlank)
ErrNotDivisible = NewError("is not divisible", message.NotDivisible)
ErrNotDivisibleCount = NewError("not divisible count", message.NotDivisibleCount)
ErrNotEqual = NewError("is not equal", message.NotEqual)
ErrNotExactCount = NewError("not exact count", message.NotExactCount)
ErrNotExactLength = NewError("not exact length", message.NotExactLength)
ErrNotFalse = NewError("is not false", message.NotFalse)
ErrNotInRange = NewError("is not in range", message.NotInRange)
ErrNotInteger = NewError("is not an integer", message.NotInteger)
ErrNotNegative = NewError("is not negative", message.NotNegative)
ErrNotNegativeOrZero = NewError("is not negative or zero", message.NotNegativeOrZero)
ErrNotNil = NewError("is not nil", message.NotNil)
ErrNotNumeric = NewError("is not numeric", message.NotNumeric)
ErrNotPositive = NewError("is not positive", message.NotPositive)
ErrNotPositiveOrZero = NewError("is not positive or zero", message.NotPositiveOrZero)
ErrNotTrue = NewError("is not true", message.NotTrue)
ErrNotUnique = NewError("is not unique", message.NotUnique)
ErrNotValid = NewError("is not valid", message.NotValid)
ErrProhibitedIP = NewError("is prohibited IP", message.ProhibitedIP)
ErrProhibitedURL = NewError("is prohibited URL", message.ProhibitedURL)
ErrTooEarly = NewError("is too early", message.TooEarly)
ErrTooEarlyOrEqual = NewError("is too early or equal", message.TooEarlyOrEqual)
ErrTooFewElements = NewError("too few elements", message.TooFewElements)
ErrTooHigh = NewError("is too high", message.TooHigh)
ErrTooHighOrEqual = NewError("is too high or equal", message.TooHighOrEqual)
ErrTooLate = NewError("is too late", message.TooLate)
ErrTooLateOrEqual = NewError("is too late or equal", message.TooLateOrEqual)
ErrTooLong = NewError("is too long", message.TooLong)
ErrTooLow = NewError("is too low", message.TooLow)
ErrTooLowOrEqual = NewError("is too low or equal", message.TooLowOrEqual)
ErrTooManyElements = NewError("too many elements", message.TooManyElements)
ErrTooShort = NewError("is too short", message.TooShort)
ErrInvalidDate = NewError("invalid date", message.InvalidDate)
ErrInvalidDateTime = NewError("invalid datetime", message.InvalidDateTime)
ErrInvalidEAN13 = NewError("invalid EAN-13", message.InvalidEAN13)
ErrInvalidEAN8 = NewError("invalid EAN-8", message.InvalidEAN8)
ErrInvalidEmail = NewError("invalid email", message.InvalidEmail)
ErrInvalidHostname = NewError("invalid hostname", message.InvalidHostname)
ErrInvalidIBAN = NewError("invalid IBAN", message.InvalidIBAN)
ErrInvalidBIC = NewError("invalid BIC", message.InvalidBIC)
ErrBICIBANCountryMismatch = NewError("BIC IBAN country mismatch", message.BICNotAssociatedWithIBAN)
ErrInvalidISIN = NewError("invalid ISIN", message.InvalidISIN)
ErrInvalidCIDR = NewError("invalid CIDR", message.InvalidCIDR)
ErrCIDRNetmaskOutOfRange = NewError("CIDR netmask out of range", message.CIDRNetmaskOutOfRange)
ErrInvalidIP = NewError("invalid IP address", message.InvalidIP)
ErrInvalidJSON = NewError("invalid JSON", message.InvalidJSON)
ErrInvalidLUHN = NewError("invalid LUHN", message.InvalidLUHN)
ErrInvalidTime = NewError("invalid time", message.InvalidTime)
ErrInvalidULID = NewError("invalid ULID", message.InvalidULID)
ErrInvalidUPCA = NewError("invalid UPC-A", message.InvalidUPCA)
ErrInvalidUPCE = NewError("invalid UPC-E", message.InvalidUPCE)
ErrInvalidURL = NewError("invalid URL", message.InvalidURL)
ErrInvalidUUID = NewError("invalid UUID", message.InvalidUUID)
ErrIsBlank = NewError("is blank", message.IsBlank)
ErrIsEqual = NewError("is equal", message.IsEqual)
ErrIsNil = NewError("is nil", message.IsNil)
ErrNoSuchChoice = NewError("no such choice", message.NoSuchChoice)
ErrNotBlank = NewError("is not blank", message.NotBlank)
ErrNotDivisible = NewError("is not divisible", message.NotDivisible)
ErrNotDivisibleCount = NewError("not divisible count", message.NotDivisibleCount)
ErrNotEqual = NewError("is not equal", message.NotEqual)
ErrNotExactCount = NewError("not exact count", message.NotExactCount)
ErrNotExactLength = NewError("not exact length", message.NotExactLength)
ErrNotFalse = NewError("is not false", message.NotFalse)
ErrNotInRange = NewError("is not in range", message.NotInRange)
ErrNotInteger = NewError("is not an integer", message.NotInteger)
ErrNotNegative = NewError("is not negative", message.NotNegative)
ErrNotNegativeOrZero = NewError("is not negative or zero", message.NotNegativeOrZero)
ErrNotNil = NewError("is not nil", message.NotNil)
ErrNotNumeric = NewError("is not numeric", message.NotNumeric)
ErrNotPositive = NewError("is not positive", message.NotPositive)
ErrNotPositiveOrZero = NewError("is not positive or zero", message.NotPositiveOrZero)
ErrNotTrue = NewError("is not true", message.NotTrue)
ErrNotUnique = NewError("is not unique", message.NotUnique)
ErrNotValid = NewError("is not valid", message.NotValid)
ErrProhibitedIP = NewError("is prohibited IP", message.ProhibitedIP)
ErrProhibitedURL = NewError("is prohibited URL", message.ProhibitedURL)
ErrTooEarly = NewError("is too early", message.TooEarly)
ErrTooEarlyOrEqual = NewError("is too early or equal", message.TooEarlyOrEqual)
ErrTooFewElements = NewError("too few elements", message.TooFewElements)
ErrTooHigh = NewError("is too high", message.TooHigh)
ErrTooHighOrEqual = NewError("is too high or equal", message.TooHighOrEqual)
ErrTooLate = NewError("is too late", message.TooLate)
ErrTooLateOrEqual = NewError("is too late or equal", message.TooLateOrEqual)
ErrTooLong = NewError("is too long", message.TooLong)
ErrTooLow = NewError("is too low", message.TooLow)
ErrTooLowOrEqual = NewError("is too low or equal", message.TooLowOrEqual)
ErrTooManyElements = NewError("too many elements", message.TooManyElements)
ErrTooShort = NewError("is too short", message.TooShort)

ErrSuspiciousInvisible = NewError("suspicious invisible characters", message.SuspiciousInvisible)
ErrSuspiciousMixedNumbers = NewError("suspicious mixed numbers", message.SuspiciousMixedNumbers)
Expand Down
10 changes: 10 additions & 0 deletions is/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ func ExampleIBAN() {
// false
}

func ExampleBIC() {
fmt.Println(is.BIC("DEUTDEFF"))
fmt.Println(is.BIC("DEUTDEF"))
fmt.Println(is.BIC("deutdeff", validate.BICCaseInsensitive()))
// Output:
// true
// false
// true
}

func ExampleISIN() {
fmt.Println(is.ISIN("US0378331005"))
fmt.Println(is.ISIN("US037833100"))
Expand Down
8 changes: 8 additions & 0 deletions is/identifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ func IBAN(value string) bool {
return validate.IBAN(value) == nil
}

// BIC validates whether the value is a valid Business Identifier Code (BIC / SWIFT).
// See [github.com/muonsoft/validation/validate.BIC] for validation rules and options.
//
// See https://en.wikipedia.org/wiki/ISO_9362.
func BIC(value string, options ...func(*validate.BICOptions)) bool {
return validate.BIC(value, options...) == nil
}

// ISIN validates whether the value is a valid International Securities Identification Number (ISIN).
// See [github.com/muonsoft/validation/validate.ISIN] for validation rules and possible errors.
//
Expand Down
142 changes: 142 additions & 0 deletions it/bic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package it

import (
"context"
"errors"

"github.com/muonsoft/validation"
"github.com/muonsoft/validation/validate"
)

// BICConstraint validates whether a string value is a valid Business Identifier Code (BIC / SWIFT).
//
// By default, validation is in strict mode (uppercase ASCII only), matching Symfony Bic::VALIDATION_MODE_STRICT.
// Use [BICConstraint.CaseInsensitive] for Symfony's case-insensitive mode.
//
// Use [BICConstraint.WithIBAN] to assert that the BIC's country/territory matches the given IBAN's country code
// (Symfony Bic "iban" option).
//
// Empty values are skipped; combine with [NotBlank] or similar to reject empty strings.
//
// See https://en.wikipedia.org/wiki/ISO_9362.
type BICConstraint struct {
isIgnored bool
groups []string
caseInsensitive bool
iban string
err error
ibanErr error
messageTemplate string
ibanMessageTemplate string
messageParameters validation.TemplateParameterList
ibanMessageParameters validation.TemplateParameterList
}

// IsBIC validates whether the value is a valid Business Identifier Code (BIC / SWIFT).
// Behavior is aligned with Symfony\Component\Validator\Constraints\Bic.
func IsBIC() BICConstraint {
return BICConstraint{
err: validation.ErrInvalidBIC,
ibanErr: validation.ErrBICIBANCountryMismatch,
messageTemplate: validation.ErrInvalidBIC.Message(),
ibanMessageTemplate: validation.ErrBICIBANCountryMismatch.Message(),
}
}

// CaseInsensitive enables Symfony Bic::VALIDATION_MODE_CASE_INSENSITIVE (lowercase letters allowed).
func (c BICConstraint) CaseInsensitive() BICConstraint {
c.caseInsensitive = true
return c
}

// WithIBAN sets an IBAN to check that its country code matches the BIC (Symfony Bic "iban" option).
func (c BICConstraint) WithIBAN(iban string) BICConstraint {
c.iban = iban
return c
}

// WithError overrides the default error for the main BIC format violation.
// IBAN country mismatch still uses [validation.ErrBICIBANCountryMismatch] unless overridden via [BICConstraint.WithIBANError].
func (c BICConstraint) WithError(err error) BICConstraint {
c.err = err
return c
}

// WithIBANError overrides the default error for BIC vs IBAN country mismatch.
func (c BICConstraint) WithIBANError(err error) BICConstraint {
c.ibanErr = err
return c
}

// WithMessage sets the violation message template for the main BIC format violation.
func (c BICConstraint) WithMessage(template string, parameters ...validation.TemplateParameter) BICConstraint {
c.messageTemplate = template
c.messageParameters = parameters
return c
}

// WithIBANMessage sets the violation message template when the BIC does not match the configured IBAN.
// Default parameters: {{ value }}, {{ iban }}.
func (c BICConstraint) WithIBANMessage(template string, parameters ...validation.TemplateParameter) BICConstraint {
c.ibanMessageTemplate = template
c.ibanMessageParameters = parameters
return c
}

// When enables conditional validation of this constraint.
func (c BICConstraint) When(condition bool) BICConstraint {
c.isIgnored = !condition
return c
}

// WhenGroups enables conditional validation by validation groups.
func (c BICConstraint) WhenGroups(groups ...string) BICConstraint {
c.groups = groups
return c
}

func (c BICConstraint) bicOptions() []func(*validate.BICOptions) {
var opts []func(*validate.BICOptions)
if c.caseInsensitive {
opts = append(opts, validate.BICCaseInsensitive())
}
if c.iban != "" {
opts = append(opts, validate.BICWithIBAN(c.iban))
}
return opts
}

func (c BICConstraint) ValidateString(ctx context.Context, validator *validation.Validator, value *string) error {
if c.isIgnored || validator.IsIgnoredForGroups(c.groups...) || value == nil || *value == "" {
return nil
}

err := validate.BIC(*value, c.bicOptions()...)
if err == nil {
return nil
}

if errors.Is(err, validate.ErrBICIBANCountryMismatch) {
return validator.BuildViolation(ctx, c.ibanErr, c.ibanMessageTemplate).
WithParameters(
c.ibanMessageParameters.Prepend(
validation.TemplateParameter{Key: "{{ iban }}", Value: c.iban},
validation.TemplateParameter{Key: "{{ value }}", Value: *value},
)...,
).
Create()
}

return validator.BuildViolation(ctx, c.err, c.messageTemplate).
WithParameters(
c.messageParameters.Prepend(
validation.TemplateParameter{Key: "{{ value }}", Value: *value},
)...,
).
Create()
}

// Validate implements [validation.Constraint][string].
func (c BICConstraint) Validate(ctx context.Context, validator *validation.Validator, v string) error {
return c.ValidateString(ctx, validator, &v)
}
14 changes: 14 additions & 0 deletions it/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ func ExampleIsIBAN_invalid() {
// violation: "This is not a valid International Bank Account Number (IBAN)."
}

func ExampleIsBIC_valid() {
err := validator.Validate(context.Background(), validation.String("DEUTDEFF", it.IsBIC()))
fmt.Println(err)
// Output:
// <nil>
}

func ExampleIsBIC_invalid() {
err := validator.Validate(context.Background(), validation.String("deutdeff", it.IsBIC()))
fmt.Println(err)
// Output:
// violation: "This is not a valid Business Identifier Code (BIC)."
}

func ExampleIsISIN_valid() {
err := validator.Validate(context.Background(), validation.String("US0378331005", it.IsISIN()))
fmt.Println(err)
Expand Down
Loading
Loading