Skip to content

Commit 6af1b55

Browse files
committed
Edit parse rule and Add Tests
1 parent 1c24ad4 commit 6af1b55

File tree

18 files changed

+645
-430
lines changed

18 files changed

+645
-430
lines changed

Package.swift

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,41 @@
1-
// swift-tools-version: 6.0
1+
// swift-tools-version: 5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55
import CompilerPluginSupport
66

77
let package = Package(
8-
name: "URLPattern",
9-
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10-
products: [
11-
// Products define the executables and libraries a package produces, making them visible to other packages.
12-
.library(
13-
name: "URLPattern",
14-
targets: ["URLPattern"]
15-
),
16-
.executable(
17-
name: "URLPatternClient",
18-
targets: ["URLPatternClient"]
19-
),
20-
],
21-
dependencies: [
22-
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"),
23-
],
24-
targets: [
25-
// Targets are the basic building blocks of a package, defining a module or a test suite.
26-
// Targets can depend on other targets in this package and products from dependencies.
27-
// Macro implementation that performs the source transformation of a macro.
28-
.macro(
29-
name: "URLPatternMacros",
30-
dependencies: [
31-
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
32-
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
33-
]
34-
),
35-
36-
// Library that exposes a macro as part of its API, which is used in client programs.
37-
.target(name: "URLPattern", dependencies: ["URLPatternMacros"]),
38-
39-
// A client of the library, which is able to use the macro in its own code.
40-
.executableTarget(name: "URLPatternClient", dependencies: ["URLPattern"]),
41-
42-
// A test target used to develop the macro implementation.
43-
.testTarget(
44-
name: "URLPatternTests",
45-
dependencies: [
46-
"URLPatternMacros",
47-
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
48-
]
49-
),
50-
]
8+
name: "URLPattern",
9+
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10+
products: [
11+
.library(
12+
name: "URLPattern",
13+
targets: ["URLPattern"]
14+
),
15+
.executable(
16+
name: "URLPatternClient",
17+
targets: ["URLPatternClient"]
18+
),
19+
],
20+
dependencies: [
21+
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"),
22+
],
23+
targets: [
24+
.macro(
25+
name: "URLPatternMacros",
26+
dependencies: [
27+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
28+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
29+
]
30+
),
31+
.target(name: "URLPattern", dependencies: ["URLPatternMacros"]),
32+
.executableTarget(name: "URLPatternClient", dependencies: ["URLPattern"]),
33+
.testTarget(
34+
name: "URLPatternTests",
35+
dependencies: [
36+
"URLPatternMacros",
37+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
38+
]
39+
),
40+
]
5141
)

Sources/URLPattern/URLPattern.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@_exported import Foundation
2+
13
@attached(member, names: arbitrary)
24
public macro URLPattern() = #externalMacro(module: "URLPatternMacros", type: "URLPatternMacro")
35

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1 @@
11
import URLPattern
2-
import Foundation
3-
4-
@URLPattern
5-
enum Deeplink {
6-
@URLPath("/post/{id}/{name}")
7-
case name(id: Int, name: String)
8-
9-
@URLPath("/post/{id}/{name}/hi/{good}")
10-
case nameDetail(id: String, name: String, good: String)
11-
12-
@URLPath("/post/{id}")
13-
case nameDetailHI(id: String)
14-
15-
@URLPath("/home")
16-
case home
17-
}
18-
19-
let url1 = URL(string: "https://channel.io/post/12/12")
20-
let url2 = URL(string: "/post/hi/hello/hi/bye")
21-
let url3 = URL(string: "/home")
22-
23-
// enumPath
24-
// inputPath
25-
26-
27-
print(url1?.pathComponents)
28-
print(url2?.pathComponents)
29-
30-
31-
let hi = URL(string: "https://post/{id}")
32-
let paths = url1!.pathComponents
33-
34-
print(Deeplink(url: url1!))
35-
print(Deeplink(url: url2!))
36-
print(Deeplink(url: url3!))
37-

Sources/URLPatternMacros/URLPathMacro.swift

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public struct URLPathMacro: PeerMacro {
1616
let name: String
1717
let type: SupportedType
1818
let pathIndex: Int
19+
let caseIndex: Int
1920
}
2021

2122
public static func expansion(
@@ -53,64 +54,64 @@ public struct URLPathMacro: PeerMacro {
5354
return (name: name, type: supportedType)
5455
} ?? []
5556

56-
let patternParams = patternPaths.enumerated()
57+
let patternParams: [PatternParam] = try patternPaths.enumerated()
5758
.filter { index, value in value.isURLPathParam }
58-
.map { index, value in
59+
.map { pathIndex, value -> PatternParam in
5960
let name = String(value.dropFirst().dropLast())
61+
62+
guard let (caseIndex, caseAssociatedType) = caseAssociatedTypes.enumerated().first(where: { name == $0.element.name }) else {
63+
throw URLPatternError("URLPath value \"\(name)\" cannot be found in the associated value")
64+
}
65+
6066
return PatternParam(
61-
name:name ,
62-
type: caseAssociatedTypes.first(where: { name == $0.name })!.type,
63-
pathIndex: index
67+
name: name,
68+
type: caseAssociatedType.type,
69+
pathIndex: pathIndex,
70+
caseIndex: caseIndex
6471
)
6572
}
73+
.sorted(by: { $0.caseIndex < $1.caseIndex })
74+
75+
let patternNames = Set(patternParams.map(\.name))
76+
let caseNames = Set(caseAssociatedTypes.map(\.name))
77+
78+
guard patternNames.count == patternParams.count else {
79+
throw URLPatternError("The name of an URLPath value cannot be duplicated")
80+
}
6681

67-
if Set(patternParams.map { $0.name }).count != patternParams.count {
82+
guard caseNames.count == caseAssociatedTypes.count else {
6883
throw URLPatternError("The name of an associated value cannot be duplicated")
6984
}
7085

71-
if Set(patternParams).count != caseAssociatedTypes.count {
86+
guard patternNames.count == caseNames.count else {
7287
throw URLPatternError("The number of associated values does not match URLPath")
7388
}
7489

90+
guard patternNames == caseNames else {
91+
throw URLPatternError("The name of the URLPath value does not match the associated value")
92+
}
93+
7594
let staticMethod = try FunctionDeclSyntax("""
7695
static func \(element.name)(_ url: URL) -> Self? {
77-
let inputPaths = url.pathComponents
78-
let patternPaths = \(raw: patternPaths)
79-
80-
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
81-
return nil
82-
}
83-
84-
\(raw: patternParams.map { param in
96+
let inputPaths = url.pathComponents
97+
let patternPaths = \(raw: patternPaths)
98+
99+
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil }
100+
\(raw: patternParams.map { param in
85101
switch param.type {
86-
case .Double:
102+
case .Double, .Float, .Int:
87103
"""
88-
guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else {
89-
return nil
90-
}
91-
"""
92-
case .Float:
93-
"""
94-
guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else {
95-
return nil
96-
}
97-
"""
98-
case .Int:
99-
"""
100-
guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else {
101-
return nil
102-
}
104+
guard let \(param.name) = \(param.type.rawValue)(inputPaths[\(param.pathIndex)]) else { return nil }
103105
"""
104106
case .String:
105107
"""
106108
let \(param.name) = inputPaths[\(param.pathIndex)]
107109
"""
108110
}
109111
}.joined(separator: "\n"))
110-
111-
return \(raw: patternParams.isEmpty
112-
? ".\(element.name.text)"
113-
: ".\(element.name.text)(\(patternParams.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))")
112+
return \(raw: patternParams.isEmpty
113+
? ".\(element.name.text)"
114+
: ".\(element.name.text)(\(patternParams.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))")
114115
}
115116
""")
116117

Sources/URLPatternMacros/URLPatternMacro.swift

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,31 @@ public struct URLPatternMacro: MemberMacro {
1414
throw URLPatternError("@URLPatternMacro can only be applied to enums")
1515
}
1616

17+
let cases = enumDecl.memberBlock.members.compactMap { member -> String? in
18+
guard
19+
let caseDecl = member.decl.as(EnumCaseDeclSyntax.self),
20+
caseDecl.attributes.contains(where: {
21+
$0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "URLPath"
22+
})
23+
else {
24+
return nil
25+
}
26+
27+
return caseDecl.elements.first?.name.text
28+
}
29+
30+
guard Set(cases).count == cases.count else {
31+
throw URLPatternError("Duplicate case names are not allowed")
32+
}
33+
1734
let urlInitializer = try InitializerDeclSyntax("init?(url: URL)") {
18-
for caseDecl in enumDecl.memberBlock.members.compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) }) {
19-
if let caseName = caseDecl.elements.first?.name.text {
20-
"""
21-
if let urlPattern = Self.\(raw: caseName)(url) {
35+
for caseName in cases {
36+
"""
37+
if let urlPattern = Self.\(raw: caseName)(url) {
2238
self = urlPattern
2339
return
24-
}
25-
"""
2640
}
41+
"""
2742
}
2843

2944
"""
@@ -33,19 +48,19 @@ public struct URLPatternMacro: MemberMacro {
3348

3449
let isValidURLPathsMethod = try FunctionDeclSyntax("""
3550
static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
36-
guard inputs.count == patterns.count else { return false }
51+
guard inputs.count == patterns.count else { return false }
3752
38-
return zip(inputs, patterns).allSatisfy { input, pattern in
39-
guard Self.isURLPathParam(pattern) else { return input == pattern }
40-
41-
return true
42-
}
53+
return zip(inputs, patterns).allSatisfy { input, pattern in
54+
guard Self.isURLPathParam(pattern) else { return input == pattern }
55+
56+
return true
57+
}
4358
}
4459
""")
4560

4661
let isURLPathParamMethod = try FunctionDeclSyntax("""
4762
static func isURLPathParam(_ string: String) -> Bool {
48-
return string.hasPrefix("{") && string.hasSuffix("}")
63+
return string.hasPrefix("{") && string.hasSuffix("}")
4964
}
5065
""")
5166

0 commit comments

Comments
 (0)