Skip to content
Draft
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
8 changes: 8 additions & 0 deletions RevoValidation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -96,6 +98,8 @@
EA1A0C732D085BFE00518928 /* RuleRegexp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleRegexp.swift; sourceTree = "<group>"; };
EA1A0C752D085C4700518928 /* RuleRegexpTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleRegexpTest.swift; sourceTree = "<group>"; };
EA1A0C772D085CDA00518928 /* RuleUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleUrl.swift; sourceTree = "<group>"; };
866C0A6DA62147E28D38A781 /* RuleIp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleIp.swift; sourceTree = "<group>"; };
9BE381B85AFB44D6A32A60FD /* RuleIpTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleIpTest.swift; sourceTree = "<group>"; };
EAFDCCD728DC71BD0038612F /* RuleUnique.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleUnique.swift; sourceTree = "<group>"; };
EAFDCCD928DC73270038612F /* ValidationTranslator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationTranslator.swift; sourceTree = "<group>"; };
F63AA1DD2E9F9DB300DBAF39 /* RuleNifDominicanRepublic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleNifDominicanRepublic.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -177,6 +181,7 @@
F63AA1F12E9F9DC500DBAF39 /* RuleNifTest.swift */,
F63AA1F22E9F9DC500DBAF39 /* SpainNifRuleTest.swift */,
EA1A0C752D085C4700518928 /* RuleRegexpTest.swift */,
9BE381B85AFB44D6A32A60FD /* RuleIpTest.swift */,
BBB84B02248D23B3004A7C76 /* Info.plist */,
B8EAC2B82DA5236F00329689 /* RuleDominicanTaxIdentificationNumberTest.swift */,
);
Expand All @@ -202,6 +207,7 @@
children = (
F63AA1E72E9F9DB300DBAF39 /* NIF */,
EA1A0C772D085CDA00518928 /* RuleUrl.swift */,
866C0A6DA62147E28D38A781 /* RuleIp.swift */,
EA1A0C732D085BFE00518928 /* RuleRegexp.swift */,
EAFDCCD728DC71BD0038612F /* RuleUnique.swift */,
BBB84B0F248D23F6004A7C76 /* RuleContainsNumber.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
1 change: 1 addition & 0 deletions RevoValidation/src/Rules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
14 changes: 14 additions & 0 deletions RevoValidation/src/Rules/RuleIp.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
64 changes: 64 additions & 0 deletions RevoValidationTests/RuleIpTest.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
18 changes: 18 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand All @@ -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