Merge pull request #1 from ailtonvivaz/service-executor

Add support to ServiceExecutor
This commit is contained in:
Ailton Vieira Pinto Filho
2023-06-28 20:09:51 -03:00
committed by GitHub
18 changed files with 250 additions and 99 deletions

23
Examples/Package.resolved Normal file
View File

@@ -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
}

34
Examples/Package.swift Normal file
View File

@@ -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"
)
]
)

View File

@@ -0,0 +1,32 @@
import Foundation
import Alamofire
import SwiftRequest
class AlamofireServiceExecutor: ServiceExecutor {
let session: Session
struct Error: Swift.Error {
let response: DataResponse<Data, AFError>
}
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 {}

View File

@@ -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]
}

View File

@@ -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)

View File

@@ -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",

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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))
}
convenience public init(baseURL: String, session: URLSession) {
self.init(baseURL: baseURL, executor: URLSessionServiceExecutor(session))
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,27 @@
import Foundation
public protocol ServiceExecutor {
func execute(_ request: Request) async throws -> (Data, HTTPURLResponse)
}
extension ServiceExecutor {
public func callAsFunction<Output: Decodable>(_ request: Request) async throws -> Output {
let (output, _): (Output, _) = try await self(request)
return output
}
public func callAsFunction<Output: Decodable>(_ request: Request) async throws -> (Output, HTTPURLResponse) {
let (data, response): (Data, HTTPURLResponse) = try await self.execute(request)
let output = try JSONDecoder().decode(Output.self, from: data)
return (output, response)
}
public func callAsFunction(_ request: Request) async throws -> Data {
let (data, _) = try await self(request)
return data
}
public func callAsFunction(_ request: Request) async throws -> (Data, HTTPURLResponse) {
return try await execute(request)
}
}

View File

@@ -1,28 +0,0 @@
import Foundation
extension URLSession {
public func execute<Output: Decodable>(_ request: Request) async throws -> Output {
let (output, _): (Output, _) = try await self.execute(request)
return output
}
public func execute<Output: Decodable>(_ request: Request) async throws -> (Output, HTTPURLResponse) {
let (data, response) = try await self.execute(request)
let output = try JSONDecoder().decode(Output.self, from: data)
return (output, response)
}
public func execute(_ request: Request) async throws -> Data {
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)
}
}

View File

@@ -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)
}
}

View File

@@ -28,7 +28,3 @@ class FunctionNamedParameter: FunctionParameter {
super.init(syntax)
}
}
class FunctionPathParameter: FunctionNamedParameter {
}

View File

@@ -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"
)
}
}

View File

@@ -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(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) }