diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 7c4452a..87e5f44 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -16,6 +16,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set Xcode Version 16.1.0 + run: sudo xcode-select -s /Applications/Xcode_16.1.app + - name: Test URLPattern run: swift test -v diff --git a/Package.swift b/Package.swift index a0d67c1..dda25c3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,51 +1,41 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription import CompilerPluginSupport let package = Package( - name: "URLPattern", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "URLPattern", - targets: ["URLPattern"] - ), - .executable( - name: "URLPatternClient", - targets: ["URLPatternClient"] - ), - ], - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - // Macro implementation that performs the source transformation of a macro. - .macro( - name: "URLPatternMacros", - dependencies: [ - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") - ] - ), - - // Library that exposes a macro as part of its API, which is used in client programs. - .target(name: "URLPattern", dependencies: ["URLPatternMacros"]), - - // A client of the library, which is able to use the macro in its own code. - .executableTarget(name: "URLPatternClient", dependencies: ["URLPattern"]), - - // A test target used to develop the macro implementation. - .testTarget( - name: "URLPatternTests", - dependencies: [ - "URLPatternMacros", - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), - ] - ), - ] + name: "URLPattern", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], + products: [ + .library( + name: "URLPattern", + targets: ["URLPattern"] + ), + .executable( + name: "URLPatternClient", + targets: ["URLPatternClient"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), + ], + targets: [ + .macro( + name: "URLPatternMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + .target(name: "URLPattern", dependencies: ["URLPatternMacros"]), + .executableTarget(name: "URLPatternClient", dependencies: ["URLPattern"]), + .testTarget( + name: "URLPatternTests", + dependencies: [ + "URLPatternMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] ) diff --git a/README.md b/README.md index 5a73d0d..f4eb566 100644 --- a/README.md +++ b/README.md @@ -1 +1,104 @@ -# URLPatternMacro \ No newline at end of file +# URLPattern +[![Swift](https://github.com/heoblitz/URLPattern/actions/workflows/swift.yml/badge.svg?branch=main)](https://github.com/heoblitz/URLPattern/actions/workflows/swift.yml) + +A Swift Macro that helps mapping URLs to Enum cases. + +## Overview + +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: + +```swift +// Traditional approach with manual URL handling +let paths = url.pathComponents + +if paths.count == 2 && paths[1] == "home" { + // Handle home +} else let match = try? url.path.firstMatch(of: /\/posts\/([^\/]+)$/) { + // Handle posts +} +``` + +This approach reduces code readability and scalability, and importantly, cannot validate incorrect patterns at compile-time. + +URLPattern solves these issues by providing compile-time URL validation and value mapping: + +```swift +@URLPattern +enum DeepLink { + @URLPath("/home") + case home + + @URLPath("/posts/{postId}") + case post(postId: String) + + @URLPath("/posts/{postId}/comments/{commentId}") + case postComment(postId: String, commentId: String) +} +``` + +## Features + +- **Compile-time Validation**: Ensures URL path values and associated value names match correctly +- **Automatic Enum Generation**: Creates initializers that map URL components to enum associated values +- **Type Support**: + - Built-in support for `String`, `Int`, `Float`, and `Double` + - Non-String types (Int, Float, Double) use String-based initialization + +## Usage + +```swift +@URLPattern +enum DeepLink { + @URLPath("/posts/{postId}") + case post(postId: String) + + @URLPath("/posts/{postId}/comments/{commentId}") + case postComment(postId: String, commentId: String) + + @URLPath("/f/{first}/s/{second}") + case reverse(second: Int, first: Int) +} +``` + +1. Declare the `@URLPattern` macro on your enum. + +2. Add `@URLPath` macro to enum cases with the desired URL pattern. + +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. + + +```swift +// ✅ Valid URLs +DeepLink(url: URL(string: "/posts/1")!) == .post(postId: "1") +DeepLink(url: URL(string: "/posts/1/comments/2")!) == .postComment(postId: "1", commentId: "2") +DeepLink(url: URL(string: "/f/1/s/2")!) == .postComment(second: 2, first: 1) + +// ❌ Invalid URLs +DeepLink(url: URL(string: "/post/1")) == nil +DeepLink(url: URL(string: "/posts/1/comments")) == nil +DeepLink(url: URL(string: "/f/string/s/string")!) == nil +``` +4. Use the `Enum.init(url: URL)` generated initializer. +``` +if let deepLink = DeepLink(url: incomingURL) { + switch deepLink { + case .post(let postId): + // Handle post + case .postComment(let postId, let commentId): + // Handle postComment + } +} +``` +5. Implement a deep link using an enum switch statement. +- For more detailed examples, please refer to the [Example project](https://github.com/heoblitz/URLPattern/tree/feature/main/URLPatternExample). + +## Rules + +- **Unique Enum Case Names**: Enum case names must be unique for better readability of expanded macro code. +- **Unique Associated Value Names**: Associated value names within each case must be unique. +- **Valid URL Patterns**: Arguments passed to @URLPath macro must be in valid URL path format. +- **Supported Types**: Only String, Int, Float, and Double are supported. + +## Installation +### Swift Package Manager +Project > Project Dependencies > Add   `https://github.com/heoblitz/URLPattern.git` diff --git a/Sources/URLPattern/URLPattern.swift b/Sources/URLPattern/URLPattern.swift index 27678e5..59652fc 100644 --- a/Sources/URLPattern/URLPattern.swift +++ b/Sources/URLPattern/URLPattern.swift @@ -1,3 +1,5 @@ +@_exported import Foundation + @attached(member, names: arbitrary) public macro URLPattern() = #externalMacro(module: "URLPatternMacros", type: "URLPatternMacro") diff --git a/Sources/URLPatternClient/main.swift b/Sources/URLPatternClient/main.swift index 2e25082..efba80d 100644 --- a/Sources/URLPatternClient/main.swift +++ b/Sources/URLPatternClient/main.swift @@ -1,35 +1 @@ import URLPattern -import Foundation - -@URLPattern -enum Deeplink { -// @URLPath("/post/{id}") -// case post(id: String) -// - @URLPath("/home/{id}/{name}") - case name(id: String, name: String) - - @URLPath("/post/{id}/{name}/hi/{good}") - case nameDetail(id: String, name: String, good: String) - - @URLPath("/post/{id}") - case nameDetailHI(id: String) -} - -let url1 = URL(string: "https://channel.io/post/12/12") -let url2 = URL(string: "/post/hi/hello/hi/bye") - -// enumPath -// inputPath - - -print(url1?.pathComponents) -print(url2?.pathComponents) - - -let hi = URL(string: "https://post/{id}") -let paths = url1!.pathComponents - -print(Deeplink(url: url1!)) -print(Deeplink(url: url2!)) - diff --git a/Sources/URLPatternMacros/Extensions/String+Extensions.swift b/Sources/URLPatternMacros/Extensions/String+Extensions.swift index 8b558f7..08d41ea 100644 --- a/Sources/URLPatternMacros/Extensions/String+Extensions.swift +++ b/Sources/URLPatternMacros/Extensions/String+Extensions.swift @@ -1,5 +1,5 @@ import Foundation extension String { - var isURLPathParam: Bool { self.hasPrefix("{") && self.hasSuffix("}") } + var isURLPathValue: Bool { self.hasPrefix("{") && self.hasSuffix("}") && self.utf16.count >= 3 } } diff --git a/Sources/URLPatternMacros/URLPathMacro.swift b/Sources/URLPatternMacros/URLPathMacro.swift index f4733dd..7201a1c 100644 --- a/Sources/URLPatternMacros/URLPathMacro.swift +++ b/Sources/URLPatternMacros/URLPathMacro.swift @@ -5,13 +5,18 @@ import SwiftSyntaxMacros import Foundation public struct URLPathMacro: PeerMacro { - struct CaseParam: Hashable { - let index: Int - let name: String + enum SupportedType: String { + case String + case Int + case Double + case Float } - struct PatternPathItem { - + struct PatternParam: Hashable { + let name: String + let type: SupportedType + let pathIndex: Int + let caseIndex: Int } public static func expansion( @@ -23,48 +28,92 @@ public struct URLPathMacro: PeerMacro { let enumCase = declaration.as(EnumCaseDeclSyntax.self), let element = enumCase.elements.first else { - throw MacroError.message("URLPatternPath macro can only be applied to enum cases") + throw URLPatternError("@URLPathMacro can only be applied to enum cases") } - + guard let argument = node.arguments?.as(LabeledExprListSyntax.self)?.first, let pathString = argument.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description else { - throw MacroError.message("Invalid path") + throw URLPatternError("URLPath is nil") } guard let pathURL = URL(string: pathString) else { - throw MacroError.message("URLPatternPath macro requires a string literal path") + throw URLPatternError("URLPath is not in a valid URL format") } let patternPaths = pathURL.pathComponents + + let caseAssociatedTypes = try element.parameterClause?.parameters.map { param -> (name: String, type: SupportedType) in + let name = param.firstName?.text ?? "" + let type = param.type.description + + guard let supportedType = SupportedType(rawValue: type) else { + throw URLPatternError("\(type) is not supported as an associated value") + } + return (name: name, type: supportedType) + } ?? [] + + let patternParams: [PatternParam] = try patternPaths.enumerated() + .filter { index, value in value.isURLPathValue } + .map { pathIndex, value -> PatternParam in + let name = String(value.dropFirst().dropLast()) + + guard let (caseIndex, caseAssociatedType) = caseAssociatedTypes.enumerated().first(where: { name == $0.element.name }) else { + throw URLPatternError("URLPath value \"\(name)\" cannot be found in the associated value") + } + + return PatternParam( + name: name, + type: caseAssociatedType.type, + pathIndex: pathIndex, + caseIndex: caseIndex + ) + } + .sorted(by: { $0.caseIndex < $1.caseIndex }) + + let patternNames = Set(patternParams.map(\.name)) + let caseNames = Set(caseAssociatedTypes.map(\.name)) - let pathComponents = pathURL.pathComponents - let parameters = pathComponents.enumerated() - .filter { index, value in value.isURLPathParam } - .map { CaseParam(index: $0.offset, name: String($0.element.dropFirst().dropLast())) } + guard patternNames.count == patternParams.count else { + throw URLPatternError("The name of an URLPath value cannot be duplicated") + } - if Set(parameters).count != parameters.count { - throw MacroError.message("변수 이름은 중복되서는 안됩니다.") + guard caseNames.count == caseAssociatedTypes.count else { + throw URLPatternError("The name of an associated value cannot be duplicated") } - + + guard patternNames.count == caseNames.count else { + throw URLPatternError("The number of associated values does not match URLPath") + } + + guard patternNames == caseNames else { + throw URLPatternError("The name of the URLPath value does not match the associated value") + } + let staticMethod = try FunctionDeclSyntax(""" static func \(element.name)(_ url: URL) -> Self? { let inputPaths = url.pathComponents let patternPaths = \(raw: patternPaths) - + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil } - - \(raw: parameters.map { param in - """ - let \(param.name) = inputPaths[\(param.index)] - """ + \(raw: patternParams.map { param in + switch param.type { + case .Double, .Float, .Int: + """ + guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { return nil } + """ + case .String: + """ + let \(param.name) = inputPaths[\(param.pathIndex)] + """ + } }.joined(separator: "\n")) - - return .\(raw: element.name.text)(\(raw: parameters.map { "\($0.name): \($0.name)" }.joined(separator: ", "))) + return \(raw: patternParams.isEmpty + ? ".\(element.name.text)" + : ".\(element.name.text)(\(patternParams.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))") } - """ - ) + """) return [DeclSyntax(staticMethod)] } diff --git a/Sources/URLPatternMacros/URLPatternMacro.swift b/Sources/URLPatternMacros/URLPatternMacro.swift index 98cd422..70a4690 100644 --- a/Sources/URLPatternMacros/URLPatternMacro.swift +++ b/Sources/URLPatternMacros/URLPatternMacro.swift @@ -4,10 +4,6 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros import Foundation -enum MacroError: Error { - case message(String) -} - public struct URLPatternMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, @@ -15,19 +11,34 @@ public struct URLPatternMacro: MemberMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { - throw MacroError.message("This macro can only be applied to enums") + throw URLPatternError("@URLPatternMacro can only be applied to enums") + } + + let cases = enumDecl.memberBlock.members.compactMap { member -> String? in + guard + let caseDecl = member.decl.as(EnumCaseDeclSyntax.self), + caseDecl.attributes.contains(where: { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "URLPath" + }) + else { + return nil + } + + return caseDecl.elements.first?.name.text + } + + guard Set(cases).count == cases.count else { + throw URLPatternError("Duplicate case names are not allowed") } let urlInitializer = try InitializerDeclSyntax("init?(url: URL)") { - for caseDecl in enumDecl.memberBlock.members.compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) }) { - if let caseName = caseDecl.elements.first?.name.text { - """ - if let result = Self.\(raw: caseName)(url) { - self = result - return - } - """ + for caseName in cases { + """ + if let urlPattern = Self.\(raw: caseName)(url) { + self = urlPattern + return } + """ } """ @@ -37,22 +48,26 @@ public struct URLPatternMacro: MemberMacro { let isValidURLPathsMethod = try FunctionDeclSyntax(""" static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool { - guard inputs.count == patterns.count else { return false } + guard inputs.count == patterns.count else { return false } - return zip(inputs, patterns).allSatisfy { input, pattern in - guard pattern.isURLPathParam else { return input == pattern } - - return true - } + return zip(inputs, patterns).allSatisfy { input, pattern in + guard Self.isURLPathValue(pattern) else { return input == pattern } + + return true + } } """) - let isURLPathParamMethod = try FunctionDeclSyntax(""" - static func isURLPathParam(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") } + let isURLPathValueMethod = try FunctionDeclSyntax(""" + static func isURLPathValue(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 } """) - return [DeclSyntax(urlInitializer), DeclSyntax(isValidURLPathsMethod), DeclSyntax(isURLPathParamMethod)] + return [ + DeclSyntax(urlInitializer), + DeclSyntax(isValidURLPathsMethod), + DeclSyntax(isURLPathValueMethod) + ] } } diff --git a/Sources/URLPatternMacros/Utils/URLPatternError.swift b/Sources/URLPatternMacros/Utils/URLPatternError.swift new file mode 100644 index 0000000..9916e4e --- /dev/null +++ b/Sources/URLPatternMacros/Utils/URLPatternError.swift @@ -0,0 +1,13 @@ +import Foundation + +struct URLPatternError: LocalizedError { + let errorDescription: String + + init(_ errorDescription: String) { + self.errorDescription = errorDescription + } +} + +extension URLPatternError: CustomStringConvertible { + var description: String { self.errorDescription } +} diff --git a/Tests/URLPatternTests/URLPatternTests.swift b/Tests/URLPatternTests/URLPatternTests.swift index 760e277..d3be924 100644 --- a/Tests/URLPatternTests/URLPatternTests.swift +++ b/Tests/URLPatternTests/URLPatternTests.swift @@ -4,88 +4,196 @@ import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest -// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. #if canImport(URLPatternMacros) import URLPatternMacros let testMacros: [String: Macro.Type] = [ - "URLPattern": URLPatternMacro.self, - "URLPath": URLPathMacro.self + "URLPattern": URLPatternMacro.self, + "URLPath": URLPathMacro.self ] #endif - - final class URLPatternTests: XCTestCase { - func testMacro() throws { - assertMacroExpansion( - """ - @URLPattern - enum Deeplink { - @URLPath("/post/{id}") - case post(id: String) - - @URLPath("/post/{id}/{name}") - case name(id: String, name: String) + func testDeepLinkMacro_normalCase() throws { + assertMacroExpansion( + """ + @URLPattern + enum DeepLink: Equatable { + @URLPath("/home") + case home + + @URLPath("/posts/{postId}") + case post(postId: String) + + @URLPath("/posts/{postId}/comments/{commentId}") + case postComment(postId: String, commentId: String) + + @URLPath("/c/{cNum}/b/{bNum}/a/{aNum}") + case complex(aNum: Int, bNum: Int, cNum: Int) + } + """, + expandedSource: """ + enum DeepLink: Equatable { + case home + + static func home(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "home"] + + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil + } + + return .home } - """, - expandedSource: """ - enum Deeplink { - case post(id: String) - static func createFromURLpost(_ url: URL) -> Self? { - let path = url.path - let components = path.split(separator: "/") + case post(postId: String) - guard components.count == 2 else { - return nil - } + static func post(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "posts", "{postId}"] - guard let id = components[1] as? String else { - return nil - } + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil + } + let postId = inputPaths[2] + return .post(postId: postId) + } - return .post(id: id) + case postComment(postId: String, commentId: String) + + static func postComment(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "posts", "{postId}", "comments", "{commentId}"] + + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil } + let postId = inputPaths[2] + let commentId = inputPaths[4] + return .postComment(postId: postId, commentId: commentId) + } - case name(id: String, name: String) + case complex(aNum: Int, bNum: Int, cNum: Int) - static func createFromURLname(_ url: URL) -> Self? { - let path = url.path - let components = path.split(separator: "/") + static func complex(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "c", "{cNum}", "b", "{bNum}", "a", "{aNum}"] - guard components.count == 3 else { - return nil - } + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil + } + guard let aNum = Int(inputPaths[6]) else { + return nil + } + guard let bNum = Int(inputPaths[4]) else { + return nil + } + guard let cNum = Int(inputPaths[2]) else { + return nil + } + return .complex(aNum: aNum, bNum: bNum, cNum: cNum) + } - guard let id = components[1] as? String else { - return nil - } - guard let name = components[2] as? String else { - return nil - } + init?(url: URL) { + if let urlPattern = Self.home(url) { + self = urlPattern + return + } + if let urlPattern = Self.post(url) { + self = urlPattern + return + } + if let urlPattern = Self.postComment(url) { + self = urlPattern + return + } + if let urlPattern = Self.complex(url) { + self = urlPattern + return + } + return nil + } - return .name(id: id, name: name) + static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool { + guard inputs.count == patterns.count else { + return false } - init?(url: URL) { - if let result = Self.createFromURLpost(url) { - self = result - return - } - if let result = Self.createFromURLname(url) { - self = result - return + return zip(inputs, patterns).allSatisfy { input, pattern in + guard Self.isURLPathValue(pattern) else { + return input == pattern } + + return true + } + } + + static func isURLPathValue(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 + } + } + """, + macros: testMacros + ) + } + + func testDeepLinkMacro_shouldIgnoreNoneMacroCase() throws { + assertMacroExpansion( + """ + @URLPattern + enum DeepLink: Equatable { + @URLPath("/home") + case home + + case complex(aNum: Int, bNum: Int, cNum: Int) + } + """, + expandedSource: """ + enum DeepLink: Equatable { + case home + + static func home(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "home"] + + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil } + + return .home + } + + case complex(aNum: Int, bNum: Int, cNum: Int) + + init?(url: URL) { + if let urlPattern = Self.home(url) { + self = urlPattern + return + } + return nil } - """, - macros: testMacros - ) - } - func testMacroWithStringLiteral() throws { + static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool { + guard inputs.count == patterns.count else { + return false + } + + return zip(inputs, patterns).allSatisfy { input, pattern in + guard Self.isURLPathValue(pattern) else { + return input == pattern + } + + return true + } + } - } + static func isURLPathValue(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 + } + } + """, + macros: testMacros + ) + } } diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..134908b --- /dev/null +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj @@ -0,0 +1,515 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + EF43E6482D9ED87500809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E6472D9ED87500809F76 /* URLPattern */; }; + EF43E64B2D9ED8A800809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64A2D9ED8A800809F76 /* URLPattern */; }; + EF43E64D2D9ED8A800809F76 /* URLPatternClient in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64C2D9ED8A800809F76 /* URLPatternClient */; }; + EF43E6502D9ED93E00809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E64F2D9ED93E00809F76 /* URLPattern */; }; + EF43E72D2D9FE2C300809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E72C2D9FE2C300809F76 /* URLPattern */; }; + EF43E7302D9FE2CC00809F76 /* URLPattern in Frameworks */ = {isa = PBXBuildFile; productRef = EF43E72F2D9FE2CC00809F76 /* URLPattern */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EF43E6112D9ED86000809F76 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EF43E6182D9ED86000809F76; + remoteInfo = URLPatternExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + EF43E6192D9ED86000809F76 /* URLPatternExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = URLPatternExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLPatternExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + EF43E61B2D9ED86000809F76 /* URLPatternExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = URLPatternExample; + sourceTree = ""; + }; + EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = URLPatternExampleTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + EF43E6162D9ED86000809F76 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EF43E72D2D9FE2C300809F76 /* URLPattern in Frameworks */, + EF43E6482D9ED87500809F76 /* URLPattern in Frameworks */, + EF43E64D2D9ED8A800809F76 /* URLPatternClient in Frameworks */, + EF43E64B2D9ED8A800809F76 /* URLPattern in Frameworks */, + EF43E6502D9ED93E00809F76 /* URLPattern in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6262D9ED86100809F76 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EF43E7302D9FE2CC00809F76 /* URLPattern in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + EF43E6102D9ED86000809F76 = { + isa = PBXGroup; + children = ( + EF43E61B2D9ED86000809F76 /* URLPatternExample */, + EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */, + EF43E72E2D9FE2CC00809F76 /* Frameworks */, + EF43E61A2D9ED86000809F76 /* Products */, + ); + sourceTree = ""; + }; + EF43E61A2D9ED86000809F76 /* Products */ = { + isa = PBXGroup; + children = ( + EF43E6192D9ED86000809F76 /* URLPatternExample.app */, + EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + EF43E72E2D9FE2CC00809F76 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + EF43E6182D9ED86000809F76 /* URLPatternExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = EF43E63D2D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExample" */; + buildPhases = ( + EF43E6152D9ED86000809F76 /* Sources */, + EF43E6162D9ED86000809F76 /* Frameworks */, + EF43E6172D9ED86000809F76 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + EF43E61B2D9ED86000809F76 /* URLPatternExample */, + ); + name = URLPatternExample; + packageProductDependencies = ( + EF43E6472D9ED87500809F76 /* URLPattern */, + EF43E64A2D9ED8A800809F76 /* URLPattern */, + EF43E64C2D9ED8A800809F76 /* URLPatternClient */, + EF43E64F2D9ED93E00809F76 /* URLPattern */, + EF43E72C2D9FE2C300809F76 /* URLPattern */, + ); + productName = URLPatternExample; + productReference = EF43E6192D9ED86000809F76 /* URLPatternExample.app */; + productType = "com.apple.product-type.application"; + }; + EF43E6282D9ED86100809F76 /* URLPatternExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EF43E6402D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleTests" */; + buildPhases = ( + EF43E6252D9ED86100809F76 /* Sources */, + EF43E6262D9ED86100809F76 /* Frameworks */, + EF43E6272D9ED86100809F76 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EF43E62B2D9ED86100809F76 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */, + ); + name = URLPatternExampleTests; + packageProductDependencies = ( + EF43E72F2D9FE2CC00809F76 /* URLPattern */, + ); + productName = URLPatternExampleTests; + productReference = EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + EF43E6112D9ED86000809F76 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + EF43E6182D9ED86000809F76 = { + CreatedOnToolsVersion = 16.2; + }; + EF43E6282D9ED86100809F76 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = EF43E6102D9ED86000809F76; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = EF43E61A2D9ED86000809F76 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EF43E6182D9ED86000809F76 /* URLPatternExample */, + EF43E6282D9ED86100809F76 /* URLPatternExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + EF43E6172D9ED86000809F76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6272D9ED86100809F76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + EF43E6152D9ED86000809F76 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6252D9ED86100809F76 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + EF43E62B2D9ED86100809F76 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EF43E6182D9ED86000809F76 /* URLPatternExample */; + targetProxy = EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + EF43E63B2D9ED86100809F76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + EF43E63C2D9ED86100809F76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + EF43E63E2D9ED86100809F76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"URLPatternExample/Preview Content\""; + DEVELOPMENT_TEAM = HK54HM2TC6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + EF43E63F2D9ED86100809F76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"URLPatternExample/Preview Content\""; + DEVELOPMENT_TEAM = HK54HM2TC6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + EF43E6412D9ED86100809F76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + EF43E6422D9ED86100809F76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EF43E63B2D9ED86100809F76 /* Debug */, + EF43E63C2D9ED86100809F76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EF43E63D2D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EF43E63E2D9ED86100809F76 /* Debug */, + EF43E63F2D9ED86100809F76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EF43E6402D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EF43E6412D9ED86100809F76 /* Debug */, + EF43E6422D9ED86100809F76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../URLPattern; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + EF43E6472D9ED87500809F76 /* URLPattern */ = { + isa = XCSwiftPackageProductDependency; + productName = URLPattern; + }; + EF43E64A2D9ED8A800809F76 /* URLPattern */ = { + isa = XCSwiftPackageProductDependency; + productName = URLPattern; + }; + EF43E64C2D9ED8A800809F76 /* URLPatternClient */ = { + isa = XCSwiftPackageProductDependency; + productName = URLPatternClient; + }; + EF43E64F2D9ED93E00809F76 /* URLPattern */ = { + isa = XCSwiftPackageProductDependency; + productName = URLPattern; + }; + EF43E72C2D9FE2C300809F76 /* URLPattern */ = { + isa = XCSwiftPackageProductDependency; + productName = URLPattern; + }; + EF43E72F2D9FE2CC00809F76 /* URLPattern */ = { + isa = XCSwiftPackageProductDependency; + package = EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */; + productName = URLPattern; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = EF43E6112D9ED86000809F76 /* Project object */; +} diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..25ea83d --- /dev/null +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b71e74ae1ae16b256751a8f504b75f549eaf477761821c6159aa06486b495baa", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 3 +} diff --git a/URLPatternExample/URLPatternExample.xcodeproj/xcshareddata/xcschemes/URLPatternExample.xcscheme b/URLPatternExample/URLPatternExample.xcodeproj/xcshareddata/xcschemes/URLPatternExample.xcscheme new file mode 100644 index 0000000..ba077b1 --- /dev/null +++ b/URLPatternExample/URLPatternExample.xcodeproj/xcshareddata/xcschemes/URLPatternExample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/URLPatternExample/URLPatternExample/Assets.xcassets/AccentColor.colorset/Contents.json b/URLPatternExample/URLPatternExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/URLPatternExample/URLPatternExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/URLPatternExample/URLPatternExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/URLPatternExample/URLPatternExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/URLPatternExample/URLPatternExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/URLPatternExample/URLPatternExample/Assets.xcassets/Contents.json b/URLPatternExample/URLPatternExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/URLPatternExample/URLPatternExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/URLPatternExample/URLPatternExample/DeepLink.swift b/URLPatternExample/URLPatternExample/DeepLink.swift new file mode 100644 index 0000000..8847dcc --- /dev/null +++ b/URLPatternExample/URLPatternExample/DeepLink.swift @@ -0,0 +1,23 @@ +// +// DeepLink.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import URLPattern + +@URLPattern +enum DeepLink: Equatable { + @URLPath("/home") + case home + + @URLPath("/posts/{postId}") + case post(postId: String) + + @URLPath("/posts/{postId}/comments/{commentId}") + case postComment(postId: String, commentId: String) + + @URLPath("/setting/{number}") + case setting(number: Int) +} diff --git a/URLPatternExample/URLPatternExample/Preview Content/Preview Assets.xcassets/Contents.json b/URLPatternExample/URLPatternExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/URLPatternExample/URLPatternExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/URLPatternExample/URLPatternExample/RootView.swift b/URLPatternExample/URLPatternExample/RootView.swift new file mode 100644 index 0000000..723836b --- /dev/null +++ b/URLPatternExample/URLPatternExample/RootView.swift @@ -0,0 +1,57 @@ +// +// RootView.swift +// URLPatternExample +// +// Created by woody on 4/3/25. +// + +import SwiftUI + +struct RootView: View { + @State private var path = NavigationPath() + + var body: some View { + NavigationStack(path: $path) { + List { + Section("DeepLink Example") { + Text("https://domain/home") + Text("https://domain/posts/1") + Text("https://domain/posts/1/comments/2") + Text("https://domain/setting/1") + } + Section("Invalid DeepLink") { + Text("https://home") + Text("https://domain/homes") + Text("https://domain/post/1") + Text("https://domain/setting/string") + } + } + .navigationTitle("URLPattern Example") + .environment(\.openURL, OpenURLAction { url in + guard let deepLink = DeepLink(url: url) else { + return .discarded + } + + switch deepLink { + case .home: + path.append(Route.home) + case .post(let postId): + path.append(Route.post(postId: postId)) + case .postComment(let postId, let commentId): + path.append(Route.post(postId: postId)) + path.append(Route.comment(commentId: commentId)) + case .setting(let number): + path.append(Route.setting(number: number)) + } + return .handled + }) + .navigationDestination(for: Route.self) { route in + route + } + } + } +} + +#Preview { + RootView() +} diff --git a/URLPatternExample/URLPatternExample/Route.swift b/URLPatternExample/URLPatternExample/Route.swift new file mode 100644 index 0000000..c35849d --- /dev/null +++ b/URLPatternExample/URLPatternExample/Route.swift @@ -0,0 +1,28 @@ +// +// Route.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import SwiftUI + +enum Route: Hashable, View { + case home + case post(postId: String) + case comment(commentId: String) + case setting(number: Int) + + var body: some View { + switch self { + case .home: + HomeView() + case .post(let postId): + PostView(postId: postId) + case .comment(let commentId): + CommentView(commentId: commentId) + case .setting(let number): + SettingView(number: number) + } + } +} diff --git a/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift b/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift new file mode 100644 index 0000000..eafa142 --- /dev/null +++ b/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift @@ -0,0 +1,17 @@ +// +// URLPatternExampleApp.swift +// URLPatternExample +// +// Created by woody on 4/3/25. +// + +import SwiftUI + +@main +struct URLPatternExampleApp: App { + var body: some Scene { + WindowGroup { + RootView() + } + } +} diff --git a/URLPatternExample/URLPatternExample/View/CommentView.swift b/URLPatternExample/URLPatternExample/View/CommentView.swift new file mode 100644 index 0000000..6ce76a6 --- /dev/null +++ b/URLPatternExample/URLPatternExample/View/CommentView.swift @@ -0,0 +1,18 @@ +// +// CommentView.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import SwiftUI + +struct CommentView: View { + let commentId: String + + var body: some View { + Text("💬 commentId: \(commentId)") + .font(.largeTitle) + .navigationTitle("Comment") + } +} diff --git a/URLPatternExample/URLPatternExample/View/HomeView.swift b/URLPatternExample/URLPatternExample/View/HomeView.swift new file mode 100644 index 0000000..9e91ea0 --- /dev/null +++ b/URLPatternExample/URLPatternExample/View/HomeView.swift @@ -0,0 +1,16 @@ +// +// HomeView.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import SwiftUI + +struct HomeView: View { + var body: some View { + Text("🏠") + .font(.largeTitle) + .navigationTitle("Home") + } +} diff --git a/URLPatternExample/URLPatternExample/View/PostView.swift b/URLPatternExample/URLPatternExample/View/PostView.swift new file mode 100644 index 0000000..11f83e9 --- /dev/null +++ b/URLPatternExample/URLPatternExample/View/PostView.swift @@ -0,0 +1,18 @@ +// +// PostView.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import SwiftUI + +struct PostView: View { + let postId: String + + var body: some View { + Text("📮 postId: \(postId)") + .font(.largeTitle) + .navigationTitle("Post") + } +} diff --git a/URLPatternExample/URLPatternExample/View/SettingView.swift b/URLPatternExample/URLPatternExample/View/SettingView.swift new file mode 100644 index 0000000..729f1f1 --- /dev/null +++ b/URLPatternExample/URLPatternExample/View/SettingView.swift @@ -0,0 +1,18 @@ +// +// SettingView.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +import SwiftUI + +struct SettingView: View { + let number: Int + + var body: some View { + Text("⚙️ number: \(number)") + .font(.largeTitle) + .navigationTitle("Setting") + } +} diff --git a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift new file mode 100644 index 0000000..d4814e2 --- /dev/null +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -0,0 +1,294 @@ +// +// URLPatternExampleTests.swift +// URLPatternExampleTests +// +// Created by woody on 4/3/25. +// + +import Testing +import URLPattern + +@URLPattern +enum DeepLinkMock: Equatable { + @URLPath("/home") + case home + + @URLPath("/posts/{postId}") + case post(postId: String) + + @URLPath("/posts/{postId}/comments/{commentId}") + case postComment(postId: String, commentId: String) + + @URLPath("/setting/phone/{number}") + case setting(number: Int) + + @URLPath("/int/{int}") + case int(int: Int) + + @URLPath("/float/{float}") + case float(float: Float) + + @URLPath("/string/{string}") + case string(string: String) + + @URLPath("/c/{cNum}/b/{bNum}/a/{aNum}") + case complex(aNum: Int, bNum: Int, cNum: Int) +} + +struct URLPatternExampleTests { + // MARK: - Home Tests + @Test("Valid Home URLPatterns", arguments: [ + "https://domain.com/home", + "/home" + ]) + func parseHome_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .home) + } + + @Test("Invalid Home URLPatterns", arguments: [ + "https://domain.com/home/12", + "https://home", + "home" + ]) + func parseHome_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Post Tests + @Test("Valid Post URLPatterns", arguments: [ + "https://domain.com/posts/1", + "/posts/1" + ]) + func parsePost_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .post(postId: "1")) + } + + @Test("Invalid Post URLPatterns", arguments: [ + "https://domain.com/posts/1/test", + "https://domain.com/post/1", + "/post/1" + ]) + func parsePost_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Post Comment Tests + @Test("Valid PostComment URLPatterns", arguments: [ + "https://domain.com/posts/1/comments/2", + "/posts/1/comments/2" + ]) + func parsePostComment_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .postComment(postId: "1", commentId: "2")) + } + + @Test("Invalid PostComment URLPatterns", arguments: [ + "https://domain.com/posts/1/comment/2", + "/posts/1/comments", + "/posts/comments/2" + ]) + func parsePostComment_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Setting Tests + @Test("Valid Setting URLPatterns", arguments: [ + "https://domain.com/setting/phone/42", + "/setting/phone/42" + ]) + func parseSetting_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .setting(number: 42)) + } + + @Test("Invalid Setting URLPatterns", arguments: [ + "https://domain.com/setting/abc/42", + "/setting/phone/12.34", + "/setting/phone" + ]) + func parseSetting_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Int Tests + @Test("Valid Int URLPatterns", arguments: [ + "https://domain.com/int/-42", + "/int/-42" + ]) + func parseInt_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .int(int: -42)) + } + + @Test("Invalid Int URLPatterns", arguments: [ + "https://domain.com/int/abc", + "/int/12.34", + "/int/" + ]) + func parseInt_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Float Tests + @Test("Valid Float URLPatterns", arguments: [ + "https://domain.com/float/42.5", + "/float/42.5" + ]) + func parseFloat_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .float(float: 42.5)) + } + + @Test("Invalid Float URLPatterns", arguments: [ + "https://domain.com/float/abc", + "/float/", + "/float/." + ]) + func parseFloat_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - String Tests + @Test("Valid String URLPatterns", arguments: [ + "https://domain.com/string/hello", + "/string/hello" + ]) + func parseString_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .string(string: "hello")) + } + + @Test("Invalid String URLPatterns", arguments: [ + "https://domain.com/string", + "/string/", + "/string/test/extra" + ]) + func parseString_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Complex Tests + @Test("Valid Complex URLPatterns", arguments: [ + "https://domain.com/c/1/b/2/a/3", + "/c/1/b/2/a/3" + ]) + func parseComplex_success(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = try #require(DeepLinkMock(url: url)) + #expect(deepLink == .complex(aNum: 3, bNum: 2, cNum: 1)) + } + + @Test("Invalid Complex URLPatterns", arguments: [ + "https://domain.com/c/1/b/2/a/abc", + "/c/1/b/abc/a/3", + "/c/abc/b/2/a/3", + "/c/1/b/2/a", + "/c/1/b/a/3", + "/a/1/b/2/c/3" + ]) + func parseComplex_failure(urlString: String) async throws { + let url = try #require(URL(string: urlString)) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == nil) + } + + // MARK: - Unicode Tests + @Test("Valid Unicodes", arguments: [ + "안녕하세요", + "こんにちは", + "☺️👍" + ]) + + func paresPost_success_with_unicode(value: String) async throws { + let url = try #require(URL(string: "/posts/\(value)")) + let deepLink = DeepLinkMock(url: url) + #expect(deepLink == .post(postId: value)) + } + + // MARK: - Priority Tests + @URLPattern + enum PriorityTest: Equatable { + @URLPath("/{a}/{b}") + case all(a: String, b: String) + + @URLPath("/post/{postId}") + case post(postId: Int) + } + + @Test("Test Scope") + func checkPriorityCases() async throws { + let url = try #require(URL(string: "/post/1")) + #expect(PriorityTest(url: url) == .all(a: "post", b: "1")) + } + + // MARK: - Scope Tests + @URLPattern + enum ScopeTest { + @URLPath("/") + case zero + + @URLPath("/{a}") + case one(a: String) + + @URLPath("/{a}/{b}") + case two(a: String, b: String) + + @URLPath("/{a}/{b}/{c}") + case three(a: String, b: String, c: String) + + @URLPath("/{a}/{b}/{c}/{d}") + case four(a: String, b: String, c: String, d: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}") + case five(a: String, b: String, c: String, d: String, e: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}") + case six(a: String, b: String, c: String, d: String, e: String, f: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}") + case seven(a: String, b: String, c: String, d: String, e: String, f: String, g: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}") + case eight(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}") + case nine(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String, i: String) + + @URLPath("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}") + case ten(a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String, i: String, j: String) + } + + @Test("Test scope", arguments: Array(0...20)) + func checkScope(num: Int) async throws { + let url = try #require(URL(string: "/" + (0.. 10 { + #expect(ScopeTest(url: url) == nil) + } else { + #expect(ScopeTest(url: url) != nil) + } + } +}