Skip to content

Commit 756c21f

Browse files
committed
init project
1 parent af3400e commit 756c21f

10 files changed

Lines changed: 349 additions & 0 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ fastlane/report.xml
6060
fastlane/Preview.html
6161
fastlane/screenshots/**/*.png
6262
fastlane/test_output
63+
.DS_Store
64+
/.build
65+
/Packages
66+
xcuserdata/
67+
DerivedData/
68+
.swiftpm/configuration/registries.json
69+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
70+
.netrc

Package.resolved

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
import CompilerPluginSupport
6+
7+
let package = Package(
8+
name: "URLPattern",
9+
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10+
products: [
11+
// 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+
]
51+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@attached(member, names: arbitrary)
2+
public macro URLPattern() = #externalMacro(module: "URLPatternMacros", type: "URLPatternMacro")
3+
4+
@attached(peer, names: arbitrary)
5+
public macro URLPath(_ path: String) = #externalMacro(module: "URLPatternMacros", type: "URLPathMacro")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import URLPattern
2+
import Foundation
3+
4+
@URLPattern
5+
enum Deeplink {
6+
// @URLPath("/post/{id}")
7+
// case post(id: String)
8+
//
9+
@URLPath("/home/{id}/{name}")
10+
case name(id: String, name: String)
11+
12+
@URLPath("/post/{id}/{name}/hi/{good}")
13+
case nameDetail(id: String, name: String, good: String)
14+
15+
@URLPath("/post/{id}")
16+
case nameDetailHI(id: String)
17+
}
18+
19+
let url1 = URL(string: "https://channel.io/post/12/12")
20+
let url2 = URL(string: "/post/hi/hello/hi/bye")
21+
22+
// enumPath
23+
// inputPath
24+
25+
26+
print(url1?.pathComponents)
27+
print(url2?.pathComponents)
28+
29+
30+
let hi = URL(string: "https://post/{id}")
31+
let paths = url1!.pathComponents
32+
33+
print(Deeplink(url: url1!))
34+
print(Deeplink(url: url2!))
35+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
extension String {
4+
var isURLPathParam: Bool { self.hasPrefix("{") && self.hasSuffix("}") }
5+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
import Foundation
6+
7+
public struct URLPathMacro: PeerMacro {
8+
struct CaseParam: Hashable {
9+
let index: Int
10+
let name: String
11+
}
12+
13+
struct PatternPathItem {
14+
15+
}
16+
17+
public static func expansion(
18+
of node: AttributeSyntax,
19+
providingPeersOf declaration: some DeclSyntaxProtocol,
20+
in context: some MacroExpansionContext
21+
) throws -> [DeclSyntax] {
22+
guard
23+
let enumCase = declaration.as(EnumCaseDeclSyntax.self),
24+
let element = enumCase.elements.first
25+
else {
26+
throw MacroError.message("URLPatternPath macro can only be applied to enum cases")
27+
}
28+
29+
guard
30+
let argument = node.arguments?.as(LabeledExprListSyntax.self)?.first,
31+
let pathString = argument.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description
32+
else {
33+
throw MacroError.message("Invalid path")
34+
}
35+
36+
guard let pathURL = URL(string: pathString) else {
37+
throw MacroError.message("URLPatternPath macro requires a string literal path")
38+
}
39+
40+
let patternPaths = pathURL.pathComponents
41+
42+
let pathComponents = pathURL.pathComponents
43+
let parameters = pathComponents.enumerated()
44+
.filter { index, value in value.isURLPathParam }
45+
.map { CaseParam(index: $0.offset, name: String($0.element.dropFirst().dropLast())) }
46+
47+
if Set(parameters).count != parameters.count {
48+
throw MacroError.message("변수 이름은 중복되서는 안됩니다.")
49+
}
50+
51+
let staticMethod = try FunctionDeclSyntax("""
52+
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 }
57+
58+
\(raw: parameters.map { param in
59+
"""
60+
let \(param.name) = inputPaths[\(param.index)]
61+
"""
62+
}.joined(separator: "\n"))
63+
64+
return .\(raw: element.name.text)(\(raw: parameters.map { "\($0.name): \($0.name)" }.joined(separator: ", ")))
65+
}
66+
"""
67+
)
68+
69+
return [DeclSyntax(staticMethod)]
70+
}
71+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
import Foundation
6+
7+
enum MacroError: Error {
8+
case message(String)
9+
}
10+
11+
public struct URLPatternMacro: MemberMacro {
12+
public static func expansion(
13+
of node: AttributeSyntax,
14+
providingMembersOf declaration: some DeclGroupSyntax,
15+
in context: some MacroExpansionContext
16+
) throws -> [DeclSyntax] {
17+
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
18+
throw MacroError.message("This macro can only be applied to enums")
19+
}
20+
21+
let urlInitializer = try InitializerDeclSyntax("init?(url: URL)") {
22+
for caseDecl in enumDecl.memberBlock.members.compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) }) {
23+
if let caseName = caseDecl.elements.first?.name.text {
24+
"""
25+
if let result = Self.\(raw: caseName)(url) {
26+
self = result
27+
return
28+
}
29+
"""
30+
}
31+
}
32+
33+
"""
34+
return nil
35+
"""
36+
}
37+
38+
let isValidURLPathsMethod = try FunctionDeclSyntax("""
39+
static func isValidURLPaths(inputPaths inputs: [String], patternPaths patterns: [String]) -> Bool {
40+
guard inputs.count == patterns.count else { return false }
41+
42+
return zip(inputs, patterns).allSatisfy { input, pattern in
43+
guard pattern.isURLPathParam else { return input == pattern }
44+
45+
return true
46+
}
47+
}
48+
""")
49+
50+
let isURLPathParamMethod = try FunctionDeclSyntax("""
51+
static func isURLPathParam(_ string: String) -> Bool {
52+
return string.hasPrefix("{") && string.hasSuffix("}") }
53+
}
54+
""")
55+
56+
return [DeclSyntax(urlInitializer), DeclSyntax(isValidURLPathsMethod), DeclSyntax(isURLPathParamMethod)]
57+
}
58+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
import Foundation
6+
7+
@main
8+
struct URLPatternPlugin: CompilerPlugin {
9+
let providingMacros: [Macro.Type] = [URLPatternMacro.self, URLPathMacro.self]
10+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
import SwiftSyntaxMacros
4+
import SwiftSyntaxMacrosTestSupport
5+
import XCTest
6+
7+
// 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.
8+
#if canImport(URLPatternMacros)
9+
import URLPatternMacros
10+
11+
let testMacros: [String: Macro.Type] = [
12+
"URLPattern": URLPatternMacro.self,
13+
"URLPath": URLPathMacro.self
14+
]
15+
#endif
16+
17+
18+
19+
final class URLPatternTests: XCTestCase {
20+
func testMacro() throws {
21+
assertMacroExpansion(
22+
"""
23+
@URLPattern
24+
enum Deeplink {
25+
@URLPath("/post/{id}")
26+
case post(id: String)
27+
28+
@URLPath("/post/{id}/{name}")
29+
case name(id: String, name: String)
30+
}
31+
""",
32+
expandedSource: """
33+
enum Deeplink {
34+
case post(id: String)
35+
36+
static func createFromURLpost(_ url: URL) -> Self? {
37+
let path = url.path
38+
let components = path.split(separator: "/")
39+
40+
guard components.count == 2 else {
41+
return nil
42+
}
43+
44+
guard let id = components[1] as? String else {
45+
return nil
46+
}
47+
48+
return .post(id: id)
49+
}
50+
51+
case name(id: String, name: String)
52+
53+
static func createFromURLname(_ url: URL) -> Self? {
54+
let path = url.path
55+
let components = path.split(separator: "/")
56+
57+
guard components.count == 3 else {
58+
return nil
59+
}
60+
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+
}
67+
68+
return .name(id: id, name: name)
69+
}
70+
71+
init?(url: URL) {
72+
if let result = Self.createFromURLpost(url) {
73+
self = result
74+
return
75+
}
76+
if let result = Self.createFromURLname(url) {
77+
self = result
78+
return
79+
}
80+
return nil
81+
}
82+
}
83+
""",
84+
macros: testMacros
85+
)
86+
}
87+
88+
func testMacroWithStringLiteral() throws {
89+
90+
}
91+
}

0 commit comments

Comments
 (0)