Skip to content

Commit 4debd21

Browse files
committed
Add mvp features
1 parent f1a7dd2 commit 4debd21

5 files changed

Lines changed: 135 additions & 69 deletions

File tree

Sources/URLPatternClient/main.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import Foundation
33

44
@URLPattern
55
enum Deeplink {
6-
// @URLPath("/post/{id}")
7-
// case post(id: String)
8-
//
9-
@URLPath("/home/{id}/{name}")
10-
case name(id: String, name: String)
6+
@URLPath("/post/{id}/{name}")
7+
case name(id: Int, name: String)
118

129
@URLPath("/post/{id}/{name}/hi/{good}")
1310
case nameDetail(id: String, name: String, good: String)

Sources/URLPatternMacros/URLPathMacro.swift

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import SwiftSyntaxMacros
55
import Foundation
66

77
public struct URLPathMacro: PeerMacro {
8-
struct CaseParam: Hashable {
9-
let index: Int
10-
let name: String
8+
enum SupportedType: String {
9+
case String
10+
case Int
11+
case Double
12+
case Float
1113
}
1214

13-
struct PatternPathItem {
14-
15+
struct PatternParam: Hashable {
16+
let name: String
17+
let type: SupportedType
18+
let pathIndex: Int
1519
}
1620

1721
public static func expansion(
@@ -23,48 +27,90 @@ public struct URLPathMacro: PeerMacro {
2327
let enumCase = declaration.as(EnumCaseDeclSyntax.self),
2428
let element = enumCase.elements.first
2529
else {
26-
throw MacroError.message("URLPatternPath macro can only be applied to enum cases")
30+
throw URLPatternError("@URLPathMacro can only be applied to enum cases")
2731
}
28-
32+
2933
guard
3034
let argument = node.arguments?.as(LabeledExprListSyntax.self)?.first,
3135
let pathString = argument.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description
3236
else {
33-
throw MacroError.message("Invalid path")
37+
throw URLPatternError("URLPath is nil")
3438
}
3539

3640
guard let pathURL = URL(string: pathString) else {
37-
throw MacroError.message("URLPatternPath macro requires a string literal path")
41+
throw URLPatternError("URLPath is not in a valid URL format")
3842
}
3943

4044
let patternPaths = pathURL.pathComponents
41-
42-
let pathComponents = pathURL.pathComponents
43-
let parameters = pathComponents.enumerated()
45+
46+
let caseAssociatedTypes = try element.parameterClause?.parameters.map { param -> (name: String, type: SupportedType) in
47+
let name = param.firstName?.text ?? ""
48+
let type = param.type.description
49+
50+
guard let supportedType = SupportedType(rawValue: type) else {
51+
throw URLPatternError("\(type) is not supported as an associated value")
52+
}
53+
return (name: name, type: supportedType)
54+
} ?? []
55+
56+
let patternParams = patternPaths.enumerated()
4457
.filter { index, value in value.isURLPathParam }
45-
.map { CaseParam(index: $0.offset, name: String($0.element.dropFirst().dropLast())) }
58+
.map { index, value in
59+
let name = String(value.dropFirst().dropLast())
60+
return PatternParam(
61+
name:name ,
62+
type: caseAssociatedTypes.first(where: { name == $0.name })!.type,
63+
pathIndex: index
64+
)
65+
}
4666

47-
if Set(parameters).count != parameters.count {
48-
throw MacroError.message("변수 이름은 중복되서는 안됩니다.")
67+
if Set(patternParams.map { $0.name }).count != patternParams.count {
68+
throw URLPatternError("The name of an associated value cannot be duplicated")
4969
}
50-
70+
71+
if Set(patternParams).count != caseAssociatedTypes.count {
72+
throw URLPatternError("The number of associated values does not match URLPath")
73+
}
74+
5175
let staticMethod = try FunctionDeclSyntax("""
5276
static func \(element.name)(_ url: URL) -> Self? {
53-
let inputPaths = url.pathComponents
54-
let patternPaths = \(raw: patternPaths)
55-
56-
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else { return nil }
77+
let inputPaths = url.pathComponents
78+
let patternPaths = \(raw: patternPaths)
5779
58-
\(raw: parameters.map { param in
59-
"""
60-
let \(param.name) = inputPaths[\(param.index)]
61-
"""
80+
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
81+
return nil
82+
}
83+
84+
\(raw: patternParams.map { param in
85+
switch param.type {
86+
case .Double:
87+
"""
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+
}
103+
"""
104+
case .String:
105+
"""
106+
let \(param.name) = inputPaths[\(param.pathIndex)]
107+
"""
108+
}
62109
}.joined(separator: "\n"))
63110
64-
return .\(raw: element.name.text)(\(raw: parameters.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))
111+
return .\(raw: element.name.text)(\(raw: patternParams.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))
65112
}
66-
"""
67-
)
113+
""")
68114

69115
return [DeclSyntax(staticMethod)]
70116
}

Sources/URLPatternMacros/URLPatternMacro.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,23 @@ import SwiftSyntaxBuilder
44
import SwiftSyntaxMacros
55
import Foundation
66

7-
enum MacroError: Error {
8-
case message(String)
9-
}
10-
117
public struct URLPatternMacro: MemberMacro {
128
public static func expansion(
139
of node: AttributeSyntax,
1410
providingMembersOf declaration: some DeclGroupSyntax,
1511
in context: some MacroExpansionContext
1612
) throws -> [DeclSyntax] {
1713
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
18-
throw MacroError.message("This macro can only be applied to enums")
14+
throw URLPatternError("@URLPatternMacro can only be applied to enums")
1915
}
2016

2117
let urlInitializer = try InitializerDeclSyntax("init?(url: URL)") {
2218
for caseDecl in enumDecl.memberBlock.members.compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) }) {
2319
if let caseName = caseDecl.elements.first?.name.text {
2420
"""
25-
if let result = Self.\(raw: caseName)(url) {
26-
self = result
27-
return
21+
if let urlPattern = Self.\(raw: caseName)(url) {
22+
self = urlPattern
23+
return
2824
}
2925
"""
3026
}
@@ -40,7 +36,7 @@ public struct URLPatternMacro: MemberMacro {
4036
guard inputs.count == patterns.count else { return false }
4137
4238
return zip(inputs, patterns).allSatisfy { input, pattern in
43-
guard pattern.isURLPathParam else { return input == pattern }
39+
guard Self.isURLPathParam(pattern) else { return input == pattern }
4440
4541
return true
4642
}
@@ -49,10 +45,14 @@ public struct URLPatternMacro: MemberMacro {
4945

5046
let isURLPathParamMethod = try FunctionDeclSyntax("""
5147
static func isURLPathParam(_ string: String) -> Bool {
52-
return string.hasPrefix("{") && string.hasSuffix("}") }
48+
return string.hasPrefix("{") && string.hasSuffix("}")
5349
}
5450
""")
5551

56-
return [DeclSyntax(urlInitializer), DeclSyntax(isValidURLPathsMethod), DeclSyntax(isURLPathParamMethod)]
52+
return [
53+
DeclSyntax(urlInitializer),
54+
DeclSyntax(isValidURLPathsMethod),
55+
DeclSyntax(isURLPathParamMethod)
56+
]
5757
}
5858
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
struct URLPatternError: LocalizedError {
4+
let errorDescription: String
5+
6+
init(_ errorDescription: String) {
7+
self.errorDescription = errorDescription
8+
}
9+
}
10+
11+
extension URLPatternError: CustomStringConvertible {
12+
var description: String { self.errorDescription }
13+
}

Tests/URLPatternTests/URLPatternTests.swift

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ let testMacros: [String: Macro.Type] = [
1414
]
1515
#endif
1616

17-
18-
1917
final class URLPatternTests: XCTestCase {
2018
func testMacro() throws {
2119
assertMacroExpansion(
@@ -26,59 +24,71 @@ final class URLPatternTests: XCTestCase {
2624
case post(id: String)
2725
2826
@URLPath("/post/{id}/{name}")
29-
case name(id: String, name: String)
27+
case name(id: String, name: Int)
3028
}
3129
""",
3230
expandedSource: """
3331
enum Deeplink {
3432
case post(id: String)
3533
36-
static func createFromURLpost(_ url: URL) -> Self? {
37-
let path = url.path
38-
let components = path.split(separator: "/")
34+
static func post(_ url: URL) -> Self? {
35+
let inputPaths = url.pathComponents
36+
let patternPaths = ["/", "post", "{id}"]
3937
40-
guard components.count == 2 else {
38+
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
4139
return nil
4240
}
4341
44-
guard let id = components[1] as? String else {
45-
return nil
46-
}
42+
let id = inputPaths[2]
4743
4844
return .post(id: id)
4945
}
5046
5147
case name(id: String, name: String)
5248
53-
static func createFromURLname(_ url: URL) -> Self? {
54-
let path = url.path
55-
let components = path.split(separator: "/")
49+
static func name(_ url: URL) -> Self? {
50+
let inputPaths = url.pathComponents
51+
let patternPaths = ["/", "post", "{id}", "{name}"]
5652
57-
guard components.count == 3 else {
53+
guard isValidURLPaths(inputPaths: inputPaths, patternPaths: patternPaths) else {
5854
return nil
5955
}
6056
61-
guard let id = components[1] as? String else {
62-
return nil
63-
}
64-
guard let name = components[2] as? String else {
65-
return nil
66-
}
57+
let id = inputPaths[2]
58+
let name = inputPaths[3]
6759
6860
return .name(id: id, name: name)
6961
}
7062
7163
init?(url: URL) {
72-
if let result = Self.createFromURLpost(url) {
73-
self = result
74-
return
64+
if let result = Self.post(url) {
65+
self = result
66+
return
7567
}
76-
if let result = Self.createFromURLname(url) {
77-
self = result
78-
return
68+
if let result = Self.name(url) {
69+
self = result
70+
return
7971
}
8072
return nil
8173
}
74+
75+
static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
76+
guard inputs.count == patterns.count else {
77+
return false
78+
}
79+
80+
return zip(inputs, patterns).allSatisfy { input, pattern in
81+
guard pattern.isURLPathParam else {
82+
return input == pattern
83+
}
84+
85+
return true
86+
}
87+
}
88+
89+
static func isURLPathParam(_ string: String) -> Bool {
90+
return string.hasPrefix("{") && string.hasSuffix("}")
91+
}
8292
}
8393
""",
8494
macros: testMacros

0 commit comments

Comments
 (0)