Skip to content

Conversation

@dnadoba
Copy link
Collaborator

@dnadobadnadoba 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:
extensionHTTPClientResponse.Body{init(){...}staticfunc bytes(_ byteBuffer:ByteBuffer){...}staticfunc stream<SequenceOfBytes>( _ sequenceOfBytes:SequenceOfBytes)where SequenceOfBytes:AsyncSequence&Sendable, SequenceOfBytes.Element ==ByteBuffer{...}}
  • Finally, add a new public init to HTTPClientResponse:
extensionHTTPClientReponse{init( version:HTTPVersion=.http1_1, status:HTTPResponseStatus=.ok, headers:HTTPHeaders=[:], body:Body=Body()){...}}

Result

Users can now freely initialise and modify HTTPClientReponses:

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

@dnadobadnadoba marked this pull request as draft October 5, 2022 00:15
@LukasaLukasa requested a review from FranzBuschOctober 5, 2022 07:12
publicfunc makeAsyncIterator()->AsyncIterator{
AsyncIterator(stream:IteratorStream(bag:self.bag))

@inlinablepublicinit(){
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
@inlinablepublicinit(){
@inlinablepublicinit(){

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
CollaboratorAuthor

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.
publicstructBody:AsyncSequence,Sendable{
publictypealiasElement=ByteBuffer
@usableFromInlinetypealiasStorage=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

@FranzBuschFranzBusch 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.

}
}

@inlinablefunc 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
CollaboratorAuthor

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:

privatefunc 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
CollaboratorAuthor

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
CollaboratorAuthor

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:

extensionHTTPClientRequest.Body{publicstaticfunc bytes(_ byteBuffer:ByteBuffer)->Self public static func bytes<Bytes: RandomAccessCollection &Sendable>( _ bytes:Bytes)->Selfwhere Bytes.Element ==UInt8{Self._bytes(bytes)}publicstaticfunc bytes<Bytes:Sequence&Sendable>( _ bytes:Bytes, length:Length)->Selfwhere Bytes.Element ==UInt8 public static func bytes<Bytes: Collection &Sendable>( _ bytes: Bytes, length: Length )-> Self where Bytes.Element == UInt8publicstaticfunc stream<SequenceOfBytes:AsyncSequence&Sendable>( _ sequenceOfBytes:SequenceOfBytes, length:Length)->Selfwhere 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.

@dnadobadnadoba 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,*)
extensionSequence{
/// Turns `self` into an `AsyncSequence` by vending each element of `self` asynchronously.
@inlinablevarasync: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
CollaboratorAuthor

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.

@LukasaLukasa added the 🆕 semver/minor Adds new public API. 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?

}
}

@inlinablefunc 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.

@dnadobadnadoba requested a review from LukasaOctober 11, 2022 08:57
@dnadobadnadoba merged commit d7b69d9 into swift-server:mainOct 11, 2022
@dnadobadnadoba deleted the dn-response-init branch October 11, 2022 09:36
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minorAdds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dnadoba@Lukasa@FranzBusch@fabianfett