Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make HTTPClientResponse.init public #632

Merged
merged 6 commits into from
Oct 11, 2022

Conversation

dnadoba
Copy link
Collaborator

@dnadoba dnadoba commented Oct 4, 2022

Motivation

We want to allow user to initialise HTTPClientResponse for mocking purposes in tests.

Modifications

  • add AnyAsyncSequence<Element> to erase the type of the user provided AsyncSequences. We need this because we do not want to make HTTPClientResponse.Body generic over the specific AsyncSequence.
  • rename the old implementation of HTTPClientResponse.Body to TransactionBody and make it internal.
  • remove no longer necessary type hiding in TransactionBody as it is internal.
  • add Either<A, B> and conditionally conform it to AsyncSequence if A and B are are AsyncSequences with the same Element type. We use it to still allow the compiler to specialize the HTTPClientResponse.Body default case where the AsyncSequence is of type TransactionBody
  • use Either<TransactionBody, AnyAsyncSeqence<ByteBuffer> as the new storage type for HTTPClientReponse.Body. We still hide the concrete storage type behind a struct.
  • move AsyncSequenceFromSyncSequence from AsyncHTTPClientTests to AsyncHTTPClient and make it inlinable. (we could replace the implementation with AsyncLazySequence from swift-async-algorithms)
  • Add there new public HTTPClientResponse.Body inits:
extension HTTPClientResponse.Body { 
    init() { ... }
    static func bytes(_ byteBuffer: ByteBuffer) { ... }
    static func stream<SequenceOfBytes>(
        _ sequenceOfBytes: SequenceOfBytes
    ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { ... }
}
  • Finally, add a new public init to HTTPClientResponse:
extension HTTPClientReponse {
    init(
        version: HTTPVersion = .http1_1,
        status: HTTPResponseStatus = .ok,
        headers: HTTPHeaders = [:],
        body: Body = Body()
    ) { ... }
}

Result

Users can now freely initialise and modify HTTPClientReponses:

var response = HTTPClientResponse()
response.headers = [
    "Content-Type": "text/html"
]
response.body = .bytes(ByteBuffer(string: "<html> .... </html>"))

@dnadoba dnadoba marked this pull request as draft October 5, 2022 00:15
@Lukasa Lukasa requested a review from FranzBusch October 5, 2022 07:12
Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift Outdated Show resolved Hide resolved
public func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(stream: IteratorStream(bag: self.bag))

@inlinable public init(){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@inlinable public init(){
@inlinable public init() {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to make this a memberwise initializer with these defaults already provided?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We eventually want to add support for trailing headers and this would require us to add another init for it if we want to allow trailing headers in the init too.
However, I'm fine with adding a member wise init already today if you are too. We aren't likely to add any more properties to this struct (famous last words) and one additional init in the future isn't too bad.

/// are entirely synthetic and have no semantic meaning.
public struct Body: AsyncSequence, Sendable {
public typealias Element = ByteBuffer
@usableFromInline typealias Storage = Either<TransactionBody, AnyAsyncSequence<ByteBuffer>>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I think the use of Either is excessively clever, and it leads to some unclear code (case a and case b). I'd rather see a specific-case enum for this use-case.

Copy link
Collaborator

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, this looks like a fine approach to me. However, I would like to discuss the similarities and differences between the approach we took in gRPC and here. In gRPC we are just providing a testing interface which you can use to drive the sequence. Here we allow the user to provider their own AsyncSequence.

The former makes it slightly easier to get started testing since you can just use that interface to yield and finish. The latter provides a bit more customisation because you can drive more than just the next method, i.e. also the makeAsyncIterator.

}
}

@inlinable func makeAsyncIterator() -> AsyncIterator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a different note and from our other learnings, we might want to ensure that only a single iterator is ever created here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do something similar already here:

private func verifyStreamIDIsEqual(
registered: HTTPClientResponse.Body.IteratorStream.ID,
this: HTTPClientResponse.Body.IteratorStream.ID,
file: StaticString = #file,
line: UInt = #line
) {
if registered != this {
preconditionFailure(
"Tried to use a second iterator on response body stream. Multiple iterators are not supported.",
file: file, line: line
)
}
}

This implementation is a bit more clever. It will allow the creation of multiple iterators and will only fail slightly delayed if someone tries to iterate over more than one of them. I think this is actually too clever and we should remove this eventually and just fail early during the creation of a second iterator. I'm not aware of any use case for creating unused iterators but maybe @fabianfett has some in mind.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it was the best thing I came up with at the time. I def. like the atomic approach that we can see here the most so far. SingleIteratorPrecondition. However I'm not sure if we should change this behavior now. Because this would IMO be a breaking change. I just went through this in GRPC...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to agree with Fabian: there's no good sense in breaking the use-case that already exists, even though it'd be better defined if we did.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it back to the old behaviour. However, we now only enforce this on user provided AsyncSequences. Can also change that to use the more relaxed precondition if you want.

@dnadoba
Copy link
Collaborator Author

dnadoba commented Oct 5, 2022

I think that in AHC we usually do not want to write individual ByteBuffers to the stream. My guess is that the most common use case will be to just init the Body with a complete ByteBuffer.

We may also want to think about aligning the Request and Response API in AHC.
A HTTPClientRequest.Body can be created in the following ways:

extension HTTPClientRequest.Body {
    public static func bytes(_ byteBuffer: ByteBuffer) -> Self

    public static func bytes<Bytes: RandomAccessCollection & Sendable>(
        _ bytes: Bytes
    ) -> Self where Bytes.Element == UInt8 {
        Self._bytes(bytes)
    }
    public static func bytes<Bytes: Sequence & Sendable>(
        _ bytes: Bytes,
        length: Length
    ) -> Self where Bytes.Element == UInt8

    public static func bytes<Bytes: Collection & Sendable>(
        _ bytes: Bytes,
        length: Length
    ) -> Self where Bytes.Element == UInt8

    public static func stream<SequenceOfBytes: AsyncSequence & Sendable>(
        _ sequenceOfBytes: SequenceOfBytes,
        length: Length
    ) -> Self where SequenceOfBytes.Element == ByteBuffer
    
    public static func stream<Bytes: AsyncSequence & Sendable>(
        _ bytes: Bytes,
        length: Length
    ) -> Self where Bytes.Element == UInt8
}

We could provide a similar interface for HTTPClientResponse.Body excluding the length parameter.

@dnadoba dnadoba marked this pull request as ready for review October 10, 2022 14:01
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Sequence {
/// Turns `self` into an `AsyncSequence` by vending each element of `self` asynchronously.
@inlinable var async: AsyncLazySequence<Self> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This modifier already exists, doesn't it? I see it is local here, but I assume that we want to make it public eventually...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to make it public. It exists in swift-async-algorithms. However, it doesn't yet have a 1.x release.

@Lukasa Lukasa added the semver/minor For PRs that when merged cause a bump of the minor version, ie. 1.x.0 -> 1.(x+1).0 label Oct 10, 2022

import Atomics

/// Makes sure that a consumer of this `AsyncSequence`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure they what?

}
}

@inlinable func makeAsyncIterator() -> AsyncIterator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to agree with Fabian: there's no good sense in breaking the use-case that already exists, even though it'd be better defined if we did.

@dnadoba dnadoba requested a review from Lukasa October 11, 2022 08:57
@dnadoba dnadoba merged commit d7b69d9 into swift-server:main Oct 11, 2022
@dnadoba dnadoba deleted the dn-response-init branch October 11, 2022 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/minor For PRs that when merged cause a bump of the minor version, ie. 1.x.0 -> 1.(x+1).0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants