diff --git a/CHANGELOG.md b/CHANGELOG.md index eccc36e7..1e38012b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog -## v5.3.3 +## v5.4.0 - Use new `Chunk()` LINQ API for .NET 6 and change own `Partition()` method into polyfill for older framework targets. +- Added `AcceptCountryRule` to restrict validation to a specific set of countries. The rule can be added via the validator options or dependency registration extensions. ## v5.3.2 diff --git a/src/IbanNet/Resources.Designer.cs b/src/IbanNet/Resources.Designer.cs index 147553a4..87293347 100644 --- a/src/IbanNet/Resources.Designer.cs +++ b/src/IbanNet/Resources.Designer.cs @@ -8,9 +8,10 @@ // //------------------------------------------------------------------------------ +using System.Reflection; + namespace IbanNet { using System; - using System.Reflection; /// @@ -20,7 +21,7 @@ namespace IbanNet { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -61,6 +62,15 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to At least one country code must be provided.. + /// + internal static string ArgumentException_At_least_one_country_code_must_be_provided { + get { + return ResourceManager.GetString("ArgumentException_At_least_one_country_code_must_be_provided", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid country code. must be exactly two characters long.. /// @@ -124,6 +134,15 @@ internal static string ArgumentException_The_structure_segment_0_is_invalid { } } + /// + /// Looks up a localized string similar to Bank account numbers from country {0} are not accepted.. + /// + internal static string CountryNotAcceptedResult_Bank_account_numbers_from_country_0_are_not_accepted { + get { + return ResourceManager.GetString("CountryNotAcceptedResult_Bank_account_numbers_from_country_0_are_not_accepted", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enum value '{0}' should be defined in the '{1}' enum.. /// diff --git a/src/IbanNet/Resources.resx b/src/IbanNet/Resources.resx index cf58bbf2..0a87800f 100644 --- a/src/IbanNet/Resources.resx +++ b/src/IbanNet/Resources.resx @@ -179,4 +179,10 @@ The country '{0}' does not define a IBAN pattern. + + Bank account numbers from country {0} are not accepted. + + + At least one country code must be provided. + diff --git a/src/IbanNet/Validation/Results/CountryNotAcceptedResult.cs b/src/IbanNet/Validation/Results/CountryNotAcceptedResult.cs new file mode 100644 index 00000000..0e9bc541 --- /dev/null +++ b/src/IbanNet/Validation/Results/CountryNotAcceptedResult.cs @@ -0,0 +1,16 @@ +using IbanNet.Registry; + +namespace IbanNet.Validation.Results; + +/// +public class CountryNotAcceptedResult : ErrorResult +{ + /// + /// The result returned when the IBAN is not accepted because of restrictions by country. + /// + /// The country that was rejected. + public CountryNotAcceptedResult(IbanCountry country) + : base(string.Format(Resources.CountryNotAcceptedResult_Bank_account_numbers_from_country_0_are_not_accepted, country.DisplayName)) + { + } +} diff --git a/src/IbanNet/Validation/Rules/AcceptCountryRule.cs b/src/IbanNet/Validation/Rules/AcceptCountryRule.cs new file mode 100644 index 00000000..3c586dcc --- /dev/null +++ b/src/IbanNet/Validation/Rules/AcceptCountryRule.cs @@ -0,0 +1,39 @@ +using IbanNet.Validation.Results; + +namespace IbanNet.Validation.Rules; + +/// +/// A validation rule that accepts only specific countries. +/// The rule can be used if your use case needs a limitation on countries to allow. +/// +/// Returns on validation failures. +public class AcceptCountryRule : IIbanValidationRule +{ + private readonly ISet _acceptedCountryCodes; + + /// + /// Initializes a new instance of the using specified . + /// + /// An enumerable of accepted country codes (2 letter ISO region name) + public AcceptCountryRule(IEnumerable acceptedCountryCodes) + { + if (acceptedCountryCodes is null) + { + throw new ArgumentNullException(nameof(acceptedCountryCodes)); + } + + _acceptedCountryCodes = new HashSet(acceptedCountryCodes.Select(cc => cc.ToUpperInvariant()), StringComparer.Ordinal); + if (_acceptedCountryCodes.Count == 0) + { + throw new ArgumentException(Resources.ArgumentException_At_least_one_country_code_must_be_provided, nameof(acceptedCountryCodes)); + } + } + + /// + public ValidationRuleResult Validate(ValidationRuleContext context) + { + return _acceptedCountryCodes.Contains(context.Country!.TwoLetterISORegionName) + ? ValidationRuleResult.Success + : new CountryNotAcceptedResult(context.Country); + } +} diff --git a/test/IbanNet.Tests/Validation/Rules/AcceptCountryRuleTests.cs b/test/IbanNet.Tests/Validation/Rules/AcceptCountryRuleTests.cs new file mode 100644 index 00000000..cc0714c2 --- /dev/null +++ b/test/IbanNet.Tests/Validation/Rules/AcceptCountryRuleTests.cs @@ -0,0 +1,72 @@ +using IbanNet.Registry; +using IbanNet.Validation.Results; + +namespace IbanNet.Validation.Rules; + +public class AcceptCountryRuleTests +{ + [Theory] + [InlineData("NL91ABNA0417164300")] + [InlineData("BE68539007547034")] + public void Given_that_country_code_is_accepted_when_validating_it_should_return_success(string value) + { + var sut = new AcceptCountryRule(new[] { "NL", "BE" }); + IbanCountry ibanCountry = IbanRegistry.Default[value.Substring(0, 2)]; + + // Act + ValidationRuleResult actual = sut.Validate(new ValidationRuleContext(value, ibanCountry)); + + // Assert + actual.Should().Be(ValidationRuleResult.Success); + } + + [Theory] + [InlineData("NL91ABNA0417164300")] + [InlineData("BE68539007547034")] + public void Given_that_country_code_is_not_accepted_when_validating_it_should_return_error(string value) + { + var sut = new AcceptCountryRule(new[] { "DE", "FR" }); + IbanCountry ibanCountry = IbanRegistry.Default[value.Substring(0, 2)]; + + // Act + ValidationRuleResult actual = sut.Validate(new ValidationRuleContext(value, ibanCountry)); + + // Assert + actual.Should() + .BeOfType() + .Which.ErrorMessage.Should() + .Be(string.Format(Resources.CountryNotAcceptedResult_Bank_account_numbers_from_country_0_are_not_accepted, ibanCountry.DisplayName)); + } + + [Fact] + public void Given_that_list_is_null_when_creating_rule_it_should_throw() + { + IEnumerable acceptedCountryCodes = null; + + // Act + // ReSharper disable once AssignNullToNotNullAttribute + Func act = () => new AcceptCountryRule(acceptedCountryCodes); + + // Assert + act.Should() + .ThrowExactly() + .Which.ParamName.Should() + .Be(nameof(acceptedCountryCodes)); + } + + [Fact] + public void Given_that_list_is_empty_when_creating_rule_it_should_throw() + { + IEnumerable acceptedCountryCodes = Enumerable.Empty(); + + // Act + Func act = () => new AcceptCountryRule(acceptedCountryCodes); + + // Assert + act.Should() + .ThrowExactly() + .WithMessage(Resources.ArgumentException_At_least_one_country_code_must_be_provided + "*") + .Which.ParamName.Should() + .Be(nameof(acceptedCountryCodes)); + } +}