├── Sources
├── URLPatternClient
│ └── main.swift
├── URLPatternMacros
│ ├── Extensions
│ │ └── String+Extensions.swift
│ ├── URLPatternPlugin.swift
│ ├── Utils
│ │ └── URLPatternError.swift
│ ├── URLPatternMacro.swift
│ └── URLPathMacro.swift
└── URLPattern
│ └── URLPattern.swift
├── URLPatternExample
├── URLPatternExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── View
│ │ ├── HomeView.swift
│ │ ├── PostView.swift
│ │ ├── SettingView.swift
│ │ └── CommentView.swift
│ ├── URLPatternExampleApp.swift
│ ├── DeepLink.swift
│ ├── Route.swift
│ └── RootView.swift
├── URLPatternExample.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── URLPatternExample.xcscheme
│ └── project.pbxproj
└── URLPatternExampleTests
│ └── URLPatternExampleTests.swift
├── Package.resolved
├── .github
└── workflows
│ └── swift.yml
├── LICENSE
├── Package.swift
├── .gitignore
├── README.md
└── Tests
└── URLPatternTests
└── URLPatternTests.swift
/Sources/URLPatternClient/main.swift:
--------------------------------------------------------------------------------
1 | import URLPattern
2 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/URLPatternMacros/Extensions/String+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension String {
4 | var isURLPathValue: Bool { self.hasPrefix("{") && self.hasSuffix("}") && self.utf16.count >= 3 }
5 | }
6 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/URLPatternMacros/URLPatternPlugin.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 | import Foundation
6 |
7 | @main
8 | struct URLPatternPlugin: CompilerPlugin {
9 | let providingMacros: [Macro.Type] = [URLPatternMacro.self, URLPathMacro.self]
10 | }
11 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/View/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeView: View {
11 | var body: some View {
12 | Text("🏠")
13 | .font(.largeTitle)
14 | .navigationTitle("Home")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/URLPattern/URLPattern.swift:
--------------------------------------------------------------------------------
1 | @_exported import Foundation
2 |
3 | @attached(member, names: arbitrary)
4 | public macro URLPattern() = #externalMacro(module: "URLPatternMacros", type: "URLPatternMacro")
5 |
6 | @attached(peer, names: arbitrary)
7 | public macro URLPath(_ path: String) = #externalMacro(module: "URLPatternMacros", type: "URLPathMacro")
8 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLPatternExampleApp.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/3/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct URLPatternExampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | RootView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/URLPatternMacros/Utils/URLPatternError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct URLPatternError: LocalizedError {
4 | let errorDescription: String
5 |
6 | init(_ errorDescription: String) {
7 | self.errorDescription = errorDescription
8 | }
9 | }
10 |
11 | extension URLPatternError: CustomStringConvertible {
12 | var description: String { self.errorDescription }
13 | }
14 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/View/PostView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostView.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PostView: View {
11 | let postId: String
12 |
13 | var body: some View {
14 | Text("📮 postId: \(postId)")
15 | .font(.largeTitle)
16 | .navigationTitle("Post")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/View/SettingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingView.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SettingView: View {
11 | let number: Int
12 |
13 | var body: some View {
14 | Text("⚙️ number: \(number)")
15 | .font(.largeTitle)
16 | .navigationTitle("Setting")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/View/CommentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentView.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CommentView: View {
11 | let commentId: String
12 |
13 | var body: some View {
14 | Text("💬 commentId: \(commentId)")
15 | .font(.largeTitle)
16 | .navigationTitle("Comment")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "a3de0173b246e82aff67cee90e8455823733a63c034b091a81ef3d81558d8a5a",
3 | "pins" : [
4 | {
5 | "identity" : "swift-syntax",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/swiftlang/swift-syntax.git",
8 | "state" : {
9 | "revision" : "0687f71944021d616d34d922343dcef086855920",
10 | "version" : "600.0.1"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/DeepLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeepLink.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import URLPattern
9 |
10 | @URLPattern
11 | enum DeepLink: Equatable {
12 | @URLPath("/home")
13 | case home
14 |
15 | @URLPath("/posts/{postId}")
16 | case post(postId: String)
17 |
18 | @URLPath("/posts/{postId}/comments/{commentId}")
19 | case postComment(postId: String, commentId: String)
20 |
21 | @URLPath("/setting/{number}")
22 | case setting(number: Int)
23 | }
24 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "b71e74ae1ae16b256751a8f504b75f549eaf477761821c6159aa06486b495baa",
3 | "pins" : [
4 | {
5 | "identity" : "swift-syntax",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/swiftlang/swift-syntax.git",
8 | "state" : {
9 | "revision" : "0687f71944021d616d34d922343dcef086855920",
10 | "version" : "600.0.1"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/Route.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Route.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/4/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum Route: Hashable, View {
11 | case home
12 | case post(postId: String)
13 | case comment(commentId: String)
14 | case setting(number: Int)
15 |
16 | var body: some View {
17 | switch self {
18 | case .home:
19 | HomeView()
20 | case .post(let postId):
21 | PostView(postId: postId)
22 | case .comment(let commentId):
23 | CommentView(commentId: commentId)
24 | case .setting(let number):
25 | SettingView(number: number)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | }
30 | ],
31 | "info" : {
32 | "author" : "xcode",
33 | "version" : 1
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Swift project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Swift
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | runs-on: macos-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Set Xcode Version 16.1.0
20 | run: sudo xcode-select -s /Applications/Xcode_16.1.app
21 |
22 | - name: Test URLPattern
23 | run: swift test -v
24 |
25 | - name: Test URLPatternExample
26 | run: |
27 | xcodebuild \
28 | -project URLPatternExample/URLPatternExample.xcodeproj \
29 | -scheme URLPatternExample \
30 | -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
31 | test
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Won Heo (Woody)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import CompilerPluginSupport
6 |
7 | let package = Package(
8 | name: "URLPattern",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10 | products: [
11 | .library(
12 | name: "URLPattern",
13 | targets: ["URLPattern"]
14 | ),
15 | .executable(
16 | name: "URLPatternClient",
17 | targets: ["URLPatternClient"]
18 | ),
19 | ],
20 | dependencies: [
21 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
22 | ],
23 | targets: [
24 | .macro(
25 | name: "URLPatternMacros",
26 | dependencies: [
27 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
28 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
29 | ]
30 | ),
31 | .target(name: "URLPattern", dependencies: ["URLPatternMacros"]),
32 | .executableTarget(name: "URLPatternClient", dependencies: ["URLPattern"]),
33 | .testTarget(
34 | name: "URLPatternTests",
35 | dependencies: [
36 | "URLPatternMacros",
37 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
38 | ]
39 | ),
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // URLPatternExample
4 | //
5 | // Created by woody on 4/3/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RootView: View {
11 | @State private var path = NavigationPath()
12 |
13 | var body: some View {
14 | NavigationStack(path: $path) {
15 | List {
16 | Section("DeepLink Example") {
17 | Text("https://domain/home")
18 | Text("https://domain/posts/1")
19 | Text("https://domain/posts/1/comments/2")
20 | Text("https://domain/setting/1")
21 | }
22 | Section("Invalid DeepLink") {
23 | Text("https://home")
24 | Text("https://domain/homes")
25 | Text("https://domain/post/1")
26 | Text("https://domain/setting/string")
27 | }
28 | }
29 | .navigationTitle("URLPattern Example")
30 | .environment(\.openURL, OpenURLAction { url in
31 | guard let deepLink = DeepLink(url: url) else {
32 | return .discarded
33 | }
34 |
35 | switch deepLink {
36 | case .home:
37 | path.append(Route.home)
38 | case .post(let postId):
39 | path.append(Route.post(postId: postId))
40 | case .postComment(let postId, let commentId):
41 | path.append(Route.post(postId: postId))
42 | path.append(Route.comment(commentId: commentId))
43 | case .setting(let number):
44 | path.append(Route.setting(number: number))
45 | }
46 | return .handled
47 | })
48 | .navigationDestination(for: Route.self) { route in
49 | route
50 | }
51 | }
52 | }
53 | }
54 |
55 | #Preview {
56 | RootView()
57 | }
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## Obj-C/Swift specific
9 | *.hmap
10 |
11 | ## App packaging
12 | *.ipa
13 | *.dSYM.zip
14 | *.dSYM
15 |
16 | ## Playgrounds
17 | timeline.xctimeline
18 | playground.xcworkspace
19 |
20 | # Swift Package Manager
21 | #
22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
23 | # Packages/
24 | # Package.pins
25 | # Package.resolved
26 | # *.xcodeproj
27 | #
28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
29 | # hence it is not needed unless you have added a package configuration file to your project
30 | # .swiftpm
31 |
32 | .build/
33 |
34 | # CocoaPods
35 | #
36 | # We recommend against adding the Pods directory to your .gitignore. However
37 | # you should judge for yourself, the pros and cons are mentioned at:
38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
39 | #
40 | # Pods/
41 | #
42 | # Add this line if you want to avoid checking in source code from the Xcode workspace
43 | # *.xcworkspace
44 |
45 | # Carthage
46 | #
47 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
48 | # Carthage/Checkouts
49 |
50 | Carthage/Build/
51 |
52 | # fastlane
53 | #
54 | # It is recommended to not store the screenshots in the git repo.
55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
56 | # For more information about the recommended setup visit:
57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
58 |
59 | fastlane/report.xml
60 | fastlane/Preview.html
61 | fastlane/screenshots/**/*.png
62 | fastlane/test_output
63 | .DS_Store
64 | /.build
65 | /Packages
66 | xcuserdata/
67 | DerivedData/
68 | .swiftpm/configuration/registries.json
69 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
70 | .netrc
71 |
--------------------------------------------------------------------------------
/Sources/URLPatternMacros/URLPatternMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 | import Foundation
6 |
7 | public struct URLPatternMacro: MemberMacro {
8 | public static func expansion(
9 | of node: AttributeSyntax,
10 | providingMembersOf declaration: some DeclGroupSyntax,
11 | in context: some MacroExpansionContext
12 | ) throws -> [DeclSyntax] {
13 | guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
14 | throw URLPatternError("@URLPatternMacro can only be applied to enums")
15 | }
16 |
17 | let cases = enumDecl.memberBlock.members.compactMap { member -> String? in
18 | guard
19 | let caseDecl = member.decl.as(EnumCaseDeclSyntax.self),
20 | caseDecl.attributes.contains(where: {
21 | $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "URLPath"
22 | })
23 | else {
24 | return nil
25 | }
26 |
27 | return caseDecl.elements.first?.name.text
28 | }
29 |
30 | guard Set(cases).count == cases.count else {
31 | throw URLPatternError("Duplicate case names are not allowed")
32 | }
33 |
34 | let urlInitializer = try InitializerDeclSyntax("init?(url: URL)") {
35 | for caseName in cases {
36 | """
37 | if let urlPattern = Self.\(raw: caseName)(url) {
38 | self = urlPattern
39 | return
40 | }
41 | """
42 | }
43 |
44 | """
45 | return nil
46 | """
47 | }
48 |
49 | let isValidURLPathsMethod = try FunctionDeclSyntax("""
50 | static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
51 | guard inputs.count == patterns.count else { return false }
52 |
53 | return zip(inputs, patterns).allSatisfy { input, pattern in
54 | guard Self.isURLPathValue(pattern) else { return input == pattern }
55 |
56 | return true
57 | }
58 | }
59 | """)
60 |
61 | let isURLPathValueMethod = try FunctionDeclSyntax("""
62 | static func isURLPathValue(_ string: String) -> Bool {
63 | return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3
64 | }
65 | """)
66 |
67 | return [
68 | DeclSyntax(urlInitializer),
69 | DeclSyntax(isValidURLPathsMethod),
70 | DeclSyntax(isURLPathValueMethod)
71 | ]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample.xcodeproj/xcshareddata/xcschemes/URLPatternExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
36 |
42 |
43 |
44 |
45 |
46 |
56 |
58 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # URLPattern
2 | [](https://github.com/heoblitz/URLPattern/actions/workflows/swift.yml)
3 | [](https://swiftpackageindex.com/heoblitz/URLPattern)
4 | [](https://swiftpackageindex.com/heoblitz/URLPattern)
5 |
6 | A Swift Macro that helps mapping URLs to Enum cases.
7 |
8 | https://github.com/user-attachments/assets/07bb37fd-f82f-43eb-b34a-ec9bed7f29c8
9 |
10 | ## Overview
11 |
12 | URL deep linking is a fundamental technology widely used in most services today. However, in Swift environments, implementing deep linking typically requires direct URL path manipulation or regex usage:
13 |
14 | ```swift
15 | // Traditional approach with manual URL handling
16 | let paths = url.pathComponents
17 |
18 | if paths.count == 2 && paths[1] == "home" {
19 | // Handle home
20 | } else let match = try? url.path.firstMatch(of: /\/posts\/([^\/]+)$/) {
21 | // Handle posts
22 | }
23 | ```
24 |
25 | This approach reduces code readability and scalability, and importantly, cannot validate incorrect patterns at compile-time.
26 |
27 | URLPattern solves these issues by providing compile-time URL validation and value mapping:
28 |
29 | ```swift
30 | @URLPattern
31 | enum DeepLink {
32 | @URLPath("/home")
33 | case home
34 |
35 | @URLPath("/posts/{postId}")
36 | case post(postId: String)
37 |
38 | @URLPath("/posts/{postId}/comments/{commentId}")
39 | case postComment(postId: String, commentId: String)
40 | }
41 | ```
42 |
43 | ## Features
44 |
45 | - **Compile-time Validation**: Ensures URL path values and associated value names match correctly
46 | - **Automatic Enum Generation**: Creates initializers that map URL components to enum associated values
47 | - **Type Support**:
48 | - Built-in support for `String`, `Int`, `Float`, and `Double`
49 | - Non-String types (Int, Float, Double) use String-based initialization
50 |
51 | ## Usage
52 |
53 | ```swift
54 | @URLPattern
55 | enum DeepLink {
56 | @URLPath("/posts/{postId}")
57 | case post(postId: String)
58 |
59 | @URLPath("/posts/{postId}/comments/{commentId}")
60 | case postComment(postId: String, commentId: String)
61 |
62 | @URLPath("/f/{first}/s/{second}")
63 | case reverse(second: Int, first: Int)
64 | }
65 | ```
66 |
67 | 1. Declare the `@URLPattern` macro on your enum.
68 |
69 | 2. Add `@URLPath` macro to enum cases with the desired URL pattern.
70 |
71 | 3. Use path values with `{associated_value_name}` syntax to map URL components to associated value names. If mapping code is duplicated, the topmost enum case takes precedence.
72 |
73 |
74 | ```swift
75 | // ✅ Valid URLs
76 | DeepLink(url: URL(string: "/posts/1")!) == .post(postId: "1")
77 | DeepLink(url: URL(string: "/posts/1/comments/2")!) == .postComment(postId: "1", commentId: "2")
78 | DeepLink(url: URL(string: "/f/1/s/2")!) == .reverse(second: 2, first: 1)
79 |
80 | // ❌ Invalid URLs
81 | DeepLink(url: URL(string: "/post/1")!) == nil
82 | DeepLink(url: URL(string: "/posts/1/comments")!) == nil
83 | DeepLink(url: URL(string: "/f/string/s/string")!) == nil
84 | ```
85 | 4. Use the `Enum.init(url: URL)` generated initializer.
86 | ```swift
87 | if let deepLink = DeepLink(url: incomingURL) {
88 | switch deepLink {
89 | case .post(let postId):
90 | // Handle post
91 | case .postComment(let postId, let commentId):
92 | // Handle postComment
93 | }
94 | }
95 | ```
96 | 5. Implement a deep link using an enum switch statement.
97 | - For more detailed examples, please refer to the [Example project](https://github.com/heoblitz/URLPattern/tree/main/URLPatternExample).
98 |
99 | ## Rules
100 |
101 | - **Unique Enum Case Names**: Enum case names must be unique for better readability of expanded macro code.
102 | - **Unique Associated Value Names**: Associated value names within each case must be unique.
103 | - **Valid URL Patterns**: Arguments passed to @URLPath macro must be in valid URL path format.
104 | - **Supported Types**: Only String, Int, Float, and Double are supported.
105 |
106 | ## Installation
107 | ### Swift Package Manager
108 | Project > Project Dependencies > Add `https://github.com/heoblitz/URLPattern.git`
109 |
--------------------------------------------------------------------------------
/Sources/URLPatternMacros/URLPathMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 | import Foundation
6 |
7 | public struct URLPathMacro: PeerMacro {
8 | enum SupportedType: String {
9 | case String
10 | case Int
11 | case Double
12 | case Float
13 | }
14 |
15 | struct PatternParam: Hashable {
16 | let name: String
17 | let type: SupportedType
18 | let pathIndex: Int
19 | let caseIndex: Int
20 | }
21 |
22 | public static func expansion(
23 | of node: AttributeSyntax,
24 | providingPeersOf declaration: some DeclSyntaxProtocol,
25 | in context: some MacroExpansionContext
26 | ) throws -> [DeclSyntax] {
27 | guard
28 | let enumCase = declaration.as(EnumCaseDeclSyntax.self),
29 | let element = enumCase.elements.first
30 | else {
31 | throw URLPatternError("@URLPathMacro can only be applied to enum cases")
32 | }
33 |
34 | guard
35 | let argument = node.arguments?.as(LabeledExprListSyntax.self)?.first,
36 | let pathString = argument.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description
37 | else {
38 | throw URLPatternError("URLPath is nil")
39 | }
40 |
41 | guard let pathURL = URL(string: pathString) else {
42 | throw URLPatternError("URLPath is not in a valid URL format")
43 | }
44 |
45 | let patternPaths = pathURL.pathComponents
46 |
47 | let caseAssociatedTypes = try element.parameterClause?.parameters.map { param -> (name: String, type: SupportedType) in
48 | let name = param.firstName?.text ?? ""
49 | let type = param.type.description
50 |
51 | guard let supportedType = SupportedType(rawValue: type) else {
52 | throw URLPatternError("\(type) is not supported as an associated value")
53 | }
54 | return (name: name, type: supportedType)
55 | } ?? []
56 |
57 | let patternParams: [PatternParam] = try patternPaths.enumerated()
58 | .filter { index, value in value.isURLPathValue }
59 | .map { pathIndex, value -> PatternParam in
60 | let name = String(value.dropFirst().dropLast())
61 |
62 | guard let (caseIndex, caseAssociatedType) = caseAssociatedTypes.enumerated().first(where: { name == $0.element.name }) else {
63 | throw URLPatternError("URLPath value \"\(name)\" cannot be found in the associated value")
64 | }
65 |
66 | return PatternParam(
67 | name: name,
68 | type: caseAssociatedType.type,
69 | pathIndex: pathIndex,
70 | caseIndex: caseIndex
71 | )
72 | }
73 | .sorted(by: { $0.caseIndex < $1.caseIndex })
74 |
75 | let patternNames = Set(patternParams.map(\.name))
76 | let caseNames = Set(caseAssociatedTypes.map(\.name))
77 |
78 | guard patternNames.count == patternParams.count else {
79 | throw URLPatternError("The name of an URLPath value cannot be duplicated")
80 | }
81 |
82 | guard caseNames.count == caseAssociatedTypes.count else {
83 | throw URLPatternError("The name of an associated value cannot be duplicated")
84 | }
85 |
86 | guard patternNames.count == caseNames.count else {
87 | throw URLPatternError("The number of associated values does not match URLPath")
88 | }
89 |
90 | guard patternNames == caseNames else {
91 | throw URLPatternError("The name of the URLPath value does not match the associated value")
92 | }
93 |
94 | let staticMethod = try FunctionDeclSyntax("""
95 | static func \(element.name)(_ url: URL) -> Self? {
96 | let inputPaths = url.pathComponents
97 | let patternPaths = \(raw: patternPaths)
98 |
99 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil }
100 | \(raw: patternParams.map { param in
101 | switch param.type {
102 | case .Double, .Float, .Int:
103 | """
104 | guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { return nil }
105 | """
106 | case .String:
107 | """
108 | let \(param.name) = inputPaths[\(param.pathIndex)]
109 | """
110 | }
111 | }.joined(separator: "\n"))
112 | return \(raw: patternParams.isEmpty
113 | ? ".\(element.name.text)"
114 | : ".\(element.name.text)(\(patternParams.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))")
115 | }
116 | """)
117 |
118 | return [DeclSyntax(staticMethod)]
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Tests/URLPatternTests/URLPatternTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntax
2 | import SwiftSyntaxBuilder
3 | import SwiftSyntaxMacros
4 | import SwiftSyntaxMacrosTestSupport
5 | import XCTest
6 |
7 | #if canImport(URLPatternMacros)
8 | import URLPatternMacros
9 |
10 | let testMacros: [String: Macro.Type] = [
11 | "URLPattern": URLPatternMacro.self,
12 | "URLPath": URLPathMacro.self
13 | ]
14 | #endif
15 |
16 | final class URLPatternTests: XCTestCase {
17 | func testDeepLinkMacro_normalCase() throws {
18 | assertMacroExpansion(
19 | """
20 | @URLPattern
21 | enum DeepLink: Equatable {
22 | @URLPath("/home")
23 | case home
24 |
25 | @URLPath("/posts/{postId}")
26 | case post(postId: String)
27 |
28 | @URLPath("/posts/{postId}/comments/{commentId}")
29 | case postComment(postId: String, commentId: String)
30 |
31 | @URLPath("/c/{cNum}/b/{bNum}/a/{aNum}")
32 | case complex(aNum: Int, bNum: Int, cNum: Int)
33 | }
34 | """,
35 | expandedSource: """
36 | enum DeepLink: Equatable {
37 | case home
38 |
39 | static func home(_ url: URL) -> Self? {
40 | let inputPaths = url.pathComponents
41 | let patternPaths = ["/", "home"]
42 |
43 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
44 | return nil
45 | }
46 |
47 | return .home
48 | }
49 |
50 | case post(postId: String)
51 |
52 | static func post(_ url: URL) -> Self? {
53 | let inputPaths = url.pathComponents
54 | let patternPaths = ["/", "posts", "{postId}"]
55 |
56 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
57 | return nil
58 | }
59 | let postId = inputPaths[2]
60 | return .post(postId: postId)
61 | }
62 |
63 | case postComment(postId: String, commentId: String)
64 |
65 | static func postComment(_ url: URL) -> Self? {
66 | let inputPaths = url.pathComponents
67 | let patternPaths = ["/", "posts", "{postId}", "comments", "{commentId}"]
68 |
69 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
70 | return nil
71 | }
72 | let postId = inputPaths[2]
73 | let commentId = inputPaths[4]
74 | return .postComment(postId: postId, commentId: commentId)
75 | }
76 |
77 | case complex(aNum: Int, bNum: Int, cNum: Int)
78 |
79 | static func complex(_ url: URL) -> Self? {
80 | let inputPaths = url.pathComponents
81 | let patternPaths = ["/", "c", "{cNum}", "b", "{bNum}", "a", "{aNum}"]
82 |
83 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
84 | return nil
85 | }
86 | guard let aNum = Int(inputPaths[6]) else {
87 | return nil
88 | }
89 | guard let bNum = Int(inputPaths[4]) else {
90 | return nil
91 | }
92 | guard let cNum = Int(inputPaths[2]) else {
93 | return nil
94 | }
95 | return .complex(aNum: aNum, bNum: bNum, cNum: cNum)
96 | }
97 |
98 | init?(url: URL) {
99 | if let urlPattern = Self.home(url) {
100 | self = urlPattern
101 | return
102 | }
103 | if let urlPattern = Self.post(url) {
104 | self = urlPattern
105 | return
106 | }
107 | if let urlPattern = Self.postComment(url) {
108 | self = urlPattern
109 | return
110 | }
111 | if let urlPattern = Self.complex(url) {
112 | self = urlPattern
113 | return
114 | }
115 | return nil
116 | }
117 |
118 | static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
119 | guard inputs.count == patterns.count else {
120 | return false
121 | }
122 |
123 | return zip(inputs, patterns).allSatisfy { input, pattern in
124 | guard Self.isURLPathValue(pattern) else {
125 | return input == pattern
126 | }
127 |
128 | return true
129 | }
130 | }
131 |
132 | static func isURLPathValue(_ string: String) -> Bool {
133 | return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3
134 | }
135 | }
136 | """,
137 | macros: testMacros
138 | )
139 | }
140 |
141 | func testDeepLinkMacro_shouldIgnoreNoneMacroCase() throws {
142 | assertMacroExpansion(
143 | """
144 | @URLPattern
145 | enum DeepLink: Equatable {
146 | @URLPath("/home")
147 | case home
148 |
149 | case complex(aNum: Int, bNum: Int, cNum: Int)
150 | }
151 | """,
152 | expandedSource: """
153 | enum DeepLink: Equatable {
154 | case home
155 |
156 | static func home(_ url: URL) -> Self? {
157 | let inputPaths = url.pathComponents
158 | let patternPaths = ["/", "home"]
159 |
160 | guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
161 | return nil
162 | }
163 |
164 | return .home
165 | }
166 |
167 | case complex(aNum: Int, bNum: Int, cNum: Int)
168 |
169 | init?(url: URL) {
170 | if let urlPattern = Self.home(url) {
171 | self = urlPattern
172 | return
173 | }
174 | return nil
175 | }
176 |
177 | static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
178 | guard inputs.count == patterns.count else {
179 | return false
180 | }
181 |
182 | return zip(inputs, patterns).allSatisfy { input, pattern in
183 | guard Self.isURLPathValue(pattern) else {
184 | return input == pattern
185 | }
186 |
187 | return true
188 | }
189 | }
190 |
191 | static func isURLPathValue(_ string: String) -> Bool {
192 | return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3
193 | }
194 | }
195 | """,
196 | macros: testMacros
197 | )
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLPatternExampleTests.swift
3 | // URLPatternExampleTests
4 | //
5 | // Created by woody on 4/3/25.
6 | //
7 |
8 | import Testing
9 | import URLPattern
10 |
11 | @URLPattern
12 | enum DeepLinkMock: Equatable {
13 | @URLPath("/home")
14 | case home
15 |
16 | @URLPath("/posts/{postId}")
17 | case post(postId: String)
18 |
19 | @URLPath("/posts/{postId}/comments/{commentId}")
20 | case postComment(postId: String, commentId: String)
21 |
22 | @URLPath("/setting/phone/{number}")
23 | case setting(number: Int)
24 |
25 | @URLPath("/int/{int}")
26 | case int(int: Int)
27 |
28 | @URLPath("/float/{float}")
29 | case float(float: Float)
30 |
31 | @URLPath("/string/{string}")
32 | case string(string: String)
33 |
34 | @URLPath("/c/{cNum}/b/{bNum}/a/{aNum}")
35 | case complex(aNum: Int, bNum: Int, cNum: Int)
36 | }
37 |
38 | struct URLPatternExampleTests {
39 | // MARK: - Home Tests
40 | @Test("Valid Home URLPatterns", arguments: [
41 | "https://domain.com/home",
42 | "/home"
43 | ])
44 | func parseHome_success(urlString: String) async throws {
45 | let url = try #require(URL(string: urlString))
46 | let deepLink = try #require(DeepLinkMock(url: url))
47 | #expect(deepLink == .home)
48 | }
49 |
50 | @Test("Invalid Home URLPatterns", arguments: [
51 | "https://domain.com/home/12",
52 | "https://home",
53 | "home"
54 | ])
55 | func parseHome_failure(urlString: String) async throws {
56 | let url = try #require(URL(string: urlString))
57 | let deepLink = DeepLinkMock(url: url)
58 | #expect(deepLink == nil)
59 | }
60 |
61 | // MARK: - Post Tests
62 | @Test("Valid Post URLPatterns", arguments: [
63 | "https://domain.com/posts/1",
64 | "/posts/1"
65 | ])
66 | func parsePost_success(urlString: String) async throws {
67 | let url = try #require(URL(string: urlString))
68 | let deepLink = try #require(DeepLinkMock(url: url))
69 | #expect(deepLink == .post(postId: "1"))
70 | }
71 |
72 | @Test("Invalid Post URLPatterns", arguments: [
73 | "https://domain.com/posts/1/test",
74 | "https://domain.com/post/1",
75 | "/post/1"
76 | ])
77 | func parsePost_failure(urlString: String) async throws {
78 | let url = try #require(URL(string: urlString))
79 | let deepLink = DeepLinkMock(url: url)
80 | #expect(deepLink == nil)
81 | }
82 |
83 | // MARK: - Post Comment Tests
84 | @Test("Valid PostComment URLPatterns", arguments: [
85 | "https://domain.com/posts/1/comments/2",
86 | "/posts/1/comments/2"
87 | ])
88 | func parsePostComment_success(urlString: String) async throws {
89 | let url = try #require(URL(string: urlString))
90 | let deepLink = try #require(DeepLinkMock(url: url))
91 | #expect(deepLink == .postComment(postId: "1", commentId: "2"))
92 | }
93 |
94 | @Test("Invalid PostComment URLPatterns", arguments: [
95 | "https://domain.com/posts/1/comment/2",
96 | "/posts/1/comments",
97 | "/posts/comments/2"
98 | ])
99 | func parsePostComment_failure(urlString: String) async throws {
100 | let url = try #require(URL(string: urlString))
101 | let deepLink = DeepLinkMock(url: url)
102 | #expect(deepLink == nil)
103 | }
104 |
105 | // MARK: - Setting Tests
106 | @Test("Valid Setting URLPatterns", arguments: [
107 | "https://domain.com/setting/phone/42",
108 | "/setting/phone/42"
109 | ])
110 | func parseSetting_success(urlString: String) async throws {
111 | let url = try #require(URL(string: urlString))
112 | let deepLink = try #require(DeepLinkMock(url: url))
113 | #expect(deepLink == .setting(number: 42))
114 | }
115 |
116 | @Test("Invalid Setting URLPatterns", arguments: [
117 | "https://domain.com/setting/abc/42",
118 | "/setting/phone/12.34",
119 | "/setting/phone"
120 | ])
121 | func parseSetting_failure(urlString: String) async throws {
122 | let url = try #require(URL(string: urlString))
123 | let deepLink = DeepLinkMock(url: url)
124 | #expect(deepLink == nil)
125 | }
126 |
127 | // MARK: - Int Tests
128 | @Test("Valid Int URLPatterns", arguments: [
129 | "https://domain.com/int/-42",
130 | "/int/-42"
131 | ])
132 | func parseInt_success(urlString: String) async throws {
133 | let url = try #require(URL(string: urlString))
134 | let deepLink = try #require(DeepLinkMock(url: url))
135 | #expect(deepLink == .int(int: -42))
136 | }
137 |
138 | @Test("Invalid Int URLPatterns", arguments: [
139 | "https://domain.com/int/abc",
140 | "/int/12.34",
141 | "/int/"
142 | ])
143 | func parseInt_failure(urlString: String) async throws {
144 | let url = try #require(URL(string: urlString))
145 | let deepLink = DeepLinkMock(url: url)
146 | #expect(deepLink == nil)
147 | }
148 |
149 | // MARK: - Float Tests
150 | @Test("Valid Float URLPatterns", arguments: [
151 | "https://domain.com/float/42.5",
152 | "/float/42.5"
153 | ])
154 | func parseFloat_success(urlString: String) async throws {
155 | let url = try #require(URL(string: urlString))
156 | let deepLink = try #require(DeepLinkMock(url: url))
157 | #expect(deepLink == .float(float: 42.5))
158 | }
159 |
160 | @Test("Invalid Float URLPatterns", arguments: [
161 | "https://domain.com/float/abc",
162 | "/float/",
163 | "/float/."
164 | ])
165 | func parseFloat_failure(urlString: String) async throws {
166 | let url = try #require(URL(string: urlString))
167 | let deepLink = DeepLinkMock(url: url)
168 | #expect(deepLink == nil)
169 | }
170 |
171 | // MARK: - String Tests
172 | @Test("Valid String URLPatterns", arguments: [
173 | "https://domain.com/string/hello",
174 | "/string/hello"
175 | ])
176 | func parseString_success(urlString: String) async throws {
177 | let url = try #require(URL(string: urlString))
178 | let deepLink = try #require(DeepLinkMock(url: url))
179 | #expect(deepLink == .string(string: "hello"))
180 | }
181 |
182 | @Test("Invalid String URLPatterns", arguments: [
183 | "https://domain.com/string",
184 | "/string/",
185 | "/string/test/extra"
186 | ])
187 | func parseString_failure(urlString: String) async throws {
188 | let url = try #require(URL(string: urlString))
189 | let deepLink = DeepLinkMock(url: url)
190 | #expect(deepLink == nil)
191 | }
192 |
193 | // MARK: - Complex Tests
194 | @Test("Valid Complex URLPatterns", arguments: [
195 | "https://domain.com/c/1/b/2/a/3",
196 | "/c/1/b/2/a/3"
197 | ])
198 | func parseComplex_success(urlString: String) async throws {
199 | let url = try #require(URL(string: urlString))
200 | let deepLink = try #require(DeepLinkMock(url: url))
201 | #expect(deepLink == .complex(aNum: 3, bNum: 2, cNum: 1))
202 | }
203 |
204 | @Test("Invalid Complex URLPatterns", arguments: [
205 | "https://domain.com/c/1/b/2/a/abc",
206 | "/c/1/b/abc/a/3",
207 | "/c/abc/b/2/a/3",
208 | "/c/1/b/2/a",
209 | "/c/1/b/a/3",
210 | "/a/1/b/2/c/3"
211 | ])
212 | func parseComplex_failure(urlString: String) async throws {
213 | let url = try #require(URL(string: urlString))
214 | let deepLink = DeepLinkMock(url: url)
215 | #expect(deepLink == nil)
216 | }
217 |
218 | // MARK: - Unicode Tests
219 | @Test("Valid Unicodes", arguments: [
220 | "안녕하세요",
221 | "こんにちは",
222 | "☺️👍"
223 | ])
224 |
225 | func paresPost_success_with_unicode(value: String) async throws {
226 | let url = try #require(URL(string: "/posts/\(value)"))
227 | let deepLink = DeepLinkMock(url: url)
228 | #expect(deepLink == .post(postId: value))
229 | }
230 |
231 | // MARK: - Priority Tests
232 | @URLPattern
233 | enum PriorityTest: Equatable {
234 | @URLPath("/{a}/{b}")
235 | case all(a: String, b: String)
236 |
237 | @URLPath("/post/{postId}")
238 | case post(postId: Int)
239 | }
240 |
241 | @Test("Test Scope")
242 | func checkPriorityCases() async throws {
243 | let url = try #require(URL(string: "/post/1"))
244 | #expect(PriorityTest(url: url) == .all(a: "post", b: "1"))
245 | }
246 |
247 | // MARK: - Scope Tests
248 | @URLPattern
249 | enum ScopeTest {
250 | @URLPath("/")
251 | case zero
252 |
253 | @URLPath("/{a}")
254 | case one(a: String)
255 |
256 | @URLPath("/{a}/{b}")
257 | case two(a: String, b: String)
258 |
259 | @URLPath("/{a}/{b}/{c}")
260 | case three(a: String, b: String, c: String)
261 |
262 | @URLPath("/{a}/{b}/{c}/{d}")
263 | case four(a: String, b: String, c: String, d: String)
264 |
265 | @URLPath("/{a}/{b}/{c}/{d}/{e}")
266 | case five(a: String, b: String, c: String, d: String, e: String)
267 |
268 | @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}")
269 | case six(a: String, b: String, c: String, d: String, e: String, f: String)
270 |
271 | @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}")
272 | case seven(a: String, b: String, c: String, d: String, e: String, f: String, g: String)
273 |
274 | @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}")
275 | case eight(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String)
276 |
277 | @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}")
278 | case nine(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String, i: String)
279 |
280 | @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}")
281 | case ten(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String, i: String, j: String)
282 | }
283 |
284 | @Test("Test scope", arguments: Array(0...20))
285 | func checkScope(num: Int) async throws {
286 | let url = try #require(URL(string: "/" + (0.. 10 {
289 | #expect(ScopeTest(url: url) == nil)
290 | } else {
291 | #expect(ScopeTest(url: url) != nil)
292 | }
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | EF43E6482D9ED87500809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E6472D9ED87500809F76 /* URLPattern */; };
11 | EF43E64B2D9ED8A800809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64A2D9ED8A800809F76 /* URLPattern */; };
12 | EF43E64D2D9ED8A800809F76 /* URLPatternClient in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64C2D9ED8A800809F76 /* URLPatternClient */; };
13 | EF43E6502D9ED93E00809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64F2D9ED93E00809F76 /* URLPattern */; };
14 | EF43E72D2D9FE2C300809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E72C2D9FE2C300809F76 /* URLPattern */; };
15 | EF43E7302D9FE2CC00809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E72F2D9FE2CC00809F76 /* URLPattern */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXContainerItemProxy section */
19 | EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */ = {
20 | isa = PBXContainerItemProxy;
21 | containerPortal = EF43E6112D9ED86000809F76 /* Project object */;
22 | proxyType = 1;
23 | remoteGlobalIDString = EF43E6182D9ED86000809F76;
24 | remoteInfo = URLPatternExample;
25 | };
26 | /* End PBXContainerItemProxy section */
27 |
28 | /* Begin PBXFileReference section */
29 | EF43E6192D9ED86000809F76 /* URLPatternExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = URLPatternExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
30 | EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLPatternExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
31 | /* End PBXFileReference section */
32 |
33 | /* Begin PBXFileSystemSynchronizedRootGroup section */
34 | EF43E61B2D9ED86000809F76 /* URLPatternExample */ = {
35 | isa = PBXFileSystemSynchronizedRootGroup;
36 | path = URLPatternExample;
37 | sourceTree = "";
38 | };
39 | EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */ = {
40 | isa = PBXFileSystemSynchronizedRootGroup;
41 | path = URLPatternExampleTests;
42 | sourceTree = "";
43 | };
44 | /* End PBXFileSystemSynchronizedRootGroup section */
45 |
46 | /* Begin PBXFrameworksBuildPhase section */
47 | EF43E6162D9ED86000809F76 /* Frameworks */ = {
48 | isa = PBXFrameworksBuildPhase;
49 | buildActionMask = 2147483647;
50 | files = (
51 | EF43E72D2D9FE2C300809F76 /* URLPattern in Frameworks */,
52 | EF43E6482D9ED87500809F76 /* URLPattern in Frameworks */,
53 | EF43E64D2D9ED8A800809F76 /* URLPatternClient in Frameworks */,
54 | EF43E64B2D9ED8A800809F76 /* URLPattern in Frameworks */,
55 | EF43E6502D9ED93E00809F76 /* URLPattern in Frameworks */,
56 | );
57 | runOnlyForDeploymentPostprocessing = 0;
58 | };
59 | EF43E6262D9ED86100809F76 /* Frameworks */ = {
60 | isa = PBXFrameworksBuildPhase;
61 | buildActionMask = 2147483647;
62 | files = (
63 | EF43E7302D9FE2CC00809F76 /* URLPattern in Frameworks */,
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXFrameworksBuildPhase section */
68 |
69 | /* Begin PBXGroup section */
70 | EF43E6102D9ED86000809F76 = {
71 | isa = PBXGroup;
72 | children = (
73 | EF43E61B2D9ED86000809F76 /* URLPatternExample */,
74 | EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */,
75 | EF43E72E2D9FE2CC00809F76 /* Frameworks */,
76 | EF43E61A2D9ED86000809F76 /* Products */,
77 | );
78 | sourceTree = "";
79 | };
80 | EF43E61A2D9ED86000809F76 /* Products */ = {
81 | isa = PBXGroup;
82 | children = (
83 | EF43E6192D9ED86000809F76 /* URLPatternExample.app */,
84 | EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */,
85 | );
86 | name = Products;
87 | sourceTree = "";
88 | };
89 | EF43E72E2D9FE2CC00809F76 /* Frameworks */ = {
90 | isa = PBXGroup;
91 | children = (
92 | );
93 | name = Frameworks;
94 | sourceTree = "";
95 | };
96 | /* End PBXGroup section */
97 |
98 | /* Begin PBXNativeTarget section */
99 | EF43E6182D9ED86000809F76 /* URLPatternExample */ = {
100 | isa = PBXNativeTarget;
101 | buildConfigurationList = EF43E63D2D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExample" */;
102 | buildPhases = (
103 | EF43E6152D9ED86000809F76 /* Sources */,
104 | EF43E6162D9ED86000809F76 /* Frameworks */,
105 | EF43E6172D9ED86000809F76 /* Resources */,
106 | );
107 | buildRules = (
108 | );
109 | dependencies = (
110 | );
111 | fileSystemSynchronizedGroups = (
112 | EF43E61B2D9ED86000809F76 /* URLPatternExample */,
113 | );
114 | name = URLPatternExample;
115 | packageProductDependencies = (
116 | EF43E6472D9ED87500809F76 /* URLPattern */,
117 | EF43E64A2D9ED8A800809F76 /* URLPattern */,
118 | EF43E64C2D9ED8A800809F76 /* URLPatternClient */,
119 | EF43E64F2D9ED93E00809F76 /* URLPattern */,
120 | EF43E72C2D9FE2C300809F76 /* URLPattern */,
121 | );
122 | productName = URLPatternExample;
123 | productReference = EF43E6192D9ED86000809F76 /* URLPatternExample.app */;
124 | productType = "com.apple.product-type.application";
125 | };
126 | EF43E6282D9ED86100809F76 /* URLPatternExampleTests */ = {
127 | isa = PBXNativeTarget;
128 | buildConfigurationList = EF43E6402D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleTests" */;
129 | buildPhases = (
130 | EF43E6252D9ED86100809F76 /* Sources */,
131 | EF43E6262D9ED86100809F76 /* Frameworks */,
132 | EF43E6272D9ED86100809F76 /* Resources */,
133 | );
134 | buildRules = (
135 | );
136 | dependencies = (
137 | EF43E62B2D9ED86100809F76 /* PBXTargetDependency */,
138 | );
139 | fileSystemSynchronizedGroups = (
140 | EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */,
141 | );
142 | name = URLPatternExampleTests;
143 | packageProductDependencies = (
144 | EF43E72F2D9FE2CC00809F76 /* URLPattern */,
145 | );
146 | productName = URLPatternExampleTests;
147 | productReference = EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */;
148 | productType = "com.apple.product-type.bundle.unit-test";
149 | };
150 | /* End PBXNativeTarget section */
151 |
152 | /* Begin PBXProject section */
153 | EF43E6112D9ED86000809F76 /* Project object */ = {
154 | isa = PBXProject;
155 | attributes = {
156 | BuildIndependentTargetsInParallel = 1;
157 | LastSwiftUpdateCheck = 1620;
158 | LastUpgradeCheck = 1620;
159 | TargetAttributes = {
160 | EF43E6182D9ED86000809F76 = {
161 | CreatedOnToolsVersion = 16.2;
162 | };
163 | EF43E6282D9ED86100809F76 = {
164 | CreatedOnToolsVersion = 16.2;
165 | };
166 | };
167 | };
168 | buildConfigurationList = EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */;
169 | developmentRegion = en;
170 | hasScannedForEncodings = 0;
171 | knownRegions = (
172 | en,
173 | Base,
174 | );
175 | mainGroup = EF43E6102D9ED86000809F76;
176 | minimizedProjectReferenceProxies = 1;
177 | packageReferences = (
178 | EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */,
179 | );
180 | preferredProjectObjectVersion = 77;
181 | productRefGroup = EF43E61A2D9ED86000809F76 /* Products */;
182 | projectDirPath = "";
183 | projectRoot = "";
184 | targets = (
185 | EF43E6182D9ED86000809F76 /* URLPatternExample */,
186 | EF43E6282D9ED86100809F76 /* URLPatternExampleTests */,
187 | );
188 | };
189 | /* End PBXProject section */
190 |
191 | /* Begin PBXResourcesBuildPhase section */
192 | EF43E6172D9ED86000809F76 /* Resources */ = {
193 | isa = PBXResourcesBuildPhase;
194 | buildActionMask = 2147483647;
195 | files = (
196 | );
197 | runOnlyForDeploymentPostprocessing = 0;
198 | };
199 | EF43E6272D9ED86100809F76 /* Resources */ = {
200 | isa = PBXResourcesBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | );
204 | runOnlyForDeploymentPostprocessing = 0;
205 | };
206 | /* End PBXResourcesBuildPhase section */
207 |
208 | /* Begin PBXSourcesBuildPhase section */
209 | EF43E6152D9ED86000809F76 /* Sources */ = {
210 | isa = PBXSourcesBuildPhase;
211 | buildActionMask = 2147483647;
212 | files = (
213 | );
214 | runOnlyForDeploymentPostprocessing = 0;
215 | };
216 | EF43E6252D9ED86100809F76 /* Sources */ = {
217 | isa = PBXSourcesBuildPhase;
218 | buildActionMask = 2147483647;
219 | files = (
220 | );
221 | runOnlyForDeploymentPostprocessing = 0;
222 | };
223 | /* End PBXSourcesBuildPhase section */
224 |
225 | /* Begin PBXTargetDependency section */
226 | EF43E62B2D9ED86100809F76 /* PBXTargetDependency */ = {
227 | isa = PBXTargetDependency;
228 | target = EF43E6182D9ED86000809F76 /* URLPatternExample */;
229 | targetProxy = EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */;
230 | };
231 | /* End PBXTargetDependency section */
232 |
233 | /* Begin XCBuildConfiguration section */
234 | EF43E63B2D9ED86100809F76 /* Debug */ = {
235 | isa = XCBuildConfiguration;
236 | buildSettings = {
237 | ALWAYS_SEARCH_USER_PATHS = NO;
238 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
242 | CLANG_ENABLE_MODULES = YES;
243 | CLANG_ENABLE_OBJC_ARC = YES;
244 | CLANG_ENABLE_OBJC_WEAK = YES;
245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
246 | CLANG_WARN_BOOL_CONVERSION = YES;
247 | CLANG_WARN_COMMA = YES;
248 | CLANG_WARN_CONSTANT_CONVERSION = YES;
249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
252 | CLANG_WARN_EMPTY_BODY = YES;
253 | CLANG_WARN_ENUM_CONVERSION = YES;
254 | CLANG_WARN_INFINITE_RECURSION = YES;
255 | CLANG_WARN_INT_CONVERSION = YES;
256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | COPY_PHASE_STRIP = NO;
268 | DEBUG_INFORMATION_FORMAT = dwarf;
269 | ENABLE_STRICT_OBJC_MSGSEND = YES;
270 | ENABLE_TESTABILITY = YES;
271 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
272 | GCC_C_LANGUAGE_STANDARD = gnu17;
273 | GCC_DYNAMIC_NO_PIC = NO;
274 | GCC_NO_COMMON_BLOCKS = YES;
275 | GCC_OPTIMIZATION_LEVEL = 0;
276 | GCC_PREPROCESSOR_DEFINITIONS = (
277 | "DEBUG=1",
278 | "$(inherited)",
279 | );
280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
282 | GCC_WARN_UNDECLARED_SELECTOR = YES;
283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
284 | GCC_WARN_UNUSED_FUNCTION = YES;
285 | GCC_WARN_UNUSED_VARIABLE = YES;
286 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
287 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
289 | MTL_FAST_MATH = YES;
290 | ONLY_ACTIVE_ARCH = YES;
291 | SDKROOT = iphoneos;
292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
294 | };
295 | name = Debug;
296 | };
297 | EF43E63C2D9ED86100809F76 /* Release */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ALWAYS_SEARCH_USER_PATHS = NO;
301 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
302 | CLANG_ANALYZER_NONNULL = YES;
303 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
305 | CLANG_ENABLE_MODULES = YES;
306 | CLANG_ENABLE_OBJC_ARC = YES;
307 | CLANG_ENABLE_OBJC_WEAK = YES;
308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
309 | CLANG_WARN_BOOL_CONVERSION = YES;
310 | CLANG_WARN_COMMA = YES;
311 | CLANG_WARN_CONSTANT_CONVERSION = YES;
312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
315 | CLANG_WARN_EMPTY_BODY = YES;
316 | CLANG_WARN_ENUM_CONVERSION = YES;
317 | CLANG_WARN_INFINITE_RECURSION = YES;
318 | CLANG_WARN_INT_CONVERSION = YES;
319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
323 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
325 | CLANG_WARN_STRICT_PROTOTYPES = YES;
326 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
328 | CLANG_WARN_UNREACHABLE_CODE = YES;
329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
330 | COPY_PHASE_STRIP = NO;
331 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
332 | ENABLE_NS_ASSERTIONS = NO;
333 | ENABLE_STRICT_OBJC_MSGSEND = YES;
334 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
335 | GCC_C_LANGUAGE_STANDARD = gnu17;
336 | GCC_NO_COMMON_BLOCKS = YES;
337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
339 | GCC_WARN_UNDECLARED_SELECTOR = YES;
340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
341 | GCC_WARN_UNUSED_FUNCTION = YES;
342 | GCC_WARN_UNUSED_VARIABLE = YES;
343 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
344 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
345 | MTL_ENABLE_DEBUG_INFO = NO;
346 | MTL_FAST_MATH = YES;
347 | SDKROOT = iphoneos;
348 | SWIFT_COMPILATION_MODE = wholemodule;
349 | VALIDATE_PRODUCT = YES;
350 | };
351 | name = Release;
352 | };
353 | EF43E63E2D9ED86100809F76 /* Debug */ = {
354 | isa = XCBuildConfiguration;
355 | buildSettings = {
356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
357 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
358 | CODE_SIGN_STYLE = Automatic;
359 | CURRENT_PROJECT_VERSION = 1;
360 | DEVELOPMENT_ASSET_PATHS = "\"URLPatternExample/Preview Content\"";
361 | DEVELOPMENT_TEAM = HK54HM2TC6;
362 | ENABLE_PREVIEWS = YES;
363 | GENERATE_INFOPLIST_FILE = YES;
364 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
365 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
366 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
367 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
369 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
370 | LD_RUNPATH_SEARCH_PATHS = (
371 | "$(inherited)",
372 | "@executable_path/Frameworks",
373 | );
374 | MARKETING_VERSION = 1.0;
375 | PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExample;
376 | PRODUCT_NAME = "$(TARGET_NAME)";
377 | SWIFT_EMIT_LOC_STRINGS = YES;
378 | SWIFT_VERSION = 5.0;
379 | TARGETED_DEVICE_FAMILY = "1,2";
380 | };
381 | name = Debug;
382 | };
383 | EF43E63F2D9ED86100809F76 /* Release */ = {
384 | isa = XCBuildConfiguration;
385 | buildSettings = {
386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
387 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
388 | CODE_SIGN_STYLE = Automatic;
389 | CURRENT_PROJECT_VERSION = 1;
390 | DEVELOPMENT_ASSET_PATHS = "\"URLPatternExample/Preview Content\"";
391 | DEVELOPMENT_TEAM = HK54HM2TC6;
392 | ENABLE_PREVIEWS = YES;
393 | GENERATE_INFOPLIST_FILE = YES;
394 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
395 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
396 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
397 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
398 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
399 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
400 | LD_RUNPATH_SEARCH_PATHS = (
401 | "$(inherited)",
402 | "@executable_path/Frameworks",
403 | );
404 | MARKETING_VERSION = 1.0;
405 | PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExample;
406 | PRODUCT_NAME = "$(TARGET_NAME)";
407 | SWIFT_EMIT_LOC_STRINGS = YES;
408 | SWIFT_VERSION = 5.0;
409 | TARGETED_DEVICE_FAMILY = "1,2";
410 | };
411 | name = Release;
412 | };
413 | EF43E6412D9ED86100809F76 /* Debug */ = {
414 | isa = XCBuildConfiguration;
415 | buildSettings = {
416 | CODE_SIGN_STYLE = Automatic;
417 | CURRENT_PROJECT_VERSION = 1;
418 | DEVELOPMENT_TEAM = HK54HM2TC6;
419 | GENERATE_INFOPLIST_FILE = YES;
420 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
421 | MARKETING_VERSION = 1.0;
422 | PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests;
423 | PRODUCT_NAME = "$(TARGET_NAME)";
424 | SWIFT_EMIT_LOC_STRINGS = NO;
425 | SWIFT_VERSION = 5.0;
426 | TARGETED_DEVICE_FAMILY = "1,2";
427 | };
428 | name = Debug;
429 | };
430 | EF43E6422D9ED86100809F76 /* Release */ = {
431 | isa = XCBuildConfiguration;
432 | buildSettings = {
433 | CODE_SIGN_STYLE = Automatic;
434 | CURRENT_PROJECT_VERSION = 1;
435 | DEVELOPMENT_TEAM = HK54HM2TC6;
436 | GENERATE_INFOPLIST_FILE = YES;
437 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
438 | MARKETING_VERSION = 1.0;
439 | PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests;
440 | PRODUCT_NAME = "$(TARGET_NAME)";
441 | SWIFT_EMIT_LOC_STRINGS = NO;
442 | SWIFT_VERSION = 5.0;
443 | TARGETED_DEVICE_FAMILY = "1,2";
444 | };
445 | name = Release;
446 | };
447 | /* End XCBuildConfiguration section */
448 |
449 | /* Begin XCConfigurationList section */
450 | EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */ = {
451 | isa = XCConfigurationList;
452 | buildConfigurations = (
453 | EF43E63B2D9ED86100809F76 /* Debug */,
454 | EF43E63C2D9ED86100809F76 /* Release */,
455 | );
456 | defaultConfigurationIsVisible = 0;
457 | defaultConfigurationName = Release;
458 | };
459 | EF43E63D2D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExample" */ = {
460 | isa = XCConfigurationList;
461 | buildConfigurations = (
462 | EF43E63E2D9ED86100809F76 /* Debug */,
463 | EF43E63F2D9ED86100809F76 /* Release */,
464 | );
465 | defaultConfigurationIsVisible = 0;
466 | defaultConfigurationName = Release;
467 | };
468 | EF43E6402D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleTests" */ = {
469 | isa = XCConfigurationList;
470 | buildConfigurations = (
471 | EF43E6412D9ED86100809F76 /* Debug */,
472 | EF43E6422D9ED86100809F76 /* Release */,
473 | );
474 | defaultConfigurationIsVisible = 0;
475 | defaultConfigurationName = Release;
476 | };
477 | /* End XCConfigurationList section */
478 |
479 | /* Begin XCLocalSwiftPackageReference section */
480 | EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */ = {
481 | isa = XCLocalSwiftPackageReference;
482 | relativePath = ../../URLPattern;
483 | };
484 | /* End XCLocalSwiftPackageReference section */
485 |
486 | /* Begin XCSwiftPackageProductDependency section */
487 | EF43E6472D9ED87500809F76 /* URLPattern */ = {
488 | isa = XCSwiftPackageProductDependency;
489 | productName = URLPattern;
490 | };
491 | EF43E64A2D9ED8A800809F76 /* URLPattern */ = {
492 | isa = XCSwiftPackageProductDependency;
493 | productName = URLPattern;
494 | };
495 | EF43E64C2D9ED8A800809F76 /* URLPatternClient */ = {
496 | isa = XCSwiftPackageProductDependency;
497 | productName = URLPatternClient;
498 | };
499 | EF43E64F2D9ED93E00809F76 /* URLPattern */ = {
500 | isa = XCSwiftPackageProductDependency;
501 | productName = URLPattern;
502 | };
503 | EF43E72C2D9FE2C300809F76 /* URLPattern */ = {
504 | isa = XCSwiftPackageProductDependency;
505 | productName = URLPattern;
506 | };
507 | EF43E72F2D9FE2CC00809F76 /* URLPattern */ = {
508 | isa = XCSwiftPackageProductDependency;
509 | package = EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */;
510 | productName = URLPattern;
511 | };
512 | /* End XCSwiftPackageProductDependency section */
513 | };
514 | rootObject = EF43E6112D9ED86000809F76 /* Project object */;
515 | }
516 |
--------------------------------------------------------------------------------