Skip to content

Commit

Permalink
refactor(auth): New more easy-use API for Auth (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
rockmagma02 authored Apr 22, 2024
1 parent d5286c4 commit 72ab964
Show file tree
Hide file tree
Showing 22 changed files with 623 additions and 419 deletions.
2 changes: 1 addition & 1 deletion .swiftlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
- multiline_parameters_brackets
- nimble_operator
# - no_extension_access_modifier
- no_grouping_extension
# - no_grouping_extension
- no_magic_numbers
- non_overridable_class_declaration
- nslocalizedstring_key
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/rockmagma02/SyncStream.git", from: "1.1.2"),
.package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"),
],
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.
.target(
name: "HttpX"
name: "HttpX",
dependencies: ["SyncStream"]
),
.testTarget(
name: "HttpXTests",
Expand Down
30 changes: 20 additions & 10 deletions Sources/HttpX/Auth/APIKeyAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
// limitations under the License.

import Foundation
import SyncStream

/// The APIKeyAuth class, user should provide the key.
@available(macOS 10.15, *)
public class APIKeyAuth: BaseAuth {
// MARK: Lifecycle

Expand All @@ -32,18 +32,28 @@ public class APIKeyAuth: BaseAuth {

/// default value is false
public var needRequestBody: Bool { false }

/// default value is false
public var needResponseBody: Bool { false }

public func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) {
if var request {
request.setValue(
key,
forHTTPHeaderField: "x-api-key"
)
return (request, true)
}
throw AuthError.invalidRequest(message: "Request is nil in \(APIKeyAuth.self)")
public func authFlow(
_ request: URLRequest,
continuation: BidirectionalSyncStream<URLRequest, Response, NoneType>.Continuation
) {
var request = request
request.setValue(key, forHTTPHeaderField: "X-Api-Key")
continuation.yield(request)
continuation.return(NoneType())
}

public func authFlow(
_ request: URLRequest,
continuation: BidirectionalAsyncStream<URLRequest, Response, NoneType>.Continuation
) async {
var request = request
request.setValue(key, forHTTPHeaderField: "X-Api-Key")
await continuation.yield(request)
await continuation.return(NoneType())
}

// MARK: Private
Expand Down
172 changes: 117 additions & 55 deletions Sources/HttpX/Auth/BaseAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,144 @@
// limitations under the License.

import Foundation
import SyncStream

// MARK: - BaseAuth

// swiftlint:disable closure_body_length

/// Protocol defining the base authentication flow.
@available(macOS 10.15, *)
public protocol BaseAuth {
/// Indicates if the request body is needed for authentication.
var needRequestBody: Bool { get }
/// Indicates if the response body is needed for authentication.
var needResponseBody: Bool { get }

/// Handles the authentication flow for a given request and last response.
/// The authentication flow. Bidirectional communication with the Client.
///
/// - Parameters:
/// - request: The current URLRequest that needs authentication.
/// - lastResponse: The last URLResponse received from the server.
/// - Returns: A tuple containing the modified URLRequest and a Bool indicating if auth flow is done.
/// - request: The initial request to be authenticated.
/// - continuation: The continuation of the stream, which will communicate with
/// the client. Having method `yield(_:)`, `return(_:)`, `throw(_:)`.
///
/// Basically, The Client will give the initial request to the flow, and the flow can
/// add auth header or do other something, than `yield(_:)` it back to the client.
/// The Client will send the request to the server, and fetch the response back, then
/// send it back to the flow, i.d. the `yield(_:)` will return the response. If the
/// auth progress is over, authFlow can invoke `return(NoneType())` to end the flow,
/// or continue the flow by `yield(_:)` the modified request back to the client. If
/// any error occurred, the flow can throw the error to the client by `throw(_:)`.
///
/// When you creat a custom authentication method, The `authFlow(_:, continuation:)`
/// method should be implemented.
func authFlow(
_ request: URLRequest,
continuation: BidirectionalSyncStream<URLRequest, Response, NoneType>.Continuation
)

/// The Async Version authentication flow. Bidirectional communication with the Client.
///
/// When you create a custom authentication method, you need to implement the `authFlow` method.
/// The method should take two parameters, a `URLRequest` and a `Response`, and return a tuple
/// containing the modified `URLRequest` and a `Bool` value indicating whether the auth is done.
/// - Parameters:
/// - request: The initial request to be authenticated.
/// - continuation: The continuation of the stream, which will communicate with
/// the client. Having method `yield(_:)`, `return(_:)`, `throw(_:)`.
///
/// In some situation, the auth need to use the response of the request to decide the next request.
/// Like the Digest Auth, after you send the first request, the server will return the
/// `WWW-Authenticate` header, and you need to use the header to generate the `Authorization`
/// header for the next request. In this case, you can return `false` in the second value of
/// the tuple to indicate the auth need to use the response.
func authFlow(request: URLRequest?, lastResponse: Response?) throws -> (URLRequest?, Bool)
/// Have the same behavior as the sync version, but the method is async, all interactions
/// with the `continuation` should be `await`. For the custom authentication method, you
/// can simplily copy the sync version `authFlow(_:, continuation:)` to the async version,
/// but add the `await` keyword before the `yield(_:)`, `return(_:)`, `throw(_:)`.
func authFlow(
_ request: URLRequest,
continuation: BidirectionalAsyncStream<URLRequest, Response, NoneType>.Continuation
) async
}

@available(macOS 10.15, *)
extension BaseAuth {
/// Synchronously handles the authentication flow for a given request and last response.
/// This method ensures that if the request body is needed, it is converted from a stream to data.
/// - Parameters:
/// - request: The current URLRequest that needs authentication.
/// - lastResponse: The last URLResponse received from the server.
/// - Returns: A tuple containing the modified URLRequest and a Bool indicating if auth flow is done.
func syncAuthFlow( // swiftlint:disable:this explicit_acl
request: URLRequest?, lastResponse: Response?
) throws -> (URLRequest?, Bool) {
var request = request
let lastResponse = lastResponse
if needRequestBody, request != nil {
if let stream = request!.httpBodyStream {
// read all data from the stream
let data = stream.readAllData()
request!.httpBodyStream = nil
request!.httpBody = data
func authFlowAdapter( // swiftlint:disable:this explicit_acl
_ request: URLRequest
) -> BidirectionalSyncStream<URLRequest, Response, NoneType> {
BidirectionalSyncStream<URLRequest, Response, NoneType> { continuation in
var request = request
var response: Response
if needRequestBody {
if let stream = request.httpBodyStream {
// read all data from the stream
let data = stream.readAllData()
request.httpBodyStream = nil
request.httpBody = data
}
}
let streamAdapter = BidirectionalSyncStream<URLRequest, Response, NoneType> { continuationAdapter in
self.authFlow(request, continuation: continuationAdapter)
}
do {
request = try streamAdapter.next()
} catch {
continuation.throw(error: error)
return
}
while true {
response = continuation.yield(request)
if needResponseBody {
_ = response.getData()
}
do {
request = try streamAdapter.send(response)
} catch {
if error is StopIteration<NoneType> {
break
}
continuation.throw(error: error)
return
}
}
continuation.return(NoneType())
}

if needResponseBody, lastResponse != nil {
_ = lastResponse?.getData()
}

return try authFlow(request: request, lastResponse: lastResponse)
}

func asyncAuthFlow( // swiftlint:disable:this explicit_acl
request: URLRequest?, lastResponse: Response?
) async throws -> (URLRequest?, Bool) {
var request = request
let lastResponse = lastResponse
if needRequestBody, request != nil {
if let stream = request!.httpBodyStream {
// read all data from the stream
let data = stream.readAllData()
request!.httpBodyStream = nil
request!.httpBody = data
func authFlowAdapter( // swiftlint:disable:this explicit_acl
_ request: URLRequest
) async -> BidirectionalAsyncStream<URLRequest, Response, NoneType> {
BidirectionalAsyncStream<URLRequest, Response, NoneType> { continuation in
var request = request
var response: Response
if needRequestBody {
if let stream = request.httpBodyStream {
// read all data from the stream
let data = stream.readAllData()
request.httpBodyStream = nil
request.httpBody = data
}
}
let streamAdapter = BidirectionalAsyncStream<URLRequest, Response, NoneType> { continuaitonAdapter in
await self.authFlow(request, continuation: continuaitonAdapter)
}
do {
request = try await streamAdapter.next()
} catch {
await continuation.throw(error: error)
}
while true {
response = await continuation.yield(request)
if needResponseBody {
do {
_ = try await response.getData()
} catch {
await continuation.throw(error: error)
}
}
do {
request = try await streamAdapter.send(response)
} catch {
if error is StopIteration<NoneType> {
break
}
await continuation.throw(error: error)
}
}
await continuation.return(NoneType())
}

if needResponseBody, lastResponse != nil {
_ = try await lastResponse?.getData()
}

return try authFlow(request: request, lastResponse: lastResponse)
}
}

// swiftlint:enable closure_body_length
41 changes: 28 additions & 13 deletions Sources/HttpX/Auth/BasicAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,16 @@
// limitations under the License.

import Foundation
import SyncStream

/// The BasicAuth class, user should provide the username and password.
@available(macOS 10.15, *)
public class BasicAuth: BaseAuth {
// MARK: Lifecycle

/// Initialize the BasicAuth with username and password.
///
/// - Parameters:
/// - username: The username for the basic auth.
/// - password: The password for the basic auth.
/// - username: The username for the basic auth.
/// - password: The password for the basic auth.
public init(username: String, password: String) {
self.username = username
self.password = password
Expand All @@ -35,18 +34,34 @@ public class BasicAuth: BaseAuth {

/// default value is false
public var needRequestBody: Bool { false }

/// default value is false
public var needResponseBody: Bool { false }

public func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) {
if var request {
request.setValue(
buildAuthHeader(username: username, password: password),
forHTTPHeaderField: "Authorization"
)
return (request, true)
}
throw AuthError.invalidRequest(message: "Request is nil in \(BasicAuth.self)")
public func authFlow(
_ request: URLRequest,
continuation: BidirectionalSyncStream<URLRequest, Response, NoneType>.Continuation
) {
var request = request
request.setValue(
buildAuthHeader(username: username, password: password),
forHTTPHeaderField: "Authorization"
)
continuation.yield(request)
continuation.return(NoneType())
}

public func authFlow(
_ request: URLRequest,
continuation: BidirectionalAsyncStream<URLRequest, Response, NoneType>.Continuation
) async {
var request = request
request.setValue(
buildAuthHeader(username: username, password: password),
forHTTPHeaderField: "Authorization"
)
await continuation.yield(request)
await continuation.return(NoneType())
}

// MARK: Private
Expand Down
Loading

0 comments on commit 72ab964

Please sign in to comment.