diff --git a/RevoValidation.xcodeproj/project.pbxproj b/RevoValidation.xcodeproj/project.pbxproj index 0446f68..e73ce93 100644 --- a/RevoValidation.xcodeproj/project.pbxproj +++ b/RevoValidation.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ EA1A0C742D085C0500518928 /* RuleRegexp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1A0C732D085BFE00518928 /* RuleRegexp.swift */; }; EA1A0C762D085C4F00518928 /* RuleRegexpTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1A0C752D085C4700518928 /* RuleRegexpTest.swift */; }; EA1A0C782D085CDA00518928 /* RuleUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1A0C772D085CDA00518928 /* RuleUrl.swift */; }; + F5FE9C9C09064FE0B9BE7A26 /* RuleIp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866C0A6DA62147E28D38A781 /* RuleIp.swift */; }; + F3B2FA2F789F426CB2890B20 /* RuleIpTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE381B85AFB44D6A32A60FD /* RuleIpTest.swift */; }; EAFDCCD828DC71BD0038612F /* RuleUnique.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFDCCD728DC71BD0038612F /* RuleUnique.swift */; }; EAFDCCDA28DC73270038612F /* ValidationTranslator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFDCCD928DC73270038612F /* ValidationTranslator.swift */; }; F63AA1E82E9F9DB300DBAF39 /* RuleNifPortugal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63AA1DF2E9F9DB300DBAF39 /* RuleNifPortugal.swift */; }; @@ -96,6 +98,8 @@ EA1A0C732D085BFE00518928 /* RuleRegexp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleRegexp.swift; sourceTree = ""; }; EA1A0C752D085C4700518928 /* RuleRegexpTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleRegexpTest.swift; sourceTree = ""; }; EA1A0C772D085CDA00518928 /* RuleUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleUrl.swift; sourceTree = ""; }; + 866C0A6DA62147E28D38A781 /* RuleIp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleIp.swift; sourceTree = ""; }; + 9BE381B85AFB44D6A32A60FD /* RuleIpTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleIpTest.swift; sourceTree = ""; }; EAFDCCD728DC71BD0038612F /* RuleUnique.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleUnique.swift; sourceTree = ""; }; EAFDCCD928DC73270038612F /* ValidationTranslator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationTranslator.swift; sourceTree = ""; }; F63AA1DD2E9F9DB300DBAF39 /* RuleNifDominicanRepublic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleNifDominicanRepublic.swift; sourceTree = ""; }; @@ -177,6 +181,7 @@ F63AA1F12E9F9DC500DBAF39 /* RuleNifTest.swift */, F63AA1F22E9F9DC500DBAF39 /* SpainNifRuleTest.swift */, EA1A0C752D085C4700518928 /* RuleRegexpTest.swift */, + 9BE381B85AFB44D6A32A60FD /* RuleIpTest.swift */, BBB84B02248D23B3004A7C76 /* Info.plist */, B8EAC2B82DA5236F00329689 /* RuleDominicanTaxIdentificationNumberTest.swift */, ); @@ -202,6 +207,7 @@ children = ( F63AA1E72E9F9DB300DBAF39 /* NIF */, EA1A0C772D085CDA00518928 /* RuleUrl.swift */, + 866C0A6DA62147E28D38A781 /* RuleIp.swift */, EA1A0C732D085BFE00518928 /* RuleRegexp.swift */, EAFDCCD728DC71BD0038612F /* RuleUnique.swift */, BBB84B0F248D23F6004A7C76 /* RuleContainsNumber.swift */, @@ -459,6 +465,7 @@ files = ( BBB84AEE248D23B0004A7C76 /* ViewController.swift in Sources */, EA1A0C782D085CDA00518928 /* RuleUrl.swift in Sources */, + F5FE9C9C09064FE0B9BE7A26 /* RuleIp.swift in Sources */, F63AA1E82E9F9DB300DBAF39 /* RuleNifPortugal.swift in Sources */, F63AA1E92E9F9DB300DBAF39 /* NIE.swift in Sources */, F63AA1EA2E9F9DB300DBAF39 /* CIF.swift in Sources */, @@ -495,6 +502,7 @@ buildActionMask = 2147483647; files = ( EA1A0C762D085C4F00518928 /* RuleRegexpTest.swift in Sources */, + F3B2FA2F789F426CB2890B20 /* RuleIpTest.swift in Sources */, B8EAC2B92DA5236F00329689 /* RuleDominicanTaxIdentificationNumberTest.swift in Sources */, F63AA1F32E9F9DC500DBAF39 /* PortugalNifRuleTest.swift in Sources */, F63AA1F42E9F9DC500DBAF39 /* RuleNifTest.swift in Sources */, diff --git a/RevoValidation/src/Rules.swift b/RevoValidation/src/Rules.swift index 39c5472..37cb9b9 100644 --- a/RevoValidation/src/Rules.swift +++ b/RevoValidation/src/Rules.swift @@ -28,6 +28,7 @@ public struct Rules : ExpressibleByStringLiteral { case "nif" : return RuleNif(nationality: params.last) case "unique" : return RuleUnique(existing: params.last?.explode(",") ?? []) case "url" : return RuleUrl() + case "ip" : return RuleIp() default : return nil } } diff --git a/RevoValidation/src/Rules/RuleIp.swift b/RevoValidation/src/Rules/RuleIp.swift new file mode 100644 index 0000000..89cf2e1 --- /dev/null +++ b/RevoValidation/src/Rules/RuleIp.swift @@ -0,0 +1,14 @@ +import Foundation + +public class RuleIp : Rule { + + override var errorMessage: String { "Needs to be a valid IP address" } + + override public func isValid(_ text:String) -> Bool { + // IPv4 regex: validates octets from 0-255 + let ipv4Pattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + let ipPred = NSPredicate(format:"SELF MATCHES %@", ipv4Pattern) + + return ipPred.evaluate(with: text) + } +} diff --git a/RevoValidationTests/RuleIpTest.swift b/RevoValidationTests/RuleIpTest.swift new file mode 100644 index 0000000..8de8762 --- /dev/null +++ b/RevoValidationTests/RuleIpTest.swift @@ -0,0 +1,64 @@ + +import XCTest +@testable import RevoValidation + +class RuleIpTest: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func test_it_validates_valid_ipv4_addresses(){ + let rule = RuleIp() + + // Valid IPs + XCTAssertTrue(rule.isValid("192.168.1.1")) + XCTAssertTrue(rule.isValid("10.0.0.1")) + XCTAssertTrue(rule.isValid("255.255.255.255")) + XCTAssertTrue(rule.isValid("0.0.0.0")) + XCTAssertTrue(rule.isValid("127.0.0.1")) + XCTAssertTrue(rule.isValid("8.8.8.8")) + XCTAssertTrue(rule.isValid("172.16.0.1")) + } + + func test_it_rejects_invalid_ipv4_addresses(){ + let rule = RuleIp() + + // Invalid IPs - out of range octets + XCTAssertFalse(rule.isValid("256.1.1.1")) + XCTAssertFalse(rule.isValid("1.256.1.1")) + XCTAssertFalse(rule.isValid("1.1.256.1")) + XCTAssertFalse(rule.isValid("1.1.1.256")) + XCTAssertFalse(rule.isValid("999.999.999.999")) + + // Invalid IPs - wrong format + XCTAssertFalse(rule.isValid("1.1.1")) + XCTAssertFalse(rule.isValid("1.1.1.1.1")) + XCTAssertFalse(rule.isValid("192.168.1")) + XCTAssertFalse(rule.isValid("192.168.1.1.1")) + + // Invalid IPs - non-numeric + XCTAssertFalse(rule.isValid("abc.def.ghi.jkl")) + XCTAssertFalse(rule.isValid("192.168.1.abc")) + XCTAssertFalse(rule.isValid("not an ip")) + XCTAssertFalse(rule.isValid("")) + + // Invalid IPs - with spaces + XCTAssertFalse(rule.isValid("192.168. 1.1")) + XCTAssertFalse(rule.isValid(" 192.168.1.1")) + XCTAssertFalse(rule.isValid("192.168.1.1 ")) + } + + func test_it_can_be_used_with_string_literal(){ + let rules: Rules = "ip" + let errors = rules.validate("192.168.1.1") + XCTAssertEqual(errors.count, 0) + + let errorsInvalid = rules.validate("256.1.1.1") + XCTAssertEqual(errorsInvalid.count, 1) + } +} diff --git a/readme.md b/readme.md index 1613826..8a45e0c 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,7 @@ struct ValidationForm : View { @StateObject private var formValidator = FormValidator() @State private var email: String = "" @State private var url: String = "" + @State private var ip: String = "" @State private var name:String = "" var body: some View { @@ -21,6 +22,9 @@ struct ValidationForm : View { TextField("URL", text: $url) .rules(formValidator: formValidator, $url, "url") + + TextField("IP Address", text: $ip) + .rules(formValidator: formValidator, $ip, "ip") HStack{ Spacer() @@ -42,4 +46,18 @@ struct ValidationForm : View { } ``` +## Available Rules + +- `required` - Field is required +- `email` - Validates email format +- `url` - Validates URL format +- `ip` - Validates IPv4 address format +- `numeric` - Must be numeric +- `length:X` - Minimum length of X characters +- `age:X` - Minimum age of X years +- `containsNumber` - Must contain at least one number +- `containsSpecialChars` - Must contain special characters +- `regexp:PATTERN` - Custom regular expression validation +- `nif:COUNTRY` - Validates NIF/Tax ID for specific country (ES, PT, DO) +- `unique:VALUES` - Value must not be in the provided list