mirror of
https://github.com/zhigang1992/swift-request.git
synced 2026-01-12 09:34:11 +08:00
Implement basic request methods and parameters
This commit is contained in:
committed by
Ailton Vieira
parent
82f31c624f
commit
a73ceb1aeb
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
14
Package.resolved
Normal file
14
Package.resolved
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-syntax.git",
|
||||
"state" : {
|
||||
"revision" : "83c2be9f6268e9f67622f130440cf43928c6bfb0",
|
||||
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-05-20-a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
52
Package.swift
Normal file
52
Package.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
import CompilerPluginSupport
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftRequest",
|
||||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "SwiftRequest",
|
||||
targets: ["SwiftRequest"]
|
||||
),
|
||||
.executable(
|
||||
name: "SwiftRequestClient",
|
||||
targets: ["SwiftRequestClient"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
// Depend on the latest Swift 5.9 prerelease of SwiftSyntax
|
||||
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
// Macro implementation that performs the source transformation of a macro.
|
||||
.macro(
|
||||
name: "SwiftRequestMacros",
|
||||
dependencies: [
|
||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
|
||||
]
|
||||
),
|
||||
|
||||
// Library that exposes a macro as part of its API, which is used in client programs.
|
||||
.target(name: "SwiftRequest", dependencies: ["SwiftRequestMacros"]),
|
||||
|
||||
// A client of the library, which is able to use the macro in its own code.
|
||||
.executableTarget(name: "SwiftRequestClient", dependencies: ["SwiftRequest"]),
|
||||
|
||||
// A test target used to develop the macro implementation.
|
||||
.testTarget(
|
||||
name: "SwiftRequestTests",
|
||||
dependencies: [
|
||||
"SwiftRequestMacros",
|
||||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
47
Sources/SwiftRequest/Arguments.swift
Normal file
47
Sources/SwiftRequest/Arguments.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
@propertyWrapper
|
||||
public struct Body<Value: Encodable> {
|
||||
public var wrappedValue: Value
|
||||
|
||||
public init(wrappedValue: Value) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct PathParam<Value: CustomStringConvertible> {
|
||||
public var wrappedValue: Value
|
||||
let name: String?
|
||||
|
||||
public init(wrappedValue: Value, _ name: String? = nil) {
|
||||
self.wrappedValue = wrappedValue
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct QueryParam<Value: CustomStringConvertible> {
|
||||
public var wrappedValue: Value
|
||||
let name: String?
|
||||
|
||||
public init(wrappedValue: Value, _ name: String? = nil) {
|
||||
self.wrappedValue = wrappedValue
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct Header<Value: CustomStringConvertible> {
|
||||
public var wrappedValue: Value
|
||||
let name: String?
|
||||
|
||||
public init(wrappedValue: Value, _ name: String? = nil) {
|
||||
self.wrappedValue = wrappedValue
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: CustomStringConvertible where Wrapped: CustomStringConvertible {
|
||||
public var description: String {
|
||||
self?.description ?? "nil"
|
||||
}
|
||||
}
|
||||
11
Sources/SwiftRequest/HTTPResponseError.swift
Normal file
11
Sources/SwiftRequest/HTTPResponseError.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
public struct HTTPResponseError: Error {
|
||||
public let data: Data
|
||||
public let response: HTTPURLResponse
|
||||
|
||||
public init(data: Data, response: HTTPURLResponse) {
|
||||
self.data = data
|
||||
self.response = response
|
||||
}
|
||||
}
|
||||
20
Sources/SwiftRequest/SwiftRequest.swift
Normal file
20
Sources/SwiftRequest/SwiftRequest.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
@attached(member, names: named(baseURL), named(session), named(init(baseURL:session:)))
|
||||
@attached(conformance)
|
||||
public macro Service(_ route: String? = nil) = #externalMacro(module: "SwiftRequestMacros", type: "ServiceMacro")
|
||||
|
||||
public protocol Service {}
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro GET<T: Decodable>(_ path: String, headers: [String: any CustomStringConvertible] = [:]) = #externalMacro(module: "SwiftRequestMacros", type: "GetRequestMacro")
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro POST<T: Decodable>(_ path: String, headers: [String: any CustomStringConvertible] = [:], body: T) = #externalMacro(module: "SwiftRequestMacros", type: "PostRequestMacro")
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro PUT<T: Decodable>(_ path: String, headers: [String: any CustomStringConvertible] = [:], body: T) = #externalMacro(module: "SwiftRequestMacros", type: "PutRequestMacro")
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro PATCH<T: Decodable>(_ path: String, headers: [String: any CustomStringConvertible] = [:], body: T) = #externalMacro(module: "SwiftRequestMacros", type: "PatchRequestMacro")
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro DELETE<T: Decodable>(_ path: String, headers: [String: any CustomStringConvertible] = [:]) = #externalMacro(module: "SwiftRequestMacros", type: "DeleteRequestMacro")
|
||||
38
Sources/SwiftRequestClient/main.swift
Normal file
38
Sources/SwiftRequestClient/main.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import SwiftRequest
|
||||
import Foundation
|
||||
|
||||
struct Quote: Decodable {
|
||||
let id: String
|
||||
let content: String
|
||||
let author: String
|
||||
let tags: [String]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "_id"
|
||||
case content
|
||||
case author
|
||||
case tags
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class QuoteService {
|
||||
@GET<[Quote]>("quotes/random")
|
||||
private func getRandomQuotes(@QueryParam("author") authorSlug: String? = nil) {}
|
||||
|
||||
@GET<Quote>("quotes/{id}")
|
||||
private func getQuote(@PathParam by id: String) {}
|
||||
}
|
||||
|
||||
let baseURL = URL(string: "https://api.quotable.io")!
|
||||
let service = QuoteService(baseURL: baseURL)
|
||||
|
||||
do {
|
||||
let (quotes, _) = try await service.getRandomQuotes(authorSlug: "agatha-christie")
|
||||
print(quotes[0])
|
||||
|
||||
let (quote, _) = try await service.getQuote(by: quotes[0].id)
|
||||
print(quote)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
96
Sources/SwiftRequestMacros/FunctionParameters.swift
Normal file
96
Sources/SwiftRequestMacros/FunctionParameters.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
import SwiftSyntax
|
||||
|
||||
struct FunctionParameter {
|
||||
let attribute: Attribute
|
||||
let label: String?
|
||||
let name: String
|
||||
let type: String
|
||||
let optional: Bool
|
||||
}
|
||||
|
||||
struct Attribute {
|
||||
let name: String
|
||||
let arguments: [AttributeArgument]
|
||||
}
|
||||
|
||||
struct AttributeArgument {
|
||||
let label: String?
|
||||
let value: String
|
||||
}
|
||||
|
||||
extension FunctionParameter {
|
||||
init(from syntax: FunctionParameterSyntax) {
|
||||
let attributes = syntax.attributes?.compactMap(Attribute.init) ?? []
|
||||
let label: String?
|
||||
let name: String
|
||||
if let secondName = syntax.secondName {
|
||||
label = syntax.firstName.text
|
||||
name = secondName.text
|
||||
} else {
|
||||
label = nil
|
||||
name = syntax.firstName.text
|
||||
}
|
||||
let type = syntax.type.description
|
||||
let optional = syntax.type.as(OptionalTypeSyntax.self) != nil
|
||||
|
||||
self.init(
|
||||
attribute: attributes[0],
|
||||
label: label,
|
||||
name: name,
|
||||
type: type,
|
||||
optional: optional
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
init(from syntax: AttributeSyntax) {
|
||||
let name = syntax.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name.text ?? syntax.attributeName.description
|
||||
let arguments = syntax.argument?.as(TupleExprElementListSyntax.self)?.map(AttributeArgument.init) ?? []
|
||||
|
||||
self.init(name: name, arguments: arguments)
|
||||
}
|
||||
|
||||
init?(from syntax: AttributeListSyntax.Element) {
|
||||
guard let syntax = syntax.as(AttributeSyntax.self) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(from: syntax)
|
||||
}
|
||||
}
|
||||
|
||||
extension AttributeArgument {
|
||||
init(from syntax: TupleExprElementSyntax) {
|
||||
let label = syntax.label?.text
|
||||
let value = syntax.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description
|
||||
|
||||
self.init(label: label, value: value!)
|
||||
}
|
||||
}
|
||||
|
||||
typealias FunctionParameters = [FunctionParameter]
|
||||
|
||||
extension FunctionParameters {
|
||||
init(from syntax: FunctionParameterListSyntax) {
|
||||
self.init(syntax.map(FunctionParameter.init))
|
||||
}
|
||||
|
||||
func getHeaders() -> [FunctionParameter] {
|
||||
filter { $0.attribute.name == "Header" }
|
||||
}
|
||||
|
||||
func getQueryParams() -> [FunctionParameter] {
|
||||
filter { $0.attribute.name == "QueryParam" }
|
||||
}
|
||||
|
||||
func getPathParams() -> [FunctionParameter] {
|
||||
filter { $0.attribute.name == "PathParam" }
|
||||
}
|
||||
|
||||
func getBody() -> FunctionParameter? {
|
||||
first { $0.attribute.name == "Body" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
44
Sources/SwiftRequestMacros/RequestDiagnostics.swift
Normal file
44
Sources/SwiftRequestMacros/RequestDiagnostics.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import SwiftSyntax
|
||||
import SwiftDiagnostics
|
||||
|
||||
struct RequestDiagnostics{
|
||||
let method: String
|
||||
|
||||
private static let domain = "endpoint"
|
||||
|
||||
private func diagnostic(for node: Syntax, text: String, id: String, severity: DiagnosticSeverity = .error) -> Diagnostic {
|
||||
Diagnostic(
|
||||
node: node,
|
||||
message: SimpleDiagnosticMessage(
|
||||
message: text,
|
||||
diagnosticID: .init(
|
||||
domain: Self.domain,
|
||||
id: id
|
||||
),
|
||||
severity: severity
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func justFunction(node: Syntax) -> Diagnostic {
|
||||
diagnostic(for: node, text: "@\(method) requires a function", id: "justFunction")
|
||||
}
|
||||
|
||||
func justPrivate(node: Syntax) -> Diagnostic {
|
||||
diagnostic(for: node, text: "@\(method) requires a private function", id: "justPrivate")
|
||||
}
|
||||
|
||||
func pathRequired(node: Syntax) -> Diagnostic {
|
||||
diagnostic(for: node, text: "@\(method) requires a path", id: "pathRequired")
|
||||
}
|
||||
|
||||
func outputTypeRequired(node: Syntax) -> Diagnostic {
|
||||
diagnostic(for: node, text: "@\(method) requires a type as a generic argument", id: "outputTypeRequired")
|
||||
}
|
||||
|
||||
func uniquePathParams(node: Syntax) -> Diagnostic {
|
||||
diagnostic(for: node, text: "@\(method) requires unique path params", id: "uniquePathParams")
|
||||
}
|
||||
}
|
||||
|
||||
struct MacroExpansionError: Error {}
|
||||
305
Sources/SwiftRequestMacros/RequestMacro.swift
Normal file
305
Sources/SwiftRequestMacros/RequestMacro.swift
Normal file
@@ -0,0 +1,305 @@
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxMacros
|
||||
import RegexBuilder
|
||||
import Foundation
|
||||
import SwiftDiagnostics
|
||||
|
||||
public class RequestMacro: PeerMacro {
|
||||
class var httpMethod: String { fatalError() }
|
||||
|
||||
static var diagnostics : RequestDiagnostics { RequestDiagnostics(method: httpMethod) }
|
||||
|
||||
public static func expansion(
|
||||
of node: AttributeSyntax,
|
||||
providingPeersOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [DeclSyntax] {
|
||||
// Require attached to a function
|
||||
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
||||
context.diagnose(diagnostics.justFunction(node: Syntax(node)))
|
||||
return []
|
||||
}
|
||||
|
||||
// Require a private function
|
||||
guard funcDecl.modifiers?.count == 1 && funcDecl.modifiers?.first?.as(DeclModifierSyntax.self)?.name.text == "private" else {
|
||||
context.diagnose(diagnostics.justPrivate(node: Syntax(fromProtocol: funcDecl.modifiers ?? funcDecl.funcKeyword)))
|
||||
return []
|
||||
}
|
||||
|
||||
let (path, outputType, pathParams, defaultHeaders) = try getRequestParameters(for: node, in: context)
|
||||
|
||||
// Generate the new function call
|
||||
let parameterList = funcDecl.signature.input.parameterList
|
||||
|
||||
let newParameterList = FunctionParameterListSyntax(parameterList.map { param in
|
||||
var newParam = param
|
||||
newParam.attributes = newParam.attributes?.removingFirst()
|
||||
return newParam
|
||||
})
|
||||
|
||||
let newCall: ExprSyntax = "\(funcDecl.identifier)(\(newParameterList))"
|
||||
|
||||
let functionParameters = FunctionParameters(from: parameterList)
|
||||
|
||||
let endpointDecl = try generateEndpointExpr(from: functionParameters, path: path, pathParams: pathParams, in: context)
|
||||
let queryParamsDecl = generateQueryParamsDecl(from: functionParameters)
|
||||
let urlDecl = generateUrlDecl(endpoint: endpointDecl, queryParams: queryParamsDecl)
|
||||
let requestDecl = generateRequestDecl()
|
||||
|
||||
let headersExpr = generateHeadersDecl(from: functionParameters, defaultHeaders: defaultHeaders)
|
||||
let bodyExpr = generateBodyDecl(from: functionParameters)
|
||||
|
||||
let allDeclarations: [DeclSyntax] = [
|
||||
urlDecl,
|
||||
requestDecl,
|
||||
headersExpr,
|
||||
bodyExpr
|
||||
].compactMap { $0 }
|
||||
|
||||
let allDeclarationsExpr: DeclSyntax = """
|
||||
\(raw: allDeclarations.map { expr in
|
||||
"""
|
||||
\(expr)
|
||||
"""
|
||||
}.joined(separator: "\n"))
|
||||
"""
|
||||
|
||||
let callExpr = generateCallDecl(of: outputType)
|
||||
|
||||
let newFunc: DeclSyntax = """
|
||||
func \(newCall) async throws -> (\(outputType), HTTPURLResponse) {
|
||||
\(allDeclarationsExpr)
|
||||
\(callExpr)
|
||||
}
|
||||
"""
|
||||
|
||||
return [newFunc]
|
||||
}
|
||||
|
||||
private static func generateRequestDecl() -> DeclSyntax {
|
||||
"""
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "\(raw: httpMethod)"
|
||||
"""
|
||||
}
|
||||
|
||||
private static func generateCallDecl(of outputType: TypeSyntax) -> DeclSyntax {
|
||||
let requestCall: DeclSyntax = """
|
||||
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
|
||||
guard (200..<300).contains(response.statusCode) else {
|
||||
throw HTTPResponseError(data: data, response: response)
|
||||
}
|
||||
"""
|
||||
|
||||
let returnData: DeclSyntax = if outputType.description != "Data" {
|
||||
"""
|
||||
\(requestCall)
|
||||
let decoder = JSONDecoder()
|
||||
return (try decoder.decode(\(outputType).self, from: data), response)
|
||||
"""
|
||||
} else {
|
||||
"""
|
||||
\(requestCall)
|
||||
return (data, response)
|
||||
"""
|
||||
}
|
||||
|
||||
return returnData
|
||||
}
|
||||
|
||||
private static func generateBodyDecl(from parameters: FunctionParameters) -> DeclSyntax? {
|
||||
if let body = parameters.getBody() {
|
||||
"""
|
||||
let encoder = JSONEncoder()
|
||||
request.httpBody = try encoder.encode(\(raw: body.name))
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
"""
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func generateHeadersDecl(from parameters: FunctionParameters, defaultHeaders: ExprSyntax?) -> DeclSyntax? {
|
||||
let headers = parameters.getHeaders().map { param in
|
||||
let key = param.attribute.arguments.first?.value ?? param.name
|
||||
return (key: key, value: param.name, optional: param.optional)
|
||||
}
|
||||
|
||||
if headers.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expressions: [String] = []
|
||||
|
||||
if let defaultHeaders {
|
||||
expressions.append("""
|
||||
let defaultHeaders: [String: (any CustomStringConvertible)?] = \(defaultHeaders)
|
||||
defaultHeaders.forEach { request.setValue($0.value?.description, forHTTPHeaderField: $0.key) }
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
expressions.append(contentsOf: headers.map { (key, value, optional) -> String in
|
||||
let setExpr = """
|
||||
headers["\(key)"] = \(value).description
|
||||
"""
|
||||
if optional {
|
||||
return """
|
||||
if let \(value) {
|
||||
\(setExpr)
|
||||
}
|
||||
"""
|
||||
} else {
|
||||
return setExpr
|
||||
}
|
||||
})
|
||||
|
||||
let headerExprs: DeclSyntax = """
|
||||
\(raw: expressions.joined(separator: "\n"))
|
||||
"""
|
||||
|
||||
return headerExprs
|
||||
}
|
||||
|
||||
private static func generateUrlDecl(
|
||||
endpoint endpointExpr: ExprSyntax,
|
||||
queryParams queryParamsDecl: DeclSyntax?
|
||||
) -> DeclSyntax {
|
||||
return if let queryParamsDecl {
|
||||
"""
|
||||
let urlWithPath = \(endpointExpr)
|
||||
var urlComponents = URLComponents(url: urlWithPath, resolvingAgainstBaseURL: false)!
|
||||
var queryItems: [URLQueryItem] = []
|
||||
\(queryParamsDecl)
|
||||
urlComponents.queryItems = queryItems
|
||||
let url = urlComponents.url!
|
||||
"""
|
||||
} else {
|
||||
"""
|
||||
let url = \(endpointExpr)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
private static func generateQueryParamsDecl(from parameters: FunctionParameters) -> DeclSyntax? {
|
||||
let queryParams = parameters.getQueryParams().map { param in
|
||||
let key = param.attribute.arguments.first?.value ?? param.name
|
||||
return (key: key, value: param.name, optional: param.optional)
|
||||
}
|
||||
|
||||
if queryParams.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let queryParamExpr: DeclSyntax = """
|
||||
\(raw: queryParams.map { (key, value, optional) -> String in
|
||||
let appendExpr = """
|
||||
queryItems.append(URLQueryItem(name: "\(key)", value: \(value).description))
|
||||
"""
|
||||
if optional {
|
||||
return """
|
||||
if let \(value) {
|
||||
\(appendExpr)
|
||||
}
|
||||
"""
|
||||
} else {
|
||||
return appendExpr
|
||||
}
|
||||
}.joined(separator: "\n"))
|
||||
"""
|
||||
|
||||
return queryParamExpr
|
||||
}
|
||||
|
||||
private static func generateEndpointExpr(
|
||||
from parameters: FunctionParameters,
|
||||
path: TokenSyntax,
|
||||
pathParams: [String],
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> ExprSyntax {
|
||||
let argumentsPathParams: [(name: String, argument: String)] = parameters.getPathParams().map { pathParam in
|
||||
let name = pathParam.attribute.arguments.first?.value ?? pathParam.name
|
||||
return (name: name, argument: pathParam.name)
|
||||
}
|
||||
|
||||
// Required matching path params
|
||||
guard pathParams.count == argumentsPathParams.count,
|
||||
Set(pathParams) == Set(argumentsPathParams.map(\.name)) else {
|
||||
context.diagnose(diagnostics.pathRequired(node: Syntax(path)))
|
||||
throw MacroExpansionError()
|
||||
}
|
||||
|
||||
// Replace path params in path with arguments
|
||||
let pathString = argumentsPathParams.reduce(path.text) { path, pathParam in
|
||||
path.replacingOccurrences(of: "{\(pathParam.name)}", with: "\\(\(pathParam.argument))")
|
||||
}
|
||||
|
||||
return """
|
||||
baseURL.appendingPathComponent("\(raw: pathString)")
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getRequestParameters(
|
||||
for node: AttributeSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> (path: TokenSyntax, outputType: TypeSyntax, pathParams: [String], defaultsHeaders: ExprSyntax?) {
|
||||
// Require a type as a generic argument
|
||||
guard let outputType = node.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause?.arguments.first?.argumentType else {
|
||||
context.diagnose(RequestMacro.diagnostics.outputTypeRequired(node: Syntax(node)))
|
||||
throw MacroExpansionError()
|
||||
}
|
||||
|
||||
// Require a path without interpolation
|
||||
guard let path = node.argument?.as(TupleExprElementListSyntax.self)?.first?.expression.as(StringLiteralExprSyntax.self)?.segments.first?.as(StringSegmentSyntax.self)?.content else {
|
||||
context.diagnose(diagnostics.pathRequired(node: Syntax(node)))
|
||||
throw MacroExpansionError()
|
||||
}
|
||||
|
||||
// Extract path params
|
||||
let pathParams = path.text
|
||||
.split(separator: "/")
|
||||
.filter { $0.hasPrefix("{") && $0.hasSuffix("}") }
|
||||
.map { $0.dropFirst().dropLast() }
|
||||
.map(String.init)
|
||||
|
||||
// Require unique path params
|
||||
guard Set(pathParams).count == pathParams.count else {
|
||||
context.diagnose(diagnostics.uniquePathParams(node: Syntax(node)))
|
||||
throw MacroExpansionError()
|
||||
}
|
||||
|
||||
// Get the optional defaults headers
|
||||
let defaultHeadersExpr = getExpr(for: "headers", in: node)
|
||||
|
||||
return (path, outputType, pathParams, defaultHeadersExpr)
|
||||
}
|
||||
|
||||
private static func getExpr(for argument: String, in node: AttributeSyntax) -> ExprSyntax? {
|
||||
node.argument?
|
||||
.as(TupleExprElementListSyntax.self)?
|
||||
.first(where: {
|
||||
$0.as(TupleExprElementSyntax.self)?.label?.text == argument
|
||||
})?.expression
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final class GetRequestMacro: RequestMacro {
|
||||
override class var httpMethod: String { "GET" }
|
||||
}
|
||||
|
||||
public final class PostRequestMacro: RequestMacro {
|
||||
override class var httpMethod: String { "POST" }
|
||||
}
|
||||
|
||||
public final class PutRequestMacro: RequestMacro {
|
||||
override class var httpMethod: String { "PUT" }
|
||||
}
|
||||
|
||||
public final class PatchRequestMacro: RequestMacro {
|
||||
override class var httpMethod: String { "PATCH" }
|
||||
}
|
||||
|
||||
public final class DeleteRequestMacro: RequestMacro {
|
||||
override class var httpMethod: String { "DELETE" }
|
||||
}
|
||||
32
Sources/SwiftRequestMacros/ServiceMacro.swift
Normal file
32
Sources/SwiftRequestMacros/ServiceMacro.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxBuilder
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
public struct ServiceMacro: MemberMacro {
|
||||
public static func expansion(
|
||||
of node: AttributeSyntax,
|
||||
providingMembersOf declaration: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [DeclSyntax] {
|
||||
return [
|
||||
"""
|
||||
private let baseURL: URL
|
||||
private let session: URLSession
|
||||
init(baseURL: URL, session: URLSession = .shared) {
|
||||
self.baseURL = baseURL
|
||||
self.session = session
|
||||
}
|
||||
"""
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension ServiceMacro: ConformanceMacro {
|
||||
public static func expansion(
|
||||
of node: AttributeSyntax,
|
||||
providingConformancesOf declaration: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
|
||||
[("Service", nil)]
|
||||
}
|
||||
}
|
||||
8
Sources/SwiftRequestMacros/SimpleDiagnosticMessage.swift
Normal file
8
Sources/SwiftRequestMacros/SimpleDiagnosticMessage.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import SwiftSyntax
|
||||
import SwiftDiagnostics
|
||||
|
||||
struct SimpleDiagnosticMessage: DiagnosticMessage, Error {
|
||||
let message: String
|
||||
let diagnosticID: MessageID
|
||||
let severity: DiagnosticSeverity
|
||||
}
|
||||
14
Sources/SwiftRequestMacros/SwiftRequestPlugin.swift
Normal file
14
Sources/SwiftRequestMacros/SwiftRequestPlugin.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import SwiftCompilerPlugin
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
@main
|
||||
struct SwiftRequestPlugin: CompilerPlugin {
|
||||
let providingMacros: [Macro.Type] = [
|
||||
ServiceMacro.self,
|
||||
GetRequestMacro.self,
|
||||
PostRequestMacro.self,
|
||||
PutRequestMacro.self,
|
||||
PatchRequestMacro.self,
|
||||
DeleteRequestMacro.self
|
||||
]
|
||||
}
|
||||
40
Tests/SwiftRequestTests/RequestTests.swift
Normal file
40
Tests/SwiftRequestTests/RequestTests.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import SwiftSyntaxMacros
|
||||
import SwiftSyntaxMacrosTestSupport
|
||||
import XCTest
|
||||
import SwiftRequestMacros
|
||||
|
||||
final class RequestTests: XCTestCase {
|
||||
let testMacros: [String: Macro.Type] = [
|
||||
"GET": GetRequestMacro.self,
|
||||
"POST": PostRequestMacro.self,
|
||||
"PUT": PutRequestMacro.self,
|
||||
"PATCH": PatchRequestMacro.self,
|
||||
"DELETE": DeleteRequestMacro.self,
|
||||
]
|
||||
|
||||
func testGetMacroWithSimplePathAndData() {
|
||||
assertMacroExpansion(
|
||||
"""
|
||||
@GET<Data>("hello")
|
||||
private func hello() {}
|
||||
""",
|
||||
expandedSource: """
|
||||
|
||||
private func hello() {
|
||||
}
|
||||
func hello() async throws -> (Data, HTTPURLResponse) {
|
||||
let url = baseURL.appendingPathComponent("hello")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
|
||||
guard (200 ..< 300).contains(response.statusCode) else {
|
||||
throw HTTPResponseError(data: data, response: response)
|
||||
}
|
||||
return (data, response)
|
||||
}
|
||||
""",
|
||||
macros: testMacros
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
32
Tests/SwiftRequestTests/ServiceTests.swift
Normal file
32
Tests/SwiftRequestTests/ServiceTests.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import SwiftSyntaxMacros
|
||||
import SwiftSyntaxMacrosTestSupport
|
||||
import XCTest
|
||||
import SwiftRequestMacros
|
||||
|
||||
final class ServiceTests: XCTestCase {
|
||||
let testMacros: [String: Macro.Type] = [
|
||||
"Service": ServiceMacro.self,
|
||||
]
|
||||
|
||||
func testMacro() {
|
||||
assertMacroExpansion(
|
||||
"""
|
||||
@Service
|
||||
class MyService {
|
||||
}
|
||||
""",
|
||||
expandedSource: """
|
||||
|
||||
class MyService {
|
||||
private let baseURL: URL
|
||||
private let session: URLSession
|
||||
init(baseURL: URL, session: URLSession = .shared) {
|
||||
self.baseURL = baseURL
|
||||
self.session = session
|
||||
}
|
||||
}
|
||||
""",
|
||||
macros: testMacros
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user