Skip to content
Open
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
87 changes: 87 additions & 0 deletions Proposals/NNNN-uuid-regex-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# UUID Regex Parser Component

* Proposal: [SF-NNNN](NNNN-uuid-regex-parser.md)
* Authors: [beltradini](https://github.com/beltradini)
* Review Manager: TBD
* Status: **Awaiting implementation**
* Bug: [swiftlang/swift-foundation#1470](https://github.com/swiftlang/swift-foundation/issues/1470)
* Implementation: [swiftlang/swift-foundation#1547](https://github.com/swiftlang/swift-foundation/pull/1547)

## Introduction

This proposal adds a dedicated UUID parser component for Swift Regex so callers can match and capture UUID text directly as `UUID` values.

## Motivation

Users can currently match UUID-shaped text with a string pattern and then manually construct `UUID(uuidString:)` in a follow-up step. This is repetitive and easy to get wrong when combining with larger regexes.

A first-class regex component improves ergonomics and makes captures strongly typed.

## Proposed solution

Add a new nested type, `UUID.RegexComponent`, that conforms to `CustomConsumingRegexComponent`.

Also add a convenience regex factory:

```swift
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should all be FoundationPreview 6.4, I think.

extension RegexComponent where Self == UUID.RegexComponent {
public static var uuid: UUID.RegexComponent { UUID.RegexComponent() }
}
```

This enables regex usage like:

```swift
let match = input.firstMatch(of: /id=\(.uuid)/)
```

## Detailed design

`UUID.RegexComponent` consumes exactly 36 characters from the current regex index and uses `UUID(uuidString:)` to validate and produce a typed `UUID` capture.

If there are fewer than 36 characters remaining, or the 36-character slice is not a valid UUID, matching fails at that position.

### API surface

```swift
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension UUID {
public struct RegexComponent : CustomConsumingRegexComponent {
public init()
public typealias RegexOutput = UUID
public func consuming(
_ input: String,
startingAt index: String.Index,
in bounds: Range<String.Index>
) throws -> (upperBound: String.Index, output: UUID)?
}
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension RegexComponent where Self == UUID.RegexComponent {
public static var uuid: UUID.RegexComponent
}
```

## Source compatibility

This is an additive API change with no source break.

## Implications on adoption

Adopters can gate use with standard availability checks for Regex APIs (`macOS 13 / iOS 16 / tvOS 16 / watchOS 9`).

## Future directions

Future revisions could add parsing components for other common scalar identifiers that already have string initializers.

## Alternatives considered

### Keep using string captures + manual parsing

This remains possible but duplicates parsing work at call sites and weakens type-safety.

### Make `UUID` itself the regex component

Using a dedicated parser type avoids making ordinary `UUID` values double as parser instances and keeps API intent explicit.
35 changes: 35 additions & 0 deletions Sources/FoundationEssentials/UUID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,41 @@ extension UUID : Codable {
}
}

// MARK: Regex

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension UUID {
/// A regex component that parses a UUID string and captures it as a `UUID`.
public struct RegexComponent : CustomConsumingRegexComponent {
public init() {}

public typealias RegexOutput = UUID

public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: UUID)? {
guard index < bounds.upperBound else {
return nil
}

let uuidStringLength = 36
guard let endIndex = input.index(index, offsetBy: uuidStringLength, limitedBy: bounds.upperBound),
let parsed = UUID(uuidString: String(input[index..<endIndex])) else {
return nil
}

return (endIndex, parsed)
}
}
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension RegexComponent where Self == UUID.RegexComponent {
/// Creates a regex component to match a UUID string, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F",
/// and capture the string as a `UUID`.
public static var uuid: UUID.RegexComponent {
UUID.RegexComponent()
}
}

@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
extension UUID : Comparable {
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
Expand Down
22 changes: 22 additions & 0 deletions Tests/FoundationEssentialsTests/UUIDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ private struct UUIDTests {
}
}



@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
@Test func regexComponentParsesUUID() throws {
let input = "prefix E621E1F8-C36C-495A-93FC-0C247A3E6E5F suffix"
let match = try #require(input.firstMatch(of: /prefix \(.uuid) suffix/))
#expect(match.1 == UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"))
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
@Test func regexComponentRejectsInvalidUUID() throws {
let input = "prefix E621E1F8-C36C-495A-93FC-0C247A3E6E5Z suffix"
let match = try input.firstMatch(of: /prefix \(.uuid) suffix/)
#expect(match == nil)
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
@Test func regexComponentCanBeUsedDirectly() throws {
let input = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
let match = try #require(input.wholeMatch(of: UUID.RegexComponent()))
#expect(match.output == UUID(uuidString: input))
}
@Test func deterministicRandomGeneration() {
var generator = PCGRandomNumberGenerator(seed: 123456789)

Expand Down