mirror of
https://github.com/zhigang1992/swift-request.git
synced 2026-01-12 22:52:42 +08:00
Add support to ServiceExecutor
This commit is contained in:
23
Examples/Package.resolved
Normal file
23
Examples/Package.resolved
Normal 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
34
Examples/Package.swift
Normal 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"
|
||||
)
|
||||
]
|
||||
)
|
||||
32
Examples/Sources/AlamofireServiceExecutor.swift
Normal file
32
Examples/Sources/AlamofireServiceExecutor.swift
Normal 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 {}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
48
Sources/SwiftRequest/Request+toURLRequest.swift
Normal file
48
Sources/SwiftRequest/Request+toURLRequest.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,26 @@ import Foundation
|
||||
|
||||
open class Service {
|
||||
public let baseURL: URL
|
||||
public let session: URLSession
|
||||
public let executor: ServiceExecutor
|
||||
|
||||
public init(baseURL: URL, session: URLSession = .shared) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
19
Sources/SwiftRequest/ServiceContainer.swift
Normal file
19
Sources/SwiftRequest/ServiceContainer.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
extension URLSession {
|
||||
public protocol ServiceExecutor {
|
||||
func execute(_ request: Request) async throws -> (Data, HTTPURLResponse)
|
||||
}
|
||||
|
||||
extension ServiceExecutor {
|
||||
public func execute<Output: Decodable>(_ 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)
|
||||
}
|
||||
}
|
||||
18
Sources/SwiftRequest/URLSessionServiceExecutor.swift
Normal file
18
Sources/SwiftRequest/URLSessionServiceExecutor.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,3 @@ class FunctionNamedParameter: FunctionParameter {
|
||||
super.init(syntax)
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionPathParameter: FunctionNamedParameter {
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user