From 4debd2131b43c33b0461091b4ccdddc810846711 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Thu, 3 Apr 2025 23:24:33 +0900 Subject: [PATCH 01/10] Add mvp features --- Sources/URLPatternClient/main.swift | 7 +- Sources/URLPatternMacros/URLPathMacro.swift | 100 +++++++++++++----- .../URLPatternMacros/URLPatternMacro.swift | 22 ++-- .../Utils/URLPatternError.swift | 13 +++ Tests/URLPatternTests/URLPatternTests.swift | 62 ++++++----- 5 files changed, 135 insertions(+), 69 deletions(-) create mode 100644 Sources/URLPatternMacros/Utils/URLPatternError.swift diff --git a/Sources/URLPatternClient/main.swift b/Sources/URLPatternClient/main.swift index 2e25082..a9d351a 100644 --- a/Sources/URLPatternClient/main.swift +++ b/Sources/URLPatternClient/main.swift @@ -3,11 +3,8 @@ 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}") + case name(id: Int, name: String) @URLPath("/post/{id}/{name}/hi/{good}") case nameDetail(id: String, name: String, good: String) diff --git a/Sources/URLPatternMacros/URLPathMacro.swift b/Sources/URLPatternMacros/URLPathMacro.swift index f4733dd..5144cb7 100644 --- a/Sources/URLPatternMacros/URLPathMacro.swift +++ b/Sources/URLPatternMacros/URLPathMacro.swift @@ -5,13 +5,17 @@ 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 } public static func expansion( @@ -23,48 +27,90 @@ 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 pathComponents = pathURL.pathComponents - let parameters = pathComponents.enumerated() + + 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 = patternPaths.enumerated() .filter { index, value in value.isURLPathParam } - .map { CaseParam(index: $0.offset, name: String($0.element.dropFirst().dropLast())) } + .map { index, value in + let name = String(value.dropFirst().dropLast()) + return PatternParam( + name:name , + type: caseAssociatedTypes.first(where: { name == $0.name })!.type, + pathIndex: index + ) + } - if Set(parameters).count != parameters.count { - throw MacroError.message("변수 이름은 중복되서는 안됩니다.") + if Set(patternParams.map { $0.name }).count != patternParams.count { + throw URLPatternError("The name of an associated value cannot be duplicated") } - + + if Set(patternParams).count != caseAssociatedTypes.count { + throw URLPatternError("The number of associated values does not match URLPath") + } + 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 } + let inputPaths = url.pathComponents + let patternPaths = \(raw: patternPaths) - \(raw: parameters.map { param in - """ - let \(param.name) = inputPaths[\(param.index)] - """ + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil + } + + \(raw: patternParams.map { param in + switch param.type { + case .Double: + """ + guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { + return nil + } + """ + case .Float: + """ + guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { + return nil + } + """ + case .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: element.name.text)(\(raw: 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..566e020 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,16 +11,16 @@ 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 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 + if let urlPattern = Self.\(raw: caseName)(url) { + self = urlPattern + return } """ } @@ -40,7 +36,7 @@ public struct URLPatternMacro: MemberMacro { guard inputs.count == patterns.count else { return false } return zip(inputs, patterns).allSatisfy { input, pattern in - guard pattern.isURLPathParam else { return input == pattern } + guard Self.isURLPathParam(pattern) else { return input == pattern } return true } @@ -49,10 +45,14 @@ public struct URLPatternMacro: MemberMacro { let isURLPathParamMethod = try FunctionDeclSyntax(""" static func isURLPathParam(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") } + return string.hasPrefix("{") && string.hasSuffix("}") } """) - return [DeclSyntax(urlInitializer), DeclSyntax(isValidURLPathsMethod), DeclSyntax(isURLPathParamMethod)] + return [ + DeclSyntax(urlInitializer), + DeclSyntax(isValidURLPathsMethod), + DeclSyntax(isURLPathParamMethod) + ] } } 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..a0ae823 100644 --- a/Tests/URLPatternTests/URLPatternTests.swift +++ b/Tests/URLPatternTests/URLPatternTests.swift @@ -14,8 +14,6 @@ let testMacros: [String: Macro.Type] = [ ] #endif - - final class URLPatternTests: XCTestCase { func testMacro() throws { assertMacroExpansion( @@ -26,59 +24,71 @@ final class URLPatternTests: XCTestCase { case post(id: String) @URLPath("/post/{id}/{name}") - case name(id: String, name: String) + case name(id: String, name: Int) } """, expandedSource: """ enum Deeplink { case post(id: String) - static func createFromURLpost(_ url: URL) -> Self? { - let path = url.path - let components = path.split(separator: "/") + static func post(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "post", "{id}"] - guard components.count == 2 else { + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil } - guard let id = components[1] as? String else { - return nil - } + let id = inputPaths[2] return .post(id: id) } case name(id: String, name: String) - static func createFromURLname(_ url: URL) -> Self? { - let path = url.path - let components = path.split(separator: "/") + static func name(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "post", "{id}", "{name}"] - guard components.count == 3 else { + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil } - guard let id = components[1] as? String else { - return nil - } - guard let name = components[2] as? String else { - return nil - } + let id = inputPaths[2] + let name = inputPaths[3] return .name(id: id, name: name) } init?(url: URL) { - if let result = Self.createFromURLpost(url) { - self = result - return + if let result = Self.post(url) { + self = result + return } - if let result = Self.createFromURLname(url) { - self = result - return + if let result = Self.name(url) { + self = result + return } return nil } + + 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 pattern.isURLPathParam else { + return input == pattern + } + + return true + } + } + + static func isURLPathParam(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") + } } """, macros: testMacros From ccfe81895430b49909cb024103c9dd3d6f275acc Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 01:19:31 +0900 Subject: [PATCH 02/10] Add URLPatternExample --- Sources/URLPatternClient/main.swift | 5 + Sources/URLPatternMacros/URLPathMacro.swift | 6 +- .../project.pbxproj | 600 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/swiftpm/Package.resolved | 15 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 6 + .../URLPatternExample/ContentView.swift | 126 ++++ .../Preview Assets.xcassets/Contents.json | 6 + .../URLPatternExampleApp.swift | 17 + .../URLPatternExampleTests.swift | 30 + 12 files changed, 862 insertions(+), 2 deletions(-) create mode 100644 URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj create mode 100644 URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 URLPatternExample/URLPatternExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 URLPatternExample/URLPatternExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 URLPatternExample/URLPatternExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 URLPatternExample/URLPatternExample/Assets.xcassets/Contents.json create mode 100644 URLPatternExample/URLPatternExample/ContentView.swift create mode 100644 URLPatternExample/URLPatternExample/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 URLPatternExample/URLPatternExample/URLPatternExampleApp.swift create mode 100644 URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift diff --git a/Sources/URLPatternClient/main.swift b/Sources/URLPatternClient/main.swift index a9d351a..97a072d 100644 --- a/Sources/URLPatternClient/main.swift +++ b/Sources/URLPatternClient/main.swift @@ -11,10 +11,14 @@ enum Deeplink { @URLPath("/post/{id}") case nameDetailHI(id: String) + + @URLPath("/home") + case home } let url1 = URL(string: "https://channel.io/post/12/12") let url2 = URL(string: "/post/hi/hello/hi/bye") +let url3 = URL(string: "/home") // enumPath // inputPath @@ -29,4 +33,5 @@ let paths = url1!.pathComponents print(Deeplink(url: url1!)) print(Deeplink(url: url2!)) +print(Deeplink(url: url3!)) diff --git a/Sources/URLPatternMacros/URLPathMacro.swift b/Sources/URLPatternMacros/URLPathMacro.swift index 5144cb7..dac07d0 100644 --- a/Sources/URLPatternMacros/URLPathMacro.swift +++ b/Sources/URLPatternMacros/URLPathMacro.swift @@ -107,8 +107,10 @@ public struct URLPathMacro: PeerMacro { """ } }.joined(separator: "\n")) - - return .\(raw: element.name.text)(\(raw: patternParams.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: ", ")))") } """) diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f3f2566 --- /dev/null +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj @@ -0,0 +1,600 @@ +// !$*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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EF43E6112D9ED86000809F76 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EF43E6182D9ED86000809F76; + remoteInfo = URLPatternExample; + }; + EF43E6342D9ED86100809F76 /* 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; }; + EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLPatternExampleUITests.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 = ( + 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 = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6302D9ED86100809F76 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + EF43E6102D9ED86000809F76 = { + isa = PBXGroup; + children = ( + EF43E61B2D9ED86000809F76 /* URLPatternExample */, + EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */, + EF43E61A2D9ED86000809F76 /* Products */, + ); + sourceTree = ""; + }; + EF43E61A2D9ED86000809F76 /* Products */ = { + isa = PBXGroup; + children = ( + EF43E6192D9ED86000809F76 /* URLPatternExample.app */, + EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */, + EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */, + ); + name = Products; + 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 */, + ); + 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 = ( + ); + productName = URLPatternExampleTests; + productReference = EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EF43E6322D9ED86100809F76 /* URLPatternExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EF43E6432D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleUITests" */; + buildPhases = ( + EF43E62F2D9ED86100809F76 /* Sources */, + EF43E6302D9ED86100809F76 /* Frameworks */, + EF43E6312D9ED86100809F76 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EF43E6352D9ED86100809F76 /* PBXTargetDependency */, + ); + name = URLPatternExampleUITests; + packageProductDependencies = ( + ); + productName = URLPatternExampleUITests; + productReference = EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* 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; + TestTargetID = EF43E6182D9ED86000809F76; + }; + EF43E6322D9ED86100809F76 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = EF43E6182D9ED86000809F76; + }; + }; + }; + buildConfigurationList = EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = EF43E6102D9ED86000809F76; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + EF43E64E2D9ED93E00809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = EF43E61A2D9ED86000809F76 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EF43E6182D9ED86000809F76 /* URLPatternExample */, + EF43E6282D9ED86100809F76 /* URLPatternExampleTests */, + EF43E6322D9ED86100809F76 /* URLPatternExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + EF43E6172D9ED86000809F76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6272D9ED86100809F76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF43E6312D9ED86100809F76 /* 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; + }; + EF43E62F2D9ED86100809F76 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + EF43E62B2D9ED86100809F76 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EF43E6182D9ED86000809F76 /* URLPatternExample */; + targetProxy = EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */; + }; + EF43E6352D9ED86100809F76 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EF43E6182D9ED86000809F76 /* URLPatternExample */; + targetProxy = EF43E6342D9ED86100809F76 /* 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 = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + 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"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLPatternExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLPatternExample"; + }; + name = Debug; + }; + EF43E6422D9ED86100809F76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + 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"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLPatternExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLPatternExample"; + }; + name = Release; + }; + EF43E6442D9ED86100809F76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = URLPatternExample; + }; + name = Debug; + }; + EF43E6452D9ED86100809F76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HK54HM2TC6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = URLPatternExample; + }; + 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; + }; + EF43E6432D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EF43E6442D9ED86100809F76 /* Debug */, + EF43E6452D9ED86100809F76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + EF43E64E2D9ED93E00809F76 /* 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; + }; +/* 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/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/ContentView.swift b/URLPatternExample/URLPatternExample/ContentView.swift new file mode 100644 index 0000000..72a475f --- /dev/null +++ b/URLPatternExample/URLPatternExample/ContentView.swift @@ -0,0 +1,126 @@ +// +// ContentView.swift +// URLPatternExample +// +// Created by woody on 4/3/25. +// + +import SwiftUI +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) +} + +enum Route: Hashable { + case home + case post(postId: String) + case comment(commentId: String) + case setting(number: Int) +} + +struct ContentView: 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("Wrong DeepLink Example") { + 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) { deepLink in + switch deepLink { + case .home: + Home() + case .post(let postId): + Post(postId: postId) + case .comment(let commentId): + Comment(commentId: commentId) + case .setting(let number): + Setting(number: number) + } + } + } + } +} + +struct Home: View { + var body: some View { + Text("🏠") + .font(.largeTitle) + .navigationTitle("Home") + } +} + +struct Post: View { + let postId: String + + var body: some View { + Text("📮 postId: \(postId)") + .font(.largeTitle) + .navigationTitle("Post") + } +} + +struct Comment: View { + let commentId: String + + var body: some View { + Text("💬 commentId: \(commentId)") + .font(.largeTitle) + .navigationTitle("Comment") + } +} + +struct Setting: View { + let number: Int + + var body: some View { + Text("⚙️ number: \(number)") + .font(.largeTitle) + .navigationTitle("Setting") + } +} + +#Preview { + ContentView() +} 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/URLPatternExampleApp.swift b/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift new file mode 100644 index 0000000..c85774e --- /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 { + ContentView() + } + } +} diff --git a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift new file mode 100644 index 0000000..1c7c992 --- /dev/null +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -0,0 +1,30 @@ +// +// URLPatternExampleTests.swift +// URLPatternExampleTests +// +// Created by woody on 4/3/25. +// + +import Testing +import Foundation +@testable import URLPatternExample + +struct URLPatternExampleTests { + @Test func testSingleValue() async throws { + let url = try #require(URL(string: "https://www.example.com/home")) + let deepLink = try #require(DeepLink(url: url)) + #expect(deepLink == .home) + } + + @Test func testWrongPathSingleValue() async throws { + let url = try #require(URL(string: "https://www.example.com/homes")) + let deepLink = DeepLink(url: url) + #expect(deepLink == nil) + } + + @Test func testWrongManyPathSingleValue() async throws { + let url = try #require(URL(string: "https://www.example.com/home/path")) + let deepLink = DeepLink(url: url) + #expect(deepLink == nil) + } +} From ecd1a7b0126d9341c16ff83ed195f705555e6205 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 17:43:26 +0900 Subject: [PATCH 03/10] Edit parse rule and Add Tests --- Package.swift | 78 +++---- Sources/URLPattern/URLPattern.swift | 2 + Sources/URLPatternClient/main.swift | 36 --- Sources/URLPatternMacros/URLPathMacro.swift | 71 +++--- .../URLPatternMacros/URLPatternMacro.swift | 41 ++-- Tests/URLPatternTests/URLPatternTests.swift | 220 +++++++++++++----- .../project.pbxproj | 107 +-------- .../URLPatternExample/ContentView.swift | 126 ---------- .../URLPatternExample/DeepLink.swift | 23 ++ .../URLPatternExample/Mock/DeepLinkMock.swift | 35 +++ .../URLPatternExample/RootView.swift | 57 +++++ .../URLPatternExample/Route.swift | 28 +++ .../URLPatternExampleApp.swift | 2 +- .../URLPatternExample/View/CommentView.swift | 18 ++ .../URLPatternExample/View/HomeView.swift | 16 ++ .../URLPatternExample/View/PostView.swift | 18 ++ .../URLPatternExample/View/SettingView.swift | 18 ++ .../URLPatternExampleTests.swift | 179 +++++++++++++- 18 files changed, 645 insertions(+), 430 deletions(-) delete mode 100644 URLPatternExample/URLPatternExample/ContentView.swift create mode 100644 URLPatternExample/URLPatternExample/DeepLink.swift create mode 100644 URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift create mode 100644 URLPatternExample/URLPatternExample/RootView.swift create mode 100644 URLPatternExample/URLPatternExample/Route.swift create mode 100644 URLPatternExample/URLPatternExample/View/CommentView.swift create mode 100644 URLPatternExample/URLPatternExample/View/HomeView.swift create mode 100644 URLPatternExample/URLPatternExample/View/PostView.swift create mode 100644 URLPatternExample/URLPatternExample/View/SettingView.swift 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/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 97a072d..efba80d 100644 --- a/Sources/URLPatternClient/main.swift +++ b/Sources/URLPatternClient/main.swift @@ -1,37 +1 @@ import URLPattern -import Foundation - -@URLPattern -enum Deeplink { - @URLPath("/post/{id}/{name}") - case name(id: Int, name: String) - - @URLPath("/post/{id}/{name}/hi/{good}") - case nameDetail(id: String, name: String, good: String) - - @URLPath("/post/{id}") - case nameDetailHI(id: String) - - @URLPath("/home") - case home -} - -let url1 = URL(string: "https://channel.io/post/12/12") -let url2 = URL(string: "/post/hi/hello/hi/bye") -let url3 = URL(string: "/home") - -// 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!)) -print(Deeplink(url: url3!)) - diff --git a/Sources/URLPatternMacros/URLPathMacro.swift b/Sources/URLPatternMacros/URLPathMacro.swift index dac07d0..659cb47 100644 --- a/Sources/URLPatternMacros/URLPathMacro.swift +++ b/Sources/URLPatternMacros/URLPathMacro.swift @@ -16,6 +16,7 @@ public struct URLPathMacro: PeerMacro { let name: String let type: SupportedType let pathIndex: Int + let caseIndex: Int } public static func expansion( @@ -53,53 +54,54 @@ public struct URLPathMacro: PeerMacro { return (name: name, type: supportedType) } ?? [] - let patternParams = patternPaths.enumerated() + let patternParams: [PatternParam] = try patternPaths.enumerated() .filter { index, value in value.isURLPathParam } - .map { index, value in + .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: caseAssociatedTypes.first(where: { name == $0.name })!.type, - pathIndex: index + 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)) + + guard patternNames.count == patternParams.count else { + throw URLPatternError("The name of an URLPath value cannot be duplicated") + } - if Set(patternParams.map { $0.name }).count != patternParams.count { + guard caseNames.count == caseAssociatedTypes.count else { throw URLPatternError("The name of an associated value cannot be duplicated") } - if Set(patternParams).count != caseAssociatedTypes.count { + 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: patternParams.map { param in + let inputPaths = url.pathComponents + let patternPaths = \(raw: patternPaths) + + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil } + \(raw: patternParams.map { param in switch param.type { - case .Double: + case .Double, .Float, .Int: """ - guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { - return nil - } - """ - case .Float: - """ - guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { - return nil - } - """ - case .Int: - """ - guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { - return nil - } + guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { return nil } """ case .String: """ @@ -107,10 +109,9 @@ public struct URLPathMacro: PeerMacro { """ } }.joined(separator: "\n")) - - return \(raw: patternParams.isEmpty - ? ".\(element.name.text)" - : ".\(element.name.text)(\(patternParams.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: ", ")))") } """) diff --git a/Sources/URLPatternMacros/URLPatternMacro.swift b/Sources/URLPatternMacros/URLPatternMacro.swift index 566e020..aa7d685 100644 --- a/Sources/URLPatternMacros/URLPatternMacro.swift +++ b/Sources/URLPatternMacros/URLPatternMacro.swift @@ -14,16 +14,31 @@ public struct URLPatternMacro: MemberMacro { 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 urlPattern = Self.\(raw: caseName)(url) { + for caseName in cases { + """ + if let urlPattern = Self.\(raw: caseName)(url) { self = urlPattern return - } - """ } + """ } """ @@ -33,19 +48,19 @@ 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 Self.isURLPathParam(pattern) else { return input == pattern } - - return true - } + return zip(inputs, patterns).allSatisfy { input, pattern in + guard Self.isURLPathParam(pattern) else { return input == pattern } + + return true + } } """) let isURLPathParamMethod = try FunctionDeclSyntax(""" static func isURLPathParam(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") + return string.hasPrefix("{") && string.hasSuffix("}") } """) diff --git a/Tests/URLPatternTests/URLPatternTests.swift b/Tests/URLPatternTests/URLPatternTests.swift index a0ae823..080f712 100644 --- a/Tests/URLPatternTests/URLPatternTests.swift +++ b/Tests/URLPatternTests/URLPatternTests.swift @@ -4,98 +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: Int) - } - """, - expandedSource: """ - enum Deeplink { - case post(id: 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 + } - static func post(_ url: URL) -> Self? { - let inputPaths = url.pathComponents - let patternPaths = ["/", "post", "{id}"] + return .home + } - guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { - return nil - } + case post(postId: String) - let id = inputPaths[2] + static func post(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "posts", "{postId}"] - return .post(id: id) + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil } + let postId = inputPaths[2] + return .post(postId: postId) + } - case name(id: String, name: String) + case postComment(postId: String, commentId: String) - static func name(_ url: URL) -> Self? { - let inputPaths = url.pathComponents - let patternPaths = ["/", "post", "{id}", "{name}"] + 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 - } + guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { + return nil + } + let postId = inputPaths[2] + let commentId = inputPaths[4] + return .postComment(postId: postId, commentId: commentId) + } - let id = inputPaths[2] - let name = inputPaths[3] + case complex(aNum: Int, bNum: Int, cNum: Int) - return .name(id: id, name: name) - } + static func complex(_ url: URL) -> Self? { + let inputPaths = url.pathComponents + let patternPaths = ["/", "c", "{cNum}", "b", "{bNum}", "a", "{aNum}"] - init?(url: URL) { - if let result = Self.post(url) { - self = result - return - } - if let result = Self.name(url) { - self = result - return - } + 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) + } - static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool { - guard inputs.count == patterns.count else { - return false - } + 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 zip(inputs, patterns).allSatisfy { input, pattern in - guard pattern.isURLPathParam else { + 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.isURLPathParam(pattern) else { return input == pattern } return true - } } + } + + static func isURLPathParam(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") + } + } + """, + 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) - static func isURLPathParam(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") + 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.isURLPathParam(pattern) else { + return input == pattern + } - } + return true + } + } + + static func isURLPathParam(_ string: String) -> Bool { + return string.hasPrefix("{") && string.hasSuffix("}") + } + } + """, + macros: testMacros + ) + } } diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj index f3f2566..ae0ed86 100644 --- a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj @@ -21,19 +21,11 @@ remoteGlobalIDString = EF43E6182D9ED86000809F76; remoteInfo = URLPatternExample; }; - EF43E6342D9ED86100809F76 /* 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; }; - EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLPatternExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -68,13 +60,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - EF43E6302D9ED86100809F76 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -92,7 +77,6 @@ children = ( EF43E6192D9ED86000809F76 /* URLPatternExample.app */, EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */, - EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */, ); name = Products; sourceTree = ""; @@ -149,26 +133,6 @@ productReference = EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - EF43E6322D9ED86100809F76 /* URLPatternExampleUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = EF43E6432D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleUITests" */; - buildPhases = ( - EF43E62F2D9ED86100809F76 /* Sources */, - EF43E6302D9ED86100809F76 /* Frameworks */, - EF43E6312D9ED86100809F76 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - EF43E6352D9ED86100809F76 /* PBXTargetDependency */, - ); - name = URLPatternExampleUITests; - packageProductDependencies = ( - ); - productName = URLPatternExampleUITests; - productReference = EF43E6332D9ED86100809F76 /* URLPatternExampleUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -186,10 +150,6 @@ CreatedOnToolsVersion = 16.2; TestTargetID = EF43E6182D9ED86000809F76; }; - EF43E6322D9ED86100809F76 = { - CreatedOnToolsVersion = 16.2; - TestTargetID = EF43E6182D9ED86000809F76; - }; }; }; buildConfigurationList = EF43E6142D9ED86000809F76 /* Build configuration list for PBXProject "URLPatternExample" */; @@ -211,7 +171,6 @@ targets = ( EF43E6182D9ED86000809F76 /* URLPatternExample */, EF43E6282D9ED86100809F76 /* URLPatternExampleTests */, - EF43E6322D9ED86100809F76 /* URLPatternExampleUITests */, ); }; /* End PBXProject section */ @@ -231,13 +190,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - EF43E6312D9ED86100809F76 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -255,13 +207,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - EF43E62F2D9ED86100809F76 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -270,11 +215,6 @@ target = EF43E6182D9ED86000809F76 /* URLPatternExample */; targetProxy = EF43E62A2D9ED86100809F76 /* PBXContainerItemProxy */; }; - EF43E6352D9ED86100809F76 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = EF43E6182D9ED86000809F76 /* URLPatternExample */; - targetProxy = EF43E6342D9ED86100809F76 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -465,7 +405,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HK54HM2TC6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -484,7 +424,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HK54HM2TC6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -495,40 +435,6 @@ }; name = Release; }; - EF43E6442D9ED86100809F76 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = HK54HM2TC6; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = URLPatternExample; - }; - name = Debug; - }; - EF43E6452D9ED86100809F76 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = HK54HM2TC6; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.heoblitz.URLPatternExampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = URLPatternExample; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -559,15 +465,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - EF43E6432D9ED86100809F76 /* Build configuration list for PBXNativeTarget "URLPatternExampleUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EF43E6442D9ED86100809F76 /* Debug */, - EF43E6452D9ED86100809F76 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/URLPatternExample/URLPatternExample/ContentView.swift b/URLPatternExample/URLPatternExample/ContentView.swift deleted file mode 100644 index 72a475f..0000000 --- a/URLPatternExample/URLPatternExample/ContentView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// ContentView.swift -// URLPatternExample -// -// Created by woody on 4/3/25. -// - -import SwiftUI -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) -} - -enum Route: Hashable { - case home - case post(postId: String) - case comment(commentId: String) - case setting(number: Int) -} - -struct ContentView: 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("Wrong DeepLink Example") { - 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) { deepLink in - switch deepLink { - case .home: - Home() - case .post(let postId): - Post(postId: postId) - case .comment(let commentId): - Comment(commentId: commentId) - case .setting(let number): - Setting(number: number) - } - } - } - } -} - -struct Home: View { - var body: some View { - Text("🏠") - .font(.largeTitle) - .navigationTitle("Home") - } -} - -struct Post: View { - let postId: String - - var body: some View { - Text("📮 postId: \(postId)") - .font(.largeTitle) - .navigationTitle("Post") - } -} - -struct Comment: View { - let commentId: String - - var body: some View { - Text("💬 commentId: \(commentId)") - .font(.largeTitle) - .navigationTitle("Comment") - } -} - -struct Setting: View { - let number: Int - - var body: some View { - Text("⚙️ number: \(number)") - .font(.largeTitle) - .navigationTitle("Setting") - } -} - -#Preview { - ContentView() -} 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/Mock/DeepLinkMock.swift b/URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift new file mode 100644 index 0000000..43f5476 --- /dev/null +++ b/URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift @@ -0,0 +1,35 @@ +// +// DeepLinkMock.swift +// URLPatternExample +// +// Created by woody on 4/4/25. +// + +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/{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) +} 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 index c85774e..eafa142 100644 --- a/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift +++ b/URLPatternExample/URLPatternExample/URLPatternExampleApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct URLPatternExampleApp: App { var body: some Scene { WindowGroup { - ContentView() + 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 index 1c7c992..4cd91b6 100644 --- a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -10,21 +10,182 @@ import Foundation @testable import URLPatternExample struct URLPatternExampleTests { - @Test func testSingleValue() async throws { - let url = try #require(URL(string: "https://www.example.com/home")) - let deepLink = try #require(DeepLink(url: url)) + // 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 func testWrongPathSingleValue() async throws { - let url = try #require(URL(string: "https://www.example.com/homes")) - let deepLink = DeepLink(url: url) + @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) } - @Test func testWrongManyPathSingleValue() async throws { - let url = try #require(URL(string: "https://www.example.com/home/path")) - let deepLink = DeepLink(url: url) + // 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/42", + "/setting/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", + "/setting/12.34", + "/setting/" + ]) + 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) } } From 79a94f7fe8dbcd5a3b1a8b0956d48847bb95ddea Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 17:54:50 +0900 Subject: [PATCH 04/10] Update github action --- .github/workflows/swift.yml | 3 +++ 1 file changed, 3 insertions(+) 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 From 8b11e446fec3d3dbde6de83c268e9f2c635b0183 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 18:51:16 +0900 Subject: [PATCH 05/10] Remove host in URLPatternExampleTests --- .../project.pbxproj | 32 +++++++++++++---- .../URLPatternExample/Mock/DeepLinkMock.swift | 35 ------------------- .../URLPatternExampleTests.swift | 30 ++++++++++++++-- 3 files changed, 53 insertions(+), 44 deletions(-) delete mode 100644 URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift diff --git a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj index ae0ed86..134908b 100644 --- a/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj +++ b/URLPatternExample/URLPatternExample.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */ @@ -46,6 +48,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EF43E72D2D9FE2C300809F76 /* URLPattern in Frameworks */, EF43E6482D9ED87500809F76 /* URLPattern in Frameworks */, EF43E64D2D9ED8A800809F76 /* URLPatternClient in Frameworks */, EF43E64B2D9ED8A800809F76 /* URLPattern in Frameworks */, @@ -57,6 +60,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EF43E7302D9FE2CC00809F76 /* URLPattern in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -68,6 +72,7 @@ children = ( EF43E61B2D9ED86000809F76 /* URLPatternExample */, EF43E62C2D9ED86100809F76 /* URLPatternExampleTests */, + EF43E72E2D9FE2CC00809F76 /* Frameworks */, EF43E61A2D9ED86000809F76 /* Products */, ); sourceTree = ""; @@ -81,6 +86,13 @@ name = Products; sourceTree = ""; }; + EF43E72E2D9FE2CC00809F76 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,6 +117,7 @@ EF43E64A2D9ED8A800809F76 /* URLPattern */, EF43E64C2D9ED8A800809F76 /* URLPatternClient */, EF43E64F2D9ED93E00809F76 /* URLPattern */, + EF43E72C2D9FE2C300809F76 /* URLPattern */, ); productName = URLPatternExample; productReference = EF43E6192D9ED86000809F76 /* URLPatternExample.app */; @@ -128,6 +141,7 @@ ); name = URLPatternExampleTests; packageProductDependencies = ( + EF43E72F2D9FE2CC00809F76 /* URLPattern */, ); productName = URLPatternExampleTests; productReference = EF43E6292D9ED86100809F76 /* URLPatternExampleTests.xctest */; @@ -148,7 +162,6 @@ }; EF43E6282D9ED86100809F76 = { CreatedOnToolsVersion = 16.2; - TestTargetID = EF43E6182D9ED86000809F76; }; }; }; @@ -162,7 +175,7 @@ mainGroup = EF43E6102D9ED86000809F76; minimizedProjectReferenceProxies = 1; packageReferences = ( - EF43E64E2D9ED93E00809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */, + EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */, ); preferredProjectObjectVersion = 77; productRefGroup = EF43E61A2D9ED86000809F76 /* Products */; @@ -400,7 +413,6 @@ EF43E6412D9ED86100809F76 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HK54HM2TC6; @@ -412,14 +424,12 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLPatternExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLPatternExample"; }; name = Debug; }; EF43E6422D9ED86100809F76 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HK54HM2TC6; @@ -431,7 +441,6 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLPatternExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLPatternExample"; }; name = Release; }; @@ -468,7 +477,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - EF43E64E2D9ED93E00809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */ = { + EF43E72B2D9FE2C300809F76 /* XCLocalSwiftPackageReference "../../URLPattern" */ = { isa = XCLocalSwiftPackageReference; relativePath = ../../URLPattern; }; @@ -491,6 +500,15 @@ 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/Mock/DeepLinkMock.swift b/URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift deleted file mode 100644 index 43f5476..0000000 --- a/URLPatternExample/URLPatternExample/Mock/DeepLinkMock.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// DeepLinkMock.swift -// URLPatternExample -// -// Created by woody on 4/4/25. -// - -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/{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) -} diff --git a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift index 4cd91b6..2b04215 100644 --- a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -6,8 +6,34 @@ // import Testing -import Foundation -@testable import URLPatternExample +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/{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 From 9100025592b4ea856aa51fa006d6e46c21cccb15 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 18:53:33 +0900 Subject: [PATCH 06/10] Add test xcscheme --- .../xcschemes/URLPatternExample.xcscheme | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 URLPatternExample/URLPatternExample.xcodeproj/xcshareddata/xcschemes/URLPatternExample.xcscheme 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c398db81aed88c09f040a57d58e44594ee38f764 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 19:58:52 +0900 Subject: [PATCH 07/10] Update readme --- README.md | 105 +++++++++++++++++- .../Extensions/String+Extensions.swift | 2 +- Sources/URLPatternMacros/URLPathMacro.swift | 2 +- .../URLPatternMacros/URLPatternMacro.swift | 8 +- Tests/URLPatternTests/URLPatternTests.swift | 8 +- 5 files changed, 114 insertions(+), 11 deletions(-) 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/URLPatternMacros/Extensions/String+Extensions.swift b/Sources/URLPatternMacros/Extensions/String+Extensions.swift index 8b558f7..3817296 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("}") } } diff --git a/Sources/URLPatternMacros/URLPathMacro.swift b/Sources/URLPatternMacros/URLPathMacro.swift index 659cb47..7201a1c 100644 --- a/Sources/URLPatternMacros/URLPathMacro.swift +++ b/Sources/URLPatternMacros/URLPathMacro.swift @@ -55,7 +55,7 @@ public struct URLPathMacro: PeerMacro { } ?? [] let patternParams: [PatternParam] = try patternPaths.enumerated() - .filter { index, value in value.isURLPathParam } + .filter { index, value in value.isURLPathValue } .map { pathIndex, value -> PatternParam in let name = String(value.dropFirst().dropLast()) diff --git a/Sources/URLPatternMacros/URLPatternMacro.swift b/Sources/URLPatternMacros/URLPatternMacro.swift index aa7d685..c51d1a3 100644 --- a/Sources/URLPatternMacros/URLPatternMacro.swift +++ b/Sources/URLPatternMacros/URLPatternMacro.swift @@ -51,15 +51,15 @@ public struct URLPatternMacro: MemberMacro { guard inputs.count == patterns.count else { return false } return zip(inputs, patterns).allSatisfy { input, pattern in - guard Self.isURLPathParam(pattern) else { return input == pattern } + guard Self.isURLPathValue(pattern) else { return input == pattern } return true } } """) - let isURLPathParamMethod = try FunctionDeclSyntax(""" - static func isURLPathParam(_ string: String) -> Bool { + let isURLPathValueMethod = try FunctionDeclSyntax(""" + static func isURLPathValue(_ string: String) -> Bool { return string.hasPrefix("{") && string.hasSuffix("}") } """) @@ -67,7 +67,7 @@ public struct URLPatternMacro: MemberMacro { return [ DeclSyntax(urlInitializer), DeclSyntax(isValidURLPathsMethod), - DeclSyntax(isURLPathParamMethod) + DeclSyntax(isURLPathValueMethod) ] } } diff --git a/Tests/URLPatternTests/URLPatternTests.swift b/Tests/URLPatternTests/URLPatternTests.swift index 080f712..8bf7b06 100644 --- a/Tests/URLPatternTests/URLPatternTests.swift +++ b/Tests/URLPatternTests/URLPatternTests.swift @@ -121,7 +121,7 @@ final class URLPatternTests: XCTestCase { } return zip(inputs, patterns).allSatisfy { input, pattern in - guard Self.isURLPathParam(pattern) else { + guard Self.isURLPathValue(pattern) else { return input == pattern } @@ -129,7 +129,7 @@ final class URLPatternTests: XCTestCase { } } - static func isURLPathParam(_ string: String) -> Bool { + static func isURLPathValue(_ string: String) -> Bool { return string.hasPrefix("{") && string.hasSuffix("}") } } @@ -180,7 +180,7 @@ final class URLPatternTests: XCTestCase { } return zip(inputs, patterns).allSatisfy { input, pattern in - guard Self.isURLPathParam(pattern) else { + guard Self.isURLPathValue(pattern) else { return input == pattern } @@ -188,7 +188,7 @@ final class URLPatternTests: XCTestCase { } } - static func isURLPathParam(_ string: String) -> Bool { + static func isURLPathValue(_ string: String) -> Bool { return string.hasPrefix("{") && string.hasSuffix("}") } } From 4a10ead901b11ab6ecba4b31b3a0a85f05b0a04a Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 22:57:48 +0900 Subject: [PATCH 08/10] Add test cases --- .../Extensions/String+Extensions.swift | 2 +- .../URLPatternMacros/URLPatternMacro.swift | 2 +- .../URLPatternExampleTests.swift | 85 +++++++++++++++++-- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/Sources/URLPatternMacros/Extensions/String+Extensions.swift b/Sources/URLPatternMacros/Extensions/String+Extensions.swift index 3817296..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 isURLPathValue: Bool { self.hasPrefix("{") && self.hasSuffix("}") } + var isURLPathValue: Bool { self.hasPrefix("{") && self.hasSuffix("}") && self.utf16.count >= 3 } } diff --git a/Sources/URLPatternMacros/URLPatternMacro.swift b/Sources/URLPatternMacros/URLPatternMacro.swift index c51d1a3..70a4690 100644 --- a/Sources/URLPatternMacros/URLPatternMacro.swift +++ b/Sources/URLPatternMacros/URLPatternMacro.swift @@ -60,7 +60,7 @@ public struct URLPatternMacro: MemberMacro { let isURLPathValueMethod = try FunctionDeclSyntax(""" static func isURLPathValue(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 } """) diff --git a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift index 2b04215..2463d34 100644 --- a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -19,7 +19,7 @@ enum DeepLinkMock: Equatable { @URLPath("/posts/{postId}/comments/{commentId}") case postComment(postId: String, commentId: String) - @URLPath("/setting/{number}") + @URLPath("/setting/phone/{number}") case setting(number: Int) @URLPath("/int/{int}") @@ -104,8 +104,8 @@ struct URLPatternExampleTests { // MARK: - Setting Tests @Test("Valid Setting URLPatterns", arguments: [ - "https://domain.com/setting/42", - "/setting/42" + "https://domain.com/setting/phone/42", + "/setting/phone/42" ]) func parseSetting_success(urlString: String) async throws { let url = try #require(URL(string: urlString)) @@ -114,9 +114,9 @@ struct URLPatternExampleTests { } @Test("Invalid Setting URLPatterns", arguments: [ - "https://domain.com/setting/abc", - "/setting/12.34", - "/setting/" + "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)) @@ -214,4 +214,77 @@ struct URLPatternExampleTests { 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: - Unicode Tests + @URLPattern + enum PriorityTest: Equatable { + @URLPath("/{a}/{b}") + case all(a: String, b: String) + + @URLPath("/post/{postId}") + case post(postId: Int) + } + + @Test func checkPriorityCases() async throws { + let url = try #require(URL(string: "/post/1")) + #expect(PriorityTest(url: url) == .all(a: "post", b: "1")) + } + + @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) + } + } } From dd6bbf96213e418b3fe9e2a4d4811dceeb6db5d1 Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 23:00:06 +0900 Subject: [PATCH 09/10] Fix test error --- Tests/URLPatternTests/URLPatternTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/URLPatternTests/URLPatternTests.swift b/Tests/URLPatternTests/URLPatternTests.swift index 8bf7b06..d3be924 100644 --- a/Tests/URLPatternTests/URLPatternTests.swift +++ b/Tests/URLPatternTests/URLPatternTests.swift @@ -130,7 +130,7 @@ final class URLPatternTests: XCTestCase { } static func isURLPathValue(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 } } """, @@ -189,7 +189,7 @@ final class URLPatternTests: XCTestCase { } static func isURLPathValue(_ string: String) -> Bool { - return string.hasPrefix("{") && string.hasSuffix("}") + return string.hasPrefix("{") && string.hasSuffix("}") && string.utf16.count >= 3 } } """, From 093e598076bde99a4b98b0884c8394cd0f95a69a Mon Sep 17 00:00:00 2001 From: heoblitz Date: Fri, 4 Apr 2025 23:08:24 +0900 Subject: [PATCH 10/10] Edit markups --- .../URLPatternExampleTests/URLPatternExampleTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift index 2463d34..d4814e2 100644 --- a/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift +++ b/URLPatternExample/URLPatternExampleTests/URLPatternExampleTests.swift @@ -221,13 +221,14 @@ struct URLPatternExampleTests { "こんにちは", "☺️👍" ]) + 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: - Unicode Tests + // MARK: - Priority Tests @URLPattern enum PriorityTest: Equatable { @URLPath("/{a}/{b}") @@ -237,11 +238,13 @@ struct URLPatternExampleTests { case post(postId: Int) } - @Test func checkPriorityCases() async throws { + @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("/") @@ -277,6 +280,7 @@ struct URLPatternExampleTests { @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..