Skip to content

Commit

Permalink
Add URITemplate freestanding expression macro
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdeem committed Dec 3, 2024
1 parent 59dc884 commit 775a433
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .spi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [ScreamURITemplate]
- documentation_targets: [ScreamURITemplate, ScreamURITemplateMacros]
25 changes: 22 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
// swift-tools-version: 6.0

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "ScreamURITemplate",
platforms: [.macOS(.v13)],
products: [
.library(
name: "ScreamURITemplate",
targets: ["ScreamURITemplate"]),
.library(
name: "ScreamURITemplateMacros",
targets: ["ScreamURITemplateMacros"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-syntax", from: "600.0.1"),
],
targets: [
.target(
name: "ScreamURITemplate",
dependencies: [],
resources: [.process("PrivacyInfo.xcprivacy")]),
.target(
name: "ScreamURITemplateMacros",
dependencies: ["ScreamURITemplate", "ScreamURITemplateCompilerPlugin"]),
.macro(
name: "ScreamURITemplateCompilerPlugin",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
"ScreamURITemplate",
]),
.testTarget(
name: "ScreamURITemplateTests",
dependencies: ["ScreamURITemplate"],
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
"ScreamURITemplate",
"ScreamURITemplateMacros"],
exclude: [
"data/uritemplate-test/json2xml.xslt",
"data/uritemplate-test/LICENSE",
Expand All @@ -35,6 +54,6 @@ let package = Package(
]),
.executableTarget(
name: "ScreamURITemplateExample",
dependencies: ["ScreamURITemplate"]),
dependencies: ["ScreamURITemplate", "ScreamURITemplateMacros"]),
],
swiftLanguageModes: [.v6])
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2018-2024 Alex Deem
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct URITemplateCompilerPlugin: CompilerPlugin {
var providingMacros: [Macro.Type] = [URITemplateMacro.self]
}
49 changes: 49 additions & 0 deletions Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2018-2024 Alex Deem
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftCompilerPlugin
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

import ScreamURITemplate

public struct URITemplateMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in _: some MacroExpansionContext) throws -> ExprSyntax {
guard let argument = node.arguments.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
segments.count == 1,
case let .stringSegment(literalSegment)? = segments.first
else {
throw DiagnosticsError(diagnostics: [
Diagnostic(node: node,
message: MacroExpansionErrorMessage("#URITemplate requires a static string literal")),
])
}

let uriTemplateString = literalSegment.content.text
do {
_ = try URITemplate(string: uriTemplateString)
} catch {
throw DiagnosticsError(diagnostics: [
Diagnostic(node: literalSegment,
message: MacroExpansionErrorMessage("Invalid URI template: \(error.reason) at \"\(uriTemplateString.suffix(from: error.position).prefix(50))\"")),
])
}

return "try! URITemplate(string: \(argument))"
}
}
4 changes: 4 additions & 0 deletions Sources/ScreamURITemplateExample/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Foundation
import ScreamURITemplate
import ScreamURITemplateMacros

let template = try URITemplate(string: "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}")
let variables = [
Expand All @@ -27,3 +28,6 @@ let urlString = try template.process(variables: variables)
let url = URL(string: urlString)!
print("Expanding \(template)\n with \(variables):\n")
print(url.absoluteString)

let macroExpansion = #URITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}")
print(macroExpansion)
26 changes: 26 additions & 0 deletions Sources/ScreamURITemplateMacros/Macros.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2018-2024 Alex Deem
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import ScreamURITemplate

/// Macro providing compile-time validation of a URITemplate represented by a string literal
/// Example:
/// ```swift
/// let template = #URITemplate("https://api.github.com/repos/{owner}")
/// ```
/// - Parameters:
/// - : A string literal representing the URI Template
/// - Returns: A `URITemplate` constructed from the string literal
@freestanding(expression)
public macro URITemplate(_ stringLiteral: StaticString) -> URITemplate = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "URITemplateMacro")
71 changes: 71 additions & 0 deletions Tests/ScreamURITemplateTests/MacroTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2018-2024 Alex Deem
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import ScreamURITemplateCompilerPlugin
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport

import XCTest

class MacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"URITemplate": URITemplateMacro.self
]

func testValidURITemplateMacro() throws {
assertMacroExpansion(
#"""
#URITemplate("https://api.github.com/repos/{owner}")
"""#,
expandedSource:
#"""
try! URITemplate(string: "https://api.github.com/repos/{owner}")
"""#,
diagnostics: [],
macros: testMacros)
}

func testInvalidURITemplateMacro() throws {
assertMacroExpansion(
#"""
#URITemplate("https://api.github.com/repos/{}/{repo}")
"""#,
expandedSource:
#"""
#URITemplate("https://api.github.com/repos/{}/{repo}")
"""#,
diagnostics: [
DiagnosticSpec(message: "Invalid URI template: Empty Variable Name at \"}/{repo}\"", line: 1, column: 15)
],
macros: testMacros)
}

func testMisusedURITemplateMacro() throws {
assertMacroExpansion(
#"""
let s: StaticString = "https://api.github.com/repos/{owner}"
#URITemplate(s)
"""#,
expandedSource:
#"""
let s: StaticString = "https://api.github.com/repos/{owner}"
#URITemplate(s)
"""#,
diagnostics: [
DiagnosticSpec(message: "#URITemplate requires a static string literal", line: 2, column: 1)
],
macros: testMacros)
}

}

0 comments on commit 775a433

Please sign in to comment.