Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNP-1649-url-macro #11

Merged
merged 2 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Sources/SurfMacros/Implementation/MacrosPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SwiftSyntaxMacros
struct MacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
SignalsPlugin.providingMacros,
UtilsPlugin.providingMacros,
InfrastructurePlugin.providingMacros,
FactoryPlugin.providingMacros,
RouterPlugin.providingMacros
Expand Down
77 changes: 77 additions & 0 deletions Sources/SurfMacros/Implementation/Utils/URLMacro.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
10 changes: 10 additions & 0 deletions Sources/SurfMacros/Implementation/Utils/UtilsPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

struct UtilsPlugin {
static let providingMacros: [Macro.Type] = [
URLMacro.self
]
}
6 changes: 6 additions & 0 deletions Sources/SurfMacros/Macros/Utils/URL.swift
Original file line number Diff line number Diff line change
@@ -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")
18 changes: 18 additions & 0 deletions Sources/SurfMacros/Support/Library/Errors/CustomError.swift
Original file line number Diff line number Diff line change
@@ -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
}

}
54 changes: 54 additions & 0 deletions Tests/SurfMacros/Utils/URLTests.swift
Original file line number Diff line number Diff line change
@@ -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
)
}

}