Contents

sunghyun-k/swift-path-matcher

**Declarative URL path matching library using Swift Result Builders**

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/sunghyun-k/swift-path-matcher.git", from: "0.1.5")
]

Core Components

| Component | Description | Output Type | |-----------|-------------|-------------| | "path" | Matches exact string (String Literal) | Void | | Literal("path") | Matches exact string (explicit) | Void | | Parameter() | Captures required path segment | String | | OptionalParameter() | Captures optional segment | String? |

Usage

Basic Matching

import PathMatcher

let searchMatcher: PathMatcher<Void> = PathMatcher {
    "search"
}

searchMatcher.match(["search"])  // () - success
searchMatcher.match(["profile"]) // nil - failure

String Literal Syntax

You can use string literals directly instead of Literal():

// These two matchers are equivalent
let matcher1: PathMatcher<String> = PathMatcher {
    Literal("api")
    Literal("users")
    Parameter()
}

let matcher2: PathMatcher<String> = PathMatcher {
    "api"
    "users"
    Parameter()
}

Use explicit Literal() when you need case-insensitive matching.

Multi-Segment Literals

String literals can match multiple segments separated by /:

// All three matchers work identically
let matcher1 = PathMatcher { "api"; "v2"; "books" }
let matcher2 = PathMatcher { "api/v2/books" }
let matcher3 = PathMatcher { Literal("api/v2/books") }

// All match ["api", "v2", "books"]

Case-Insensitive Matching

let matcher = PathMatcher {
    Literal("API/V2", caseInsensitive: true)
}

matcher.match(["api", "v2"])   // success
matcher.match(["API", "V2"])   // success
matcher.match(["Api", "v2"])   // success

Parameter Capture

// Pattern: "owners/:owner"
let ownerMatcher: PathMatcher<String> = PathMatcher {
    "owners"
    Parameter()
}

ownerMatcher.match(["owners", "swiftlang"]) // "swiftlang"

Multiple Parameters

Multiple parameters are automatically flattened into tuples:

// Pattern: "users/:userId/posts/:postId"
let postMatcher: PathMatcher<(String, String)> = PathMatcher {
    "users"
    Parameter()      // userId
    "posts"
    Parameter()      // postId
}

let result = postMatcher.match(["users", "john", "posts", "123"])
// result?.0 == "john", result?.1 == "123"

Optional Parameters

// Pattern: "owners/:owner/:repo?"
let repoMatcher: PathMatcher<(String, String?)> = PathMatcher {
    "owners"
    Parameter()         // required
    OptionalParameter() // optional
}

repoMatcher.match(["owners", "swift", "nio"]) // ("swift", "nio")
repoMatcher.match(["owners", "swift"])        // ("swift", nil)

PathRouter

Register multiple path patterns and route URLs:

var router = PathRouter()

router.append {
    "settings"
} handler: { url, _ in
    showSettings()
}

router.append {
    "users"
    Parameter()
} handler: { url, userID in
    showUser(id: userID)
}

router.append {
    "posts"
    Parameter()
    OptionalParameter()
} handler: { url, params in
    let (postID, action) = params
    showPost(id: postID, action: action)
}

// Handle URL - executes first matching handler
router.handle(URL(string: "myapp:///users/john")!)

Custom Components

Create your own components by implementing the PathComponent protocol:

struct IntParameter: PathComponent {
    typealias Output = Int

    var pattern: PathPattern<Int> {
        PathPattern { components, index in
            guard index < components.endIndex,
                  let value = Int(components[index]) else {
                return nil
            }
            index += 1
            return value
        }
    }
}

// Usage
let matcher: PathMatcher<Int> = PathMatcher {
    "posts"
    IntParameter()  // matches integers only
}

matcher.match(["posts", "123"])   // 123
matcher.match(["posts", "abc"])   // nil

Type System

The Result Builder automatically handles type composition:

  • Void + Void → Void
  • Void + T → T
  • T + Void → T
  • T1 + T2 → (T1, T2)
  • (T1, T2) + T3 → (T1, T2, T3) (flattened up to 6 elements)

Example App

A deep link routing demo using SwiftUI and NavigationStack is included.

Open Example.swiftpm in Xcode to run the Swift Playground app.

Requirements

  • Swift 6.1+

License

MIT License

Package Metadata

Repository: sunghyun-k/swift-path-matcher

Default branch: main

README: README.md