Skip to content

Commit

Permalink
feat: add email validation macro and update README (#2)
Browse files Browse the repository at this point in the history
* feat: add email validation macro and update README

* Run swift-format

---------

Co-authored-by: josetorronteras <[email protected]>
  • Loading branch information
josetorronteras and josetorronteras authored Feb 7, 2024
1 parent 111d4a7 commit a61a3c3
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 38 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
# Email Validation Macro

[![CodeFactor](https://www.codefactor.io/repository/github/josetorronteras/emailvalidationmacro/badge)](https://www.codefactor.io/repository/github/josetorronteras/emailvalidationmacro)

Email Validation Macro is a Swift macro framework for validating email addresses.


* [Basic Usage](#basic-usage)
* [Installation](#installation)

## Basic Usage

```swift
// Create and validate an email address
let validEmail = #email("[email protected]")

let invalidEmail = #email("[email protected]") //
```

## Installation

### Swift Package Manager

Add the following line to the dependencies in `Package.swift`:

```swift
.package(
url: "https://github.com/josetorronteras/EmailValidationMacro",
from: "1.0.0"
),
```

Then add `EmailValidationMacro` to your target's dependencies:

```swift
.product(
name: "EmailValidation",
package: "EmailValidationMacro"
),
```

### Xcode

Go to `File > Add Package Dependencies...` and paste the repo's URL:

```
https://github.com/josetorronteras/EmailValidationMacro
```

## License

This library is relased under the MIT license. See [LICENSE](LICENSE) for details.
18 changes: 12 additions & 6 deletions Sources/EmailValidation/EmailValidation.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

/// A macro that produces both a value and a string containing the
/// source code that generated the value. For example,
/// Macro that Validates the provided email address.
///
/// #stringify(x + y)
/// - Parameters:
/// - email: The email address to validate.
/// - Returns: The validated email address if valid.
///
/// produces a tuple `(x + y, "x + y")`.
/// Example:
/// ```swift
/// // Example usage of the email validation macro
/// let validatedEmail = #email("[email protected]")
/// print(validatedEmail) // Output: "[email protected]"
/// ```
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "EmailValidationMacros", type: "StringifyMacro")
public macro email(_ email: String) -> String =
#externalMacro(module: "EmailValidationMacros", type: "EmailValidationMacro")
7 changes: 2 additions & 5 deletions Sources/EmailValidationClient/main.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import EmailValidation

let a = 17
let b = 25
let email = #email("[email protected]")

let (result, code) = #stringify(a + b)

print("The value \(result) was produced by the code \"\(code)\"")
print(email)
57 changes: 39 additions & 18 deletions Sources/EmailValidationMacros/EmailValidationMacro.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import Foundation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

/// Implementation of the `stringify` macro, which takes an expression
/// of any type and produces a tuple containing the value of that expression
/// and the source code that produced the value. For example
///
/// #stringify(x + y)
///
/// will expand to
@main
struct EmailValidationPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
EmailValidationMacro.self
]
}

/// Validates the provided email address and returns it if valid.
///
/// (x + y, "x + y")
public struct StringifyMacro: ExpressionMacro {
/// Example:
/// ```swift
/// // Example usage of the email validation macro
/// let validatedEmail = #email("[email protected]")
/// print(validatedEmail) // Output: "[email protected]"
/// ```
public struct EmailValidationMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
) throws -> ExprSyntax {
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 EmailValidationMacroError.requiresStaticStringLiteral
}

return "(\(argument), \(literal: argument.description))"
let email = literalSegment.content.text
guard isValidEmail(email) else {
throw EmailValidationMacroError.malformedEmail
}

return "\(argument)"
}
}

@main
struct EmailValidationPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self
]
/// Validates whether a given string is a valid email address.
///
/// - Parameter email: The string to validate as an email address.
/// - Returns: `true` if the string is a valid email address; otherwise, `false`.
///
/// - Note: This function uses a regular expression pattern to validate email addresses.
func isValidEmail(_ email: String) -> Bool {
let emailRegex = #"^[\w\.-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)*\.[a-zA-Z]{2,}$"#
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
13 changes: 13 additions & 0 deletions Sources/EmailValidationMacros/EmailValidationMacroError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
enum EmailValidationMacroError: Error, CustomStringConvertible {
case requiresStaticStringLiteral
case malformedEmail

var description: String {
switch self {
case .requiresStaticStringLiteral:
"Requires a static string literal"
case .malformedEmail:
"The input email is malformed"
}
}
}
44 changes: 35 additions & 9 deletions Tests/EmailValidationTests/EmailValidationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,62 @@ import XCTest
import EmailValidationMacros

let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self
"email": EmailValidationMacro.self
]
#endif

final class EmailValidationTests: XCTestCase {
func testMacro() throws {

func testValidEmail() throws {
#if canImport(EmailValidationMacros)
assertMacroExpansion(
"""
#email("[email protected]")
""",
expandedSource:
"""
"[email protected]"
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}

func testInvalidEmailMalformed() throws {
#if canImport(EmailValidationMacros)
assertMacroExpansion(
"""
#stringify(a + b)
#email("invalid-email")
""",
expandedSource: """
(a + b, "a + b")
expandedSource:
"""
#email("invalid-email")
""",
diagnostics: [
DiagnosticSpec(message: "The input email is malformed", line: 1, column: 1)
],
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}

func testMacroWithStringLiteral() throws {
func testInvalidEmailStaticStringLiteral() throws {
#if canImport(EmailValidationMacros)
assertMacroExpansion(
#"""
#stringify("Hello, \(name)")
#email("\(randomString(length: 2))@email.com")
"""#,
expandedSource: #"""
("Hello, \(name)", #""Hello, \(name)""#)
expandedSource:
#"""
#email("\(randomString(length: 2))@email.com")
"""#,
diagnostics: [
DiagnosticSpec(message: "Requires a static string literal", line: 1, column: 1)
],
macros: testMacros
)
#else
Expand Down

0 comments on commit a61a3c3

Please sign in to comment.