From c53cb524cad36d8064d459a266a2ed81173af4bb Mon Sep 17 00:00:00 2001 From: Ailton Vieira Date: Wed, 28 Jun 2023 16:17:04 -0300 Subject: [PATCH] Add support to ServiceExecutor --- Examples/Package.resolved | 23 +++++++++ Examples/Package.swift | 34 +++++++++++++ .../Sources/AlamofireServiceExecutor.swift | 32 +++++++++++++ .../Sources}/Quote.swift | 0 .../Sources}/QuoteService.swift | 0 .../Sources}/TestService.swift | 2 +- .../Sources}/main.swift | 11 ++++- Package.swift | 9 +--- .../SwiftRequest/Request+toURLRequest.swift | 48 +++++++++++++++++++ Sources/SwiftRequest/Request.swift | 46 ------------------ Sources/SwiftRequest/Service.swift | 20 +++++--- Sources/SwiftRequest/ServiceContainer.swift | 19 ++++++++ ...on+execute.swift => ServiceExecutor.swift} | 15 ++---- .../URLSessionServiceExecutor.swift | 18 +++++++ .../FunctionParameter.swift | 4 -- .../MethodDiagnostics.swift | 8 ++++ .../ServiceMethodExpander.swift | 20 ++++++-- 17 files changed, 228 insertions(+), 81 deletions(-) create mode 100644 Examples/Package.resolved create mode 100644 Examples/Package.swift create mode 100644 Examples/Sources/AlamofireServiceExecutor.swift rename {Sources/SwiftRequestClient => Examples/Sources}/Quote.swift (100%) rename {Sources/SwiftRequestClient => Examples/Sources}/QuoteService.swift (100%) rename {Sources/SwiftRequestClient => Examples/Sources}/TestService.swift (63%) rename {Sources/SwiftRequestClient => Examples/Sources}/main.swift (56%) create mode 100644 Sources/SwiftRequest/Request+toURLRequest.swift create mode 100644 Sources/SwiftRequest/ServiceContainer.swift rename Sources/SwiftRequest/{URLSession+execute.swift => ServiceExecutor.swift} (58%) create mode 100644 Sources/SwiftRequest/URLSessionServiceExecutor.swift diff --git a/Examples/Package.resolved b/Examples/Package.resolved new file mode 100644 index 0000000..8c9abae --- /dev/null +++ b/Examples/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "bc268c28fb170f494de9e9927c371b8342979ece", + "version" : "5.7.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "f1e9245226002bb134884345d4809b9543da3666", + "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-17-a" + } + } + ], + "version" : 2 +} diff --git a/Examples/Package.swift b/Examples/Package.swift new file mode 100644 index 0000000..05f2ab2 --- /dev/null +++ b/Examples/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "Examples", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13) + ], + products: [ + .executable( + name: "Examples", + targets: ["Examples"] + ), + ], + dependencies: [ + .package(name: "swift-request", path: "../"), + .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.4")) + ], + targets: [ + .executableTarget( + name: "Examples", + dependencies: [ + .product(name: "SwiftRequest", package: "swift-request"), + .product(name: "Alamofire", package: "alamofire") + ], + path: "Sources" + ) + ] +) diff --git a/Examples/Sources/AlamofireServiceExecutor.swift b/Examples/Sources/AlamofireServiceExecutor.swift new file mode 100644 index 0000000..a899605 --- /dev/null +++ b/Examples/Sources/AlamofireServiceExecutor.swift @@ -0,0 +1,32 @@ +import Foundation +import Alamofire +import SwiftRequest + +class AlamofireServiceExecutor: ServiceExecutor { + let session: Session + + struct Error: Swift.Error { + let response: DataResponse + } + + init(session: Session = .default) { + self.session = session + } + + func execute(_ request: SwiftRequest.Request) async throws -> (Data, HTTPURLResponse) { + let task = session.request(request).serializingData() + let response = await task.response + + if let data = response.data, let urlResponse = response.response { + return (data, urlResponse) + } + + if let error = response.error { + throw error + } + + throw Error(response: response) + } +} + +extension SwiftRequest.Request: URLRequestConvertible {} diff --git a/Sources/SwiftRequestClient/Quote.swift b/Examples/Sources/Quote.swift similarity index 100% rename from Sources/SwiftRequestClient/Quote.swift rename to Examples/Sources/Quote.swift diff --git a/Sources/SwiftRequestClient/QuoteService.swift b/Examples/Sources/QuoteService.swift similarity index 100% rename from Sources/SwiftRequestClient/QuoteService.swift rename to Examples/Sources/QuoteService.swift diff --git a/Sources/SwiftRequestClient/TestService.swift b/Examples/Sources/TestService.swift similarity index 63% rename from Sources/SwiftRequestClient/TestService.swift rename to Examples/Sources/TestService.swift index d676d13..edc131d 100644 --- a/Sources/SwiftRequestClient/TestService.swift +++ b/Examples/Sources/TestService.swift @@ -4,5 +4,5 @@ import Foundation @Service(resource: "sample") protocol SampleService { @POST("hello/{id}/{limit}") - func hello(@Path id: String?, @Path limit: Int) async throws -> [Int] + func hello(@Path id: String, @Path limit: Int) async throws -> [Int] } diff --git a/Sources/SwiftRequestClient/main.swift b/Examples/Sources/main.swift similarity index 56% rename from Sources/SwiftRequestClient/main.swift rename to Examples/Sources/main.swift index 3ed6129..e80b8e3 100644 --- a/Sources/SwiftRequestClient/main.swift +++ b/Examples/Sources/main.swift @@ -1,7 +1,16 @@ import Foundation import SwiftRequest -let service = QuoteServiceImpl(baseURL: "https://api.quotable.io") +let useAlamofire: Bool = Bool.random() +print("Using Alamofire: \(useAlamofire)") + +let executor: any ServiceExecutor = if useAlamofire { + AlamofireServiceExecutor() +} else { + URLSessionServiceExecutor() +} + +let service = QuoteServiceImpl(baseURL: "https://api.quotable.io", executor: executor) do { let quotes = try await service.getRandomQuotes(limit: 3) diff --git a/Package.swift b/Package.swift index 3724f0e..bdb0a0d 100644 --- a/Package.swift +++ b/Package.swift @@ -12,11 +12,7 @@ let package = Package( .library( name: "SwiftRequest", targets: ["SwiftRequest"] - ), - .executable( - name: "SwiftRequestClient", - targets: ["SwiftRequestClient"] - ), + ) ], dependencies: [ // Depend on the latest Swift 5.9 prerelease of SwiftSyntax @@ -37,9 +33,6 @@ let package = Package( // 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", diff --git a/Sources/SwiftRequest/Request+toURLRequest.swift b/Sources/SwiftRequest/Request+toURLRequest.swift new file mode 100644 index 0000000..b881b87 --- /dev/null +++ b/Sources/SwiftRequest/Request+toURLRequest.swift @@ -0,0 +1,48 @@ +import Foundation + +public extension Request { + func asURLRequest() throws -> URLRequest { + let url: URL + if let queryParams { + var components = URLComponents(url: self.url, resolvingAgainstBaseURL: false)! + components.queryItems = queryParams.compactMap { key, value in + guard let value = value else { return nil } + return URLQueryItem(name: key, value: value.description) + } + + url = components.url! + } else { + url = self.url + } + + var request = URLRequest(url: url) + request.httpMethod = self.method + + if let formFields = self.formFields { + var form = URLComponents() + form.queryItems = formFields.compactMap { key, value in + guard let value = value else { return nil } + return URLQueryItem(name: key, value: value.description) + } + + request.httpBody = form.query?.data(using: .utf8) + } + + if let body { + if body is Data { + request.httpBody = body as? Data + } else { + request.httpBody = try JSONEncoder().encode(body) + } + } + + if let headers { + for (key, value) in headers { + guard let value = value else { continue } + request.addValue(value.description, forHTTPHeaderField: key) + } + } + + return request + } +} diff --git a/Sources/SwiftRequest/Request.swift b/Sources/SwiftRequest/Request.swift index 4c960aa..013d938 100644 --- a/Sources/SwiftRequest/Request.swift +++ b/Sources/SwiftRequest/Request.swift @@ -25,50 +25,4 @@ public struct Request { self.formFields = formFields self.body = body } - - func getURLRequest() throws -> URLRequest { - let url: URL - if let queryParams { - var components = URLComponents(url: self.url, resolvingAgainstBaseURL: false)! - components.queryItems = queryParams.compactMap { key, value in - guard let value = value else { return nil } - return URLQueryItem(name: key, value: value.description) - } - - url = components.url! - } else { - url = self.url - } - - var request = URLRequest(url: url) - request.httpMethod = self.method - - if let formFields = self.formFields { - var form = URLComponents() - form.queryItems = formFields.compactMap { key, value in - guard let value = value else { return nil } - return URLQueryItem(name: key, value: value.description) - } - - request.httpBody = form.query?.data(using: .utf8) - } - - if let body { - if body is Data { - request.httpBody = body as? Data - } else { - request.httpBody = try JSONEncoder().encode(body) - } - } - - if let headers { - for (key, value) in headers { - guard let value = value else { continue } - request.addValue(value.description, forHTTPHeaderField: key) - } - } - - return request - } } - diff --git a/Sources/SwiftRequest/Service.swift b/Sources/SwiftRequest/Service.swift index e131431..cde954a 100644 --- a/Sources/SwiftRequest/Service.swift +++ b/Sources/SwiftRequest/Service.swift @@ -2,18 +2,26 @@ import Foundation open class Service { public let baseURL: URL - public let session: URLSession - - public init(baseURL: URL, session: URLSession = .shared) { + public let executor: ServiceExecutor + + public init(baseURL: URL, executor: ServiceExecutor = URLSessionServiceExecutor()) { self.baseURL = baseURL - self.session = session + self.executor = executor } - convenience public init(baseURL: String, session: URLSession = .shared) { + convenience public init(baseURL: String, executor: ServiceExecutor = URLSessionServiceExecutor()) { guard let baseURL = URL(string: baseURL) else { fatalError("Invalid baseURL") } - self.init(baseURL: baseURL, session: session) + self.init(baseURL: baseURL, executor: executor) + } + + convenience public init(baseURL: URL, session: URLSession) { + self.init(baseURL: baseURL, executor: URLSessionServiceExecutor(session: session)) + } + + convenience public init(baseURL: String, session: URLSession) { + self.init(baseURL: baseURL, executor: URLSessionServiceExecutor(session: session)) } } diff --git a/Sources/SwiftRequest/ServiceContainer.swift b/Sources/SwiftRequest/ServiceContainer.swift new file mode 100644 index 0000000..a2cdb64 --- /dev/null +++ b/Sources/SwiftRequest/ServiceContainer.swift @@ -0,0 +1,19 @@ +import Foundation + +public class ServiceContainer { + let baseURL: URL + let session: URLSession + + public init(baseURL: URL, session: URLSession = .shared) { + self.baseURL = baseURL + self.session = session + } + + convenience public init(baseURL: String, session: URLSession = .shared) { + guard let baseURL = URL(string: baseURL) else { + fatalError("Invalid baseURL") + } + + self.init(baseURL: baseURL, session: session) + } +} diff --git a/Sources/SwiftRequest/URLSession+execute.swift b/Sources/SwiftRequest/ServiceExecutor.swift similarity index 58% rename from Sources/SwiftRequest/URLSession+execute.swift rename to Sources/SwiftRequest/ServiceExecutor.swift index 72764cc..1c872df 100644 --- a/Sources/SwiftRequest/URLSession+execute.swift +++ b/Sources/SwiftRequest/ServiceExecutor.swift @@ -1,6 +1,10 @@ import Foundation -extension URLSession { +public protocol ServiceExecutor { + func execute(_ request: Request) async throws -> (Data, HTTPURLResponse) +} + +extension ServiceExecutor { public func execute(_ request: Request) async throws -> Output { let (output, _): (Output, _) = try await self.execute(request) return output @@ -16,13 +20,4 @@ extension URLSession { let (data, _) = try await self.execute(request) return data } - - public func execute(_ request: Request) async throws -> (Data, HTTPURLResponse) { - let urlRequest = try request.getURLRequest() - let (data, response) = try await URLSession.shared.data(for: urlRequest) as! (Data, HTTPURLResponse) - guard (200..<300).contains(response.statusCode) else { - throw HTTPResponseError(data: data, response: response) - } - return (data, response) - } } diff --git a/Sources/SwiftRequest/URLSessionServiceExecutor.swift b/Sources/SwiftRequest/URLSessionServiceExecutor.swift new file mode 100644 index 0000000..9d80954 --- /dev/null +++ b/Sources/SwiftRequest/URLSessionServiceExecutor.swift @@ -0,0 +1,18 @@ +import Foundation + +public class URLSessionServiceExecutor: ServiceExecutor { + private let session: URLSession + + public init(session: URLSession = .shared) { + self.session = session + } + + public func execute(_ request: Request) async throws -> (Data, HTTPURLResponse) { + let urlRequest = try request.asURLRequest() + let (data, response) = try await session.data(for: urlRequest) as! (Data, HTTPURLResponse) + guard (200..<300).contains(response.statusCode) else { + throw HTTPResponseError(data: data, response: response) + } + return (data, response) + } +} diff --git a/Sources/SwiftRequestMacros/FunctionParameter.swift b/Sources/SwiftRequestMacros/FunctionParameter.swift index 9460a8f..5243ef5 100644 --- a/Sources/SwiftRequestMacros/FunctionParameter.swift +++ b/Sources/SwiftRequestMacros/FunctionParameter.swift @@ -28,7 +28,3 @@ class FunctionNamedParameter: FunctionParameter { super.init(syntax) } } - -class FunctionPathParameter: FunctionNamedParameter { - -} diff --git a/Sources/SwiftRequestMacros/MethodDiagnostics.swift b/Sources/SwiftRequestMacros/MethodDiagnostics.swift index 27b02a3..2ded409 100644 --- a/Sources/SwiftRequestMacros/MethodDiagnostics.swift +++ b/Sources/SwiftRequestMacros/MethodDiagnostics.swift @@ -91,4 +91,12 @@ struct MethodDiagnostics: Diagnostics { id: "tooManyBodyParameters" ) } + + func optionalParameterNotSupported(in node: FunctionParameterSyntax, attribute: String) -> Diagnostic { + diagnostic( + for: node, + message: "@\(attribute) does not support optional parameters", + id: "optionalParameterNotSupported" + ) + } } diff --git a/Sources/SwiftRequestMacros/ServiceMethodExpander.swift b/Sources/SwiftRequestMacros/ServiceMethodExpander.swift index 7395057..6347c02 100644 --- a/Sources/SwiftRequestMacros/ServiceMethodExpander.swift +++ b/Sources/SwiftRequestMacros/ServiceMethodExpander.swift @@ -52,7 +52,7 @@ class ServiceMethodExpander { let codeBlock = CodeBlockSyntax { VariableDeclSyntax(Keyword.let, name: "request", initializer: .init(value: request)) - ReturnStmtSyntax(expression: "try await session.execute(request)" as ExprSyntax) + ReturnStmtSyntax(expression: "try await executor.execute(request)" as ExprSyntax) } let newDeclaration = declaration @@ -113,7 +113,7 @@ class ServiceMethodExpander { of declaration: FunctionDeclSyntax, in context: some MacroExpansionContext ) -> TokenSyntax? { - let parameters: [FunctionPathParameter] = getParameters(from: declaration, with: "Body") + let parameters: [FunctionNamedParameter] = getParameters(from: declaration, with: "Body") guard parameters.count <= 1 else { context.diagnose(diagnostics.tooManyBodyParameters(in: declaration)) @@ -132,7 +132,7 @@ class ServiceMethodExpander { of declaration: FunctionDeclSyntax, in context: some MacroExpansionContext ) -> DictionaryExprSyntax? { - let parameters: [FunctionPathParameter] = getParameters(from: declaration, with: name) + let parameters: [FunctionNamedParameter] = getParameters(from: declaration, with: name) if parameters.isEmpty { return nil @@ -157,7 +157,18 @@ class ServiceMethodExpander { ) -> ExprSyntax? { let arguments = attribute.argument?.as(TupleExprElementListSyntax.self) - let parameters: [FunctionPathParameter] = getParameters(from: declaration, with: "Path") + let parameters: [FunctionNamedParameter] = getParameters(from: declaration, with: "Path") + + for parameter in parameters { + if parameter.optional { + context.diagnose( + diagnostics.optionalParameterNotSupported( + in: parameter.syntax, + attribute: "Path" + ) + ) + } + } guard let path = arguments?.first?.expression else { guard parameters.isEmpty else { @@ -180,7 +191,6 @@ class ServiceMethodExpander { let replacementBlocks = getReplacementBlocks(from: path) - let parameterNames = parameters.map(\.name) let missingParameters = replacementBlocks.filter { !parameterNames.contains($0) }