From d99ee4bf85a6cbffad0325c7f099e1c2b1d32489 Mon Sep 17 00:00:00 2001
From: Thomas De Leon <3507743+tdeleon@users.noreply.github.com>
Date: Wed, 3 Jan 2024 14:55:33 -0800
Subject: [PATCH] Add @APIService Macro (#41)
* #35 Adding RestAPI macro
* #35: Add missing names parameter for @attached extension macro declaration; remove unnecessary @attached member declaration; remove macro without baseURL paramater
* #35: Rename @RestAPI to @APIService
* Update test.yml for swift 5.9
* Update test.yml
* Add macros target as test dependency
* #35: Temporarily disable macros tests on windows
* #35 temporarily disable building macros on windows when testing
* #35: Disable macros on windows during tests
* #35: Don't use macros in regular unit tests
* Update test.yml to set Xcode 15.1 on macOS 13 runner
* Update test.yml - select swift version
* Update test.yml
* Update test.yml
* Update test.yml
* Update test.yml
* #35: Complete macro renaming
* #35: Add error handling for @APIService macro
* #35: Maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* #35: maintain support for swift <5.9
* Report swift version in ci action
* #35: Remove unnecessary import check
---
.github/workflows/test.yml | 24 ++--
.../xcschemes/Relax-Package.xcscheme | 116 ++++++++++++++++++
.../xcshareddata/xcschemes/URLMock.xcscheme | 66 ++++++++++
Package.resolved | 14 +++
Package@swift-5.9.swift | 66 ++++++++++
Sources/Relax/Macros/APIService.swift | 13 ++
Sources/RelaxMacros/APIServiceMacro.swift | 51 ++++++++
.../RelaxMacros/RelaxMacroDiagnostic.swift | 25 ++++
Sources/RelaxMacros/RelaxMacrosPlugin.swift | 16 +++
Tests/RelaxMacrosTests/MacroTests.swift | 63 ++++++++++
Tests/RelaxTests/Helpers/MockServices.swift | 5 +-
.../RelaxTests/Helpers/Service+Testing.swift | 2 +-
Tests/RelaxTests/Request/ServiceTests.swift | 2 +-
13 files changed, 451 insertions(+), 12 deletions(-)
create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Relax-Package.xcscheme
create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/URLMock.xcscheme
create mode 100644 Package.resolved
create mode 100644 Package@swift-5.9.swift
create mode 100644 Sources/Relax/Macros/APIService.swift
create mode 100644 Sources/RelaxMacros/APIServiceMacro.swift
create mode 100644 Sources/RelaxMacros/RelaxMacroDiagnostic.swift
create mode 100644 Sources/RelaxMacros/RelaxMacrosPlugin.swift
create mode 100644 Tests/RelaxMacrosTests/MacroTests.swift
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2a5f23e..2c3f814 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,29 +1,39 @@
-name: Test
+name: Build & Test
on: [push]
jobs:
build:
strategy:
- max-parallel: 5
fail-fast: false
matrix:
- os: [macos-12, macos-latest, ubuntu-latest, windows-latest]
+ os: [macos-13, ubuntu-latest, windows-latest]
+ swift: ["5.7", "5.8", "5.9"]
runs-on: ${{ matrix.os }}
steps:
- - if: runner.os == 'Windows'
- name: Setup Swift
+ - if: ${{ (runner.os != 'Windows') || (runner.os == 'Windows' && matrix.swift != '5.7') }}
+ name: Setup Swift Version
+ uses: SwiftyLab/setup-swift@latest
+ with:
+ swift-version: ${{ matrix.swift }}
+
+ - if: ${{ (runner.os == 'Windows') && (matrix.swift == '5.7') }}
+ name: Setup Swift Version (5.7 on Windows)
uses: compnerd/gha-setup-swift@main
with:
branch: swift-5.7-release
tag: 5.7-RELEASE
-
+
+ - name: Get Swift Version
+ run: swift --version
+
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Build
run: swift build
- name: Test
run: swift test
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Relax-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Relax-Package.xcscheme
new file mode 100644
index 0000000..1809d9f
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Relax-Package.xcscheme
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/URLMock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/URLMock.xcscheme
new file mode 100644
index 0000000..db97996
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/URLMock.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..6fd9cd0
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-syntax.git",
+ "state" : {
+ "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
+ "version" : "509.0.2"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..9645f0c
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,66 @@
+// swift-tools-version:5.9
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+import CompilerPluginSupport
+
+
+var targets: [Target] = [
+ .target(
+ name: "URLMock",
+ dependencies: ["Relax"]
+ ),
+ .testTarget(
+ name: "RelaxTests",
+ dependencies: ["Relax", "URLMock"]
+ ),
+]
+
+var dependencies = [Package.Dependency]()
+
+// Macros do not currently compile on windows when building tests: https://github.com/apple/swift-package-manager/issues/7174
+#if canImport(XCTest) && os(Windows)
+targets.append(.target(name: "Relax"))
+#else
+targets.append(
+ contentsOf: [
+ .target(name: "Relax", dependencies: ["RelaxMacros"]),
+ .macro(
+ name: "RelaxMacros",
+ dependencies: [
+ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
+ .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
+ ]
+ ),
+ .testTarget(
+ name: "RelaxMacrosTests",
+ dependencies: ["RelaxMacros", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")]
+ )
+ ]
+)
+dependencies = [
+ // Depend on the Swift 5.9 release of SwiftSyntax
+ .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
+]
+#endif
+
+let package = Package(
+ name: "Relax",
+ platforms: [
+ .iOS(.v14),
+ .tvOS(.v14),
+ .watchOS(.v7),
+ .macOS(.v12),
+ ],
+ products: [
+ // Products define the executables and libraries produced by a package, and make them visible to other packages.
+ .library(
+ name: "Relax",
+ targets: ["Relax"]),
+ .library(
+ name: "URLMock",
+ targets: ["URLMock"])
+ ],
+ dependencies: dependencies,
+ targets: targets
+)
diff --git a/Sources/Relax/Macros/APIService.swift b/Sources/Relax/Macros/APIService.swift
new file mode 100644
index 0000000..b7b1959
--- /dev/null
+++ b/Sources/Relax/Macros/APIService.swift
@@ -0,0 +1,13 @@
+//
+// APIService.swift
+//
+//
+// Created by Thomas De Leon on 12/27/23.
+//
+
+#if swift(>=5.9)
+import Foundation
+
+@attached(extension, conformances: APIComponent, names: named(baseURL))
+public macro APIService(_ baseURL: String) = #externalMacro(module: "RelaxMacros", type: "APIServiceMacro")
+#endif
diff --git a/Sources/RelaxMacros/APIServiceMacro.swift b/Sources/RelaxMacros/APIServiceMacro.swift
new file mode 100644
index 0000000..c5aa9db
--- /dev/null
+++ b/Sources/RelaxMacros/APIServiceMacro.swift
@@ -0,0 +1,51 @@
+//
+// APIServiceMacro.swift
+//
+//
+// Created by Thomas De Leon on 12/27/23.
+//
+
+import SwiftSyntax
+import SwiftSyntaxBuilder
+import SwiftSyntaxMacros
+import SwiftDiagnostics
+import Foundation
+
+public struct APIServiceMacro: ExtensionMacro {
+ public static func expansion(
+ of node: AttributeSyntax,
+ attachedTo declaration: some DeclGroupSyntax,
+ providingExtensionsOf type: some TypeSyntaxProtocol,
+ conformingTo protocols: [TypeSyntax],
+ in context: some MacroExpansionContext
+ ) throws -> [ExtensionDeclSyntax] {
+ guard let urlString = node
+ .arguments?.as(LabeledExprListSyntax.self)?
+ .first?.expression.as(StringLiteralExprSyntax.self)?
+ .segments
+ .first?.trimmedDescription else {
+ let error = Diagnostic(node: node, message: RelaxMacroDiagnostic.invalidBaseURL)
+ context.diagnose(error)
+ return []
+ }
+
+ // check that the URL will be valid
+ guard URL(string: urlString) != nil else {
+ let error = Diagnostic(node: node, message: RelaxMacroDiagnostic.invalidBaseURL)
+ context.diagnose(error)
+ return []
+ }
+
+ let decl: DeclSyntax =
+ """
+ extension \(type.trimmed): APIComponent {
+ static let baseURL: URL = URL(string: \"\(raw: urlString)\")!
+ }
+ """
+
+ guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
+ return []
+ }
+ return [extensionDecl]
+ }
+}
diff --git a/Sources/RelaxMacros/RelaxMacroDiagnostic.swift b/Sources/RelaxMacros/RelaxMacroDiagnostic.swift
new file mode 100644
index 0000000..078dfef
--- /dev/null
+++ b/Sources/RelaxMacros/RelaxMacroDiagnostic.swift
@@ -0,0 +1,25 @@
+//
+// RelaxMacroDiagnostic.swift
+//
+//
+// Created by Thomas De Leon on 1/2/24.
+//
+
+import SwiftDiagnostics
+
+enum RelaxMacroDiagnostic: String, DiagnosticMessage {
+ case invalidBaseURL
+
+ var severity: DiagnosticSeverity { .error }
+
+ var message: String {
+ switch self {
+ case .invalidBaseURL:
+ "The base URL is invalid."
+ }
+ }
+
+ var diagnosticID: MessageID {
+ MessageID(domain: "RelaxMacros", id: rawValue)
+ }
+}
diff --git a/Sources/RelaxMacros/RelaxMacrosPlugin.swift b/Sources/RelaxMacros/RelaxMacrosPlugin.swift
new file mode 100644
index 0000000..715a77b
--- /dev/null
+++ b/Sources/RelaxMacros/RelaxMacrosPlugin.swift
@@ -0,0 +1,16 @@
+//
+// RelaxMacrosPlugin.swift
+//
+//
+// Created by Thomas De Leon on 12/27/23.
+//
+
+import SwiftCompilerPlugin
+import SwiftSyntaxMacros
+
+@main
+struct RelaxMacrosPlugin: CompilerPlugin {
+ let providingMacros: [Macro.Type] = [
+ APIServiceMacro.self
+ ]
+}
diff --git a/Tests/RelaxMacrosTests/MacroTests.swift b/Tests/RelaxMacrosTests/MacroTests.swift
new file mode 100644
index 0000000..a281765
--- /dev/null
+++ b/Tests/RelaxMacrosTests/MacroTests.swift
@@ -0,0 +1,63 @@
+//
+// MacroTests.swift
+//
+//
+// Created by Thomas De Leon on 12/27/23.
+//
+
+import SwiftSyntaxMacros
+import SwiftSyntaxMacrosTestSupport
+import XCTest
+@testable import RelaxMacros
+
+#if canImport(RelaxMacros)
+import RelaxMacros
+let testMacros: [String: Macro.Type] = [
+ "APIService": APIServiceMacro.self
+]
+#endif
+
+final class MacroTests: XCTestCase {
+ func testAPIService() throws {
+ #if canImport(RelaxMacros)
+ assertMacroExpansion(
+ """
+ @APIService("https://example.com/")
+ enum TestService {
+ }
+ """,
+ expandedSource: """
+ enum TestService {
+ }
+
+ extension TestService: APIComponent {
+ static let baseURL: URL = URL(string: "https://example.com/")!
+ }
+ """,
+ macros: testMacros
+ )
+ #else
+ throw XCTSkip("macros are only supported when running tests for the host platform")
+ #endif
+ }
+
+ func testAPIServiceInvalidBaseURL() throws {
+ #if canImport(RelaxMacros)
+ assertMacroExpansion(
+ """
+ @APIService("https:// .com")
+ enum TestService {
+ }
+ """,
+ expandedSource: """
+ enum TestService {
+ }
+ """,
+ diagnostics: [.init(message: RelaxMacroDiagnostic.invalidBaseURL.message, line: 1, column: 1)],
+ macros: testMacros
+ )
+ #else
+ throw XCTSkip("macros are only supported when running tests for the host platform")
+ #endif
+ }
+}
diff --git a/Tests/RelaxTests/Helpers/MockServices.swift b/Tests/RelaxTests/Helpers/MockServices.swift
index 174c7b0..0d734c5 100644
--- a/Tests/RelaxTests/Helpers/MockServices.swift
+++ b/Tests/RelaxTests/Helpers/MockServices.swift
@@ -12,8 +12,7 @@ import FoundationNetworking
@testable import Relax
enum ExampleService: Service {
- static let baseURL: URL = URL(string: "https://www.example.com")!
-
+ static let baseURL: URL = URL(string: "https://example.com")!
static var session: URLSession = .shared
@RequestBuilder
@@ -136,7 +135,7 @@ enum ExampleService: Service {
}
}
-struct BadURLService: Service {
+enum BadURLService: Service {
static let baseURL: URL = URL(string: "a://@@")!
@RequestBuilder
diff --git a/Tests/RelaxTests/Helpers/Service+Testing.swift b/Tests/RelaxTests/Helpers/Service+Testing.swift
index 12830a7..0a70446 100644
--- a/Tests/RelaxTests/Helpers/Service+Testing.swift
+++ b/Tests/RelaxTests/Helpers/Service+Testing.swift
@@ -12,7 +12,7 @@ import FoundationNetworking
import XCTest
@testable import Relax
-extension Service {
+extension APIComponent {
// func checkSuccess(request: some Request, received: URLRequest) {
// // check request method type
// XCTAssertEqual(received.httpMethod, request.httpMethod.rawValue)
diff --git a/Tests/RelaxTests/Request/ServiceTests.swift b/Tests/RelaxTests/Request/ServiceTests.swift
index 35624b3..0a4eb89 100644
--- a/Tests/RelaxTests/Request/ServiceTests.swift
+++ b/Tests/RelaxTests/Request/ServiceTests.swift
@@ -49,7 +49,7 @@ final class ServiceTests: XCTestCase {
}
func testDefaults() {
- enum Testing: Service {
+ enum Testing: APIComponent {
static let baseURL = URL(string: "https://example.com")!
}