From 7f4c79a1344fb49bbeddde55eebdf27e20346567 Mon Sep 17 00:00:00 2001 From: alex beltrannn Date: Tue, 14 Oct 2025 18:23:54 -0600 Subject: [PATCH] Add UUID RegexComponent support Implements UUID parser for Swift Regex API. - Add UUID.parser for direct String to UUID conversion - Add UUID.caseInsensitiveParser variant - Add UUID: RegexComponent conformance - Add comprehensive test suite (7 test cases) --- Sources/FoundationEssentials/UUID.swift | 156 +++++++++++++++++ .../FoundationEssentialsTests/UUIDTests.swift | 161 ++++++++++++++++++ 2 files changed, 317 insertions(+) diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 568e12b88..b0582d6d5 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -11,6 +11,10 @@ internal import _FoundationCShims // uuid.h +#if canImport(RegexBuilder) +import RegexBuilder +#endif + public typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) @@ -194,3 +198,155 @@ extension UUID : Comparable { return result < 0 } } + +// MARK: - Regex Support + +#if canImport(RegexBuilder) +import RegexBuilder + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +extension UUID: RegexComponent { + /// The regex pattern that matches UUID strings. + /// + /// Matches UUIDs in the standard format: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` + /// where X represents a hexadecimal digit (0-9, A-F, a-f). + /// + /// Example usage: + /// ```swift + /// let text = "User ID: E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + /// let regex = Regex { + /// "User ID: " + /// Capture { UUID.regex } + /// } + /// if let match = text.firstMatch(of: regex) { + /// let uuidString = String(match.1) + /// let uuid = UUID(uuidString: uuidString) + /// } + /// ``` + public var regex: Regex { + Regex { + // 8 hex digits + Repeat(count: 8) { + CharacterClass(.hexDigit) + } + "-" + // 4 hex digits + Repeat(count: 4) { + CharacterClass(.hexDigit) + } + "-" + // 4 hex digits + Repeat(count: 4) { + CharacterClass(.hexDigit) + } + "-" + // 4 hex digits + Repeat(count: 4) { + CharacterClass(.hexDigit) + } + "-" + // 12 hex digits + Repeat(count: 12) { + CharacterClass(.hexDigit) + } + } + } +} + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +extension RegexComponent where Self == UUID { + /// A regex component that matches UUID strings. + /// + /// This component matches UUID strings in the standard format: + /// `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` where X represents a hexadecimal digit. + /// + /// Example usage: + /// ```swift + /// let text = "Session: E621E1F8-C36C-495A-93FC-0C247A3E6E5F active" + /// let regex = Regex { + /// "Session: " + /// Capture { UUID.regex } + /// " active" + /// } + /// if let match = text.firstMatch(of: regex) { + /// let uuidString = String(match.1) + /// let uuid = UUID(uuidString: uuidString) + /// } + /// ``` + public static var regex: UUID { UUID() } +} + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +extension UUID { + /// A regex component that captures and parses UUID strings into UUID instances. + /// + /// This parser matches UUID strings in the standard format: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` + /// where X is a hexadecimal digit (0-9, A-F, a-f). If the input does not match this format, + /// or is not a valid UUID string, the parser returns `nil`. + /// + /// Example usage: + /// ```swift + /// let text = "User ID: E621E1F8-C36C-495A-93FC-0C247A3E6E5F end" + /// let regex = Regex { + /// "User ID: " + /// Capture { UUID.parser } + /// " end" + /// } + /// if let match = text.firstMatch(of: regex) { + /// let uuid: UUID = match.1 // Direct UUID instance + /// print("Found UUID: \(uuid)") + /// } + public static var parser: some RegexComponent { + TryCapture { + Repeat(count: 8) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 12) { .hexDigit } + } transform: { (match: Substring) -> UUID? in + // match is the captured substring; convert to UUID + UUID(uuidString: String(match)) + } + } + + /// A case-insensitive regex component that captures and parses UUID strings into UUID instances. + /// + /// This parser matches UUID strings in both uppercase and lowercase format: + /// `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` or `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` + /// where X is a hexadecimal digit (0-9, A-F, a-f). If the input does not match this format, + /// or is not a valid UUID string, the parser returns `nil`. + /// + /// Example usage: + /// ```swift + /// let text = "User ID: e621e1f8-c36c-495a-93fc-0c247a3e6e5f end" + /// let regex = Regex { + /// "User ID: " + /// Capture { UUID.caseInsensitiveParser } + /// " end" + /// } + /// if let match = text.firstMatch(of: regex) { + /// let uuid: UUID = match.1 // Direct UUID instance + /// print("Found UUID: \(uuid)") + /// } + public static var caseInsensitiveParser: some RegexComponent { + TryCapture { + Repeat(count: 8) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 4) { .hexDigit } + "-" + Repeat(count: 12) { .hexDigit } + } transform: { (match: Substring) -> UUID? in + // match is the captured substring; convert to UUID + UUID(uuidString: String(match)) + } + } +} +#endif diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 424886a55..98773aa61 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -185,3 +185,164 @@ fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator { } } +#if canImport(RegexBuilder) +import RegexBuilder + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +@Suite("UUID Regex Support") +private struct UUIDRegexTests { + + @Test("UUID RegexComponent basic matching") + func uuidRegexComponent() { + let validUUIDs = [ + "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", + "123e4567-e89b-12d3-a456-426614174000", + "00000000-0000-0000-0000-000000000000", + "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + ] + + for uuidString in validUUIDs { + let regex = Regex { + UUID.regex + } + + #expect(uuidString.firstMatch(of: regex) != nil, + "Should match valid UUID: \(uuidString)") + } + } + + @Test("UUID regex in context") + func uuidRegexInContext() { + let testCases = [ + ("User ID: E621E1F8-C36C-495A-93FC-0C247A3E6E5F end", "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"), + ("Session: 123e4567-e89b-12d3-a456-426614174000 active", "123e4567-e89b-12d3-a456-426614174000"), + ("Token 00000000-0000-0000-0000-000000000000 expired", "00000000-0000-0000-0000-000000000000") + ] + + for (input, expectedUUID) in testCases { + let regex = Regex { + OneOrMore(.word) + ": " + Capture { UUID.regex } + " " + OneOrMore(.word) + } + + if let match = input.firstMatch(of: regex) { + let capturedUUID = String(match.1) + #expect(capturedUUID.uppercased() == expectedUUID.uppercased()) + #expect(UUID(uuidString: capturedUUID) != nil) + } else { + Issue.record("Should match UUID in context: \(input)") + } + } + } + + @Test("UUID parser direct conversion") + func uuidParser() throws { + let testCases = [ + "ID: E621E1F8-C36C-495A-93FC-0C247A3E6E5F", + "UUID: 123e4567-e89b-12d3-a456-426614174000", + "Key: 00000000-0000-0000-0000-000000000000" + ] + + for input in testCases { + let regex = Regex { + OneOrMore(.word) + ": " + Capture { UUID.parser } + } + + let match = try #require(input.firstMatch(of: regex), + "Should parse UUID directly from: \(input)") + let uuid: UUID = match.1 + #expect(input.contains(uuid.uuidString)) + } + } + + @Test("UUID case insensitive parser") + func uuidCaseInsensitiveParser() throws { + let testCases = [ + ("ID: e621e1f8-c36c-495a-93fc-0c247a3e6e5f", "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"), + ("UUID: 123E4567-E89B-12D3-A456-426614174000", "123E4567-E89B-12D3-A456-426614174000"), + ("Key: FfFfFfFf-FfFf-FfFf-FfFf-FfFfFfFfFfFf", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") + ] + + for (input, expectedUppercase) in testCases { + let regex = Regex { + OneOrMore(.word) + ": " + Capture { UUID.caseInsensitiveParser } + } + + let match = try #require(input.firstMatch(of: regex), + "Should parse case-insensitive UUID from: \(input)") + let uuid: UUID = match.1 + #expect(uuid.uuidString == expectedUppercase) + } + } + + @Test("Invalid UUIDs should not match") + func invalidUUIDs() { + let invalidUUIDs = [ + "E621E1F8-C36C-495A-93FC-0C247A3E6E5", // Too short + "E621E1F8-C36C-495A-93FC-0C247A3E6E5FF", // Too long + "E621E1F8_C36C_495A_93FC_0C247A3E6E5F", // Wrong separator + "GGGGGGGG-GGGG-GGGG-GGGG-GGGGGGGGGGGG", // Invalid hex + "E621E1F8-C36C-495A-93FC", // Missing parts + "E621E1F8-C36C-495A-93FC-0C247A3E6E5F-EXTRA", // Extra parts + ] + + for invalidUUID in invalidUUIDs { + let regex = Regex { + UUID.regex + } + + #expect(invalidUUID.firstMatch(of: regex) == nil, + "Should not match invalid UUID: \(invalidUUID)") + } + } + + @Test("UUID parser graceful failure") + func uuidParserFailsGracefully() { + let invalidInputs = [ + "ID: E621E1F8-C36C-495A-93FC", // Incomplete UUID + "UUID: GGGGGGGG-GGGG-GGGG-GGGG-GGGGGGGGGGGG", // Invalid hex + ] + + for input in invalidInputs { + let regex = Regex { + OneOrMore(.word) + ": " + Capture { UUID.parser } + } + + #expect(input.firstMatch(of: regex) == nil, + "Parser should fail gracefully for invalid UUID: \(input)") + } + } + + @Test("Multiple UUIDs in text") + func multipleUUIDsInText() { + let text = "First: E621E1F8-C36C-495A-93FC-0C247A3E6E5F and Second: 123e4567-e89b-12d3-a456-426614174000 done" + + let regex = Regex { + Capture { UUID.parser } + } + + let matches = text.matches(of: regex) + #expect(matches.count == 2) + + let expectedUUIDs = [ + "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", + "123E4567-E89B-12D3-A456-426614174000" + ] + + for (index, match) in matches.enumerated() { + let uuid: UUID = match.1 + #expect(uuid.uuidString == expectedUUIDs[index]) + } + } +} +#endif