#iOS

Swift Macros: Compile time URL validation

What is a Swift Macro? 

In 2023 Swift Macros were introduced, a way to make the codebase more expressive and easier to read by generating repetitive source code at compile time.

A macro is always additive, this means it only adds new code alongside the code that you wrote, but never modifies or deletes code that’s already part of your project.

There are two kinds of macros in Swift:

  • Freestanding macros that appear on their own. “#warning(..)” is Swift provided macro that emits a custom compile-time warning.
  • Attached macros that modify the declaration that they’re attached to. You can use them by writing `@` before its name.

Usage of #SafeURL 

Our first macro is a freestanding macro that checks whether a string literal is a valid URL during code compilation, helping us skip runtime validation of the URL.

let url = #SafeURL("https://www.google.com")

The macro initially checks that the argument is a single string literal segment and then validates that it is not only a valid URL, but also that it has a valid https prefix. In case of a validation error, the macro emits custom compilation errors.

Invalid URL error diagnostic

Not allowed scheme error diagnostic

#SafeURL implementation

Macros are essentially code transformations that occur at compile time. For #SafeURL we will accept a string literal (the URL) as a parameter.

First, we define the public macro that accepts a single parameter string literal of type StaticString and returns a value of type URL.

@freestanding(expression)
public macro SafeURL(_ stringLiteral: StaticString) -> URL = #externalMacro(module: "VFMacrosImp", type: "SafeURLMacro")

In the macro's implementation we first extract the first argument:

guard
    /// 1. Grab the first (and only) Macro argument.
    let argument = node.argumentList.first?.expression,
    /// 2. Ensure the argument contains of a single String literal segment.
    let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
    segments.count == 1,
    /// 3. Grab the actual String literal segment.
    case .stringSegment(let literalSegment)? = segments.first
else {
    throw URLMacroError.requiresStaticStringLiteral
}

Finally we perform our validations:

/// 4. Validate whether the String literal matches a valid URL structure.
guard let url = URL(string: literalSegment.content.text) else {
    throw URLMacroError.malformedURL(urlString: "\(argument)")
}

/// 5. Validate whether the URL has https prefix.
guard url.scheme == "https" else {
    throw URLMacroError.requiresHTTPS(urlString: "\(argument)")
}

 

Testing

Along with the macros language feature, Apple released some new testing APIs that allow us to easily unit test macro expansions.

We can test that the macro produces the expected diagnostic messages in case of an invalid URL:

func testInvalidURL() {
    let diagnostic = DiagnosticSpec(
        message: "#SafeURL requires a static string literal",
        line: 2,
        column: 1
    )
    assertMacroExpansion(
    #"""
    let url = "someURL"
    #SafeURL(url)
    """#,
    expandedSource: #"""
    let url = "someURL"
    #SafeURL(url)
    """#,
    diagnostics: [diagnostic], // expected diagnostics
    macros: testMacros
    )
}

or check that the macro correctly produces a wellformed URL:

func testValidURL() {
    assertMacroExpansion(
    #"""
    #URLOnSteroids("https://www.google.com")
    """#,
    expandedSource: #"""
    URL(string: "https://www.google.com")!
    """#,
    macros: testMacros
    )
}

 

Limitations

Swift Macros can only access the scope of the entity to which they are applied to. This means for example that if we add a macro to a function it “knows” only that function not the entire class. Therefore, it can only modify that function.

Another limitation is that macros have additional dependencies (swift-syntax) which leads to increased compilation times.

Conclusion

In conclusion, swift macros are going to be a useful tool for any iOS developer, when taking into account the limitations that they have.

The SafeURL macro is our first macro that helped us familiarise ourselves with this new Swift feature.

Thanks for reading!

Useful links:

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/

https://developer.apple.com/videos/play/wwdc2023/10166/

Loading...