diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..104b33b --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,22 @@ +name: Unit Tests + +on: [push] + +jobs: + test: + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: '5.9' + + - name: Build + run: swift build + + - name: Test + run: swift test diff --git a/Sources/SurfMacros/Implementation/MacrosPlugin.swift b/Sources/SurfMacros/Implementation/MacrosPlugin.swift index a1b0087..17c10f6 100644 --- a/Sources/SurfMacros/Implementation/MacrosPlugin.swift +++ b/Sources/SurfMacros/Implementation/MacrosPlugin.swift @@ -7,6 +7,7 @@ import SwiftSyntaxMacros struct MacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ SignalsPlugin.providingMacros, + UtilsPlugin.providingMacros, InfrastructurePlugin.providingMacros, FactoryPlugin.providingMacros, RouterPlugin.providingMacros diff --git a/Sources/SurfMacros/Implementation/Utils/URLMacro.swift b/Sources/SurfMacros/Implementation/Utils/URLMacro.swift new file mode 100644 index 0000000..60a0fbb --- /dev/null +++ b/Sources/SurfMacros/Implementation/Utils/URLMacro.swift @@ -0,0 +1,77 @@ +import Foundation +import SurfMacrosSupport +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public enum URLMacro: ExpressionMacro { + + // MARK: - Names + + private enum Names { + static let url = "URL" + static let argument = "string" + } + + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + let (argument, value) = try getInputArgument(from: node) + try checkURLString(value, from: argument) + return .init(createURLExpression(with: argument)) + } + +} + +// MARK: - Getters + +private extension URLMacro { + + static func getInputArgument( + from node: some FreestandingMacroExpansionSyntax + ) throws -> (argument: ExprSyntax, argumentValue: String) { + guard + let argument = node.argumentList.first?.expression, + let segments = argument.as(StringLiteralExprSyntax.self)?.segments, + segments.count == 1, + case .stringSegment(let literalSegment)? = segments.first + else { + throw CustomError(description: "#URL requires a static string literal") + } + return (argument: argument, argumentValue: literalSegment.content.text) + } + +} + +// MARK: - Checks + +private extension URLMacro { + + static func checkURLString(_ urlString: String, from argument: ExprSyntax) throws { + guard let _ = URL(string: urlString) else { + throw CustomError(description: "malformed url: \(argument)") + } + } + +} + +// MARK: - Creations + +private extension URLMacro { + + static func createURLExpression(with argument: ExprSyntax) -> ExprSyntaxProtocol { + let urlInit = DeclReferenceExprSyntax(baseName: .identifier(Names.url)) + let stringArgument = LabeledExprSyntax( + label: .identifier(Names.argument), + expression: argument + ) + let functionCall = FunctionCallExprSyntax( + calledExpression: urlInit, + arguments: [stringArgument] + ) + return ForceUnwrapExprSyntax(expression: functionCall) + } + +} diff --git a/Sources/SurfMacros/Implementation/Utils/UtilsPlugin.swift b/Sources/SurfMacros/Implementation/Utils/UtilsPlugin.swift new file mode 100644 index 0000000..d4429ad --- /dev/null +++ b/Sources/SurfMacros/Implementation/Utils/UtilsPlugin.swift @@ -0,0 +1,10 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +struct UtilsPlugin { + static let providingMacros: [Macro.Type] = [ + URLMacro.self + ] +} diff --git a/Sources/SurfMacros/Macros/Utils/URL.swift b/Sources/SurfMacros/Macros/Utils/URL.swift new file mode 100644 index 0000000..0d099cf --- /dev/null +++ b/Sources/SurfMacros/Macros/Utils/URL.swift @@ -0,0 +1,6 @@ +import Foundation + +/// Check if provided string literal is a valid URL and produce a non-optional +/// URL value. Emit error otherwise. +@freestanding(expression) +public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "SurfMacroBody", type: "URLMacro") diff --git a/Sources/SurfMacros/Support/Library/Errors/CustomError.swift b/Sources/SurfMacros/Support/Library/Errors/CustomError.swift new file mode 100644 index 0000000..b33c1ab --- /dev/null +++ b/Sources/SurfMacros/Support/Library/Errors/CustomError.swift @@ -0,0 +1,18 @@ +// +// CustomError.swift +// +// +// Created by pavlov on 30.06.2024. +// + +import Foundation + +public struct CustomError: Error, CustomStringConvertible { + + public let description: String + + public init(description: String) { + self.description = description + } + +} diff --git a/Tests/SurfMacros/Utils/URLTests.swift b/Tests/SurfMacros/Utils/URLTests.swift new file mode 100644 index 0000000..ddc156a --- /dev/null +++ b/Tests/SurfMacros/Utils/URLTests.swift @@ -0,0 +1,54 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +#if canImport(SurfMacroBody) +import SurfMacroBody + +private let testMacros: [String: Macro.Type] = ["URL": URLMacro.self] +#endif + +final class URLMacroTests: XCTestCase { + func testExpansionWithMalformedURLEmitsError() { + assertMacroExpansion( + """ + let invalid = #URL("https://not a url.com") + """, + expandedSource: """ + let invalid = #URL("https://not a url.com") + """, + diagnostics: [ + .init(message: #"malformed url: "https://not a url.com""#, line: 1, column: 15, severity: .error) + ], + macros: testMacros + ) + } + + func testExpansionWithStringInterpolationEmitsError() { + assertMacroExpansion( + #""" + #URL("https://\(domain)/api/path") + """#, + expandedSource: #""" + #URL("https://\(domain)/api/path") + """#, + diagnostics: [ + .init(message: "#URL requires a static string literal", line: 1, column: 1, severity: .error) + ], + macros: testMacros + ) + } + + func testExpansionWithValidURL() { + assertMacroExpansion( + """ + let valid = #URL("https://swift.org/") + """, + expandedSource: """ + let valid = URL(string: "https://swift.org/")! + """, + macros: testMacros + ) + } + +}