From 7a71e83ebf0926ad98beec58e3a1ff994afaab42 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 7 Feb 2022 17:52:43 +0100 Subject: [PATCH 1/5] async/await example --- Examples/GetHTML/GetHTML.swift | 40 +++++++++++++ Examples/GetJSON/GetJSON.swift | 56 ++++++++++++++++++ Examples/Package.swift | 57 +++++++++++++++++++ Examples/README.md | 11 ++++ .../StreamingByteCounter.swift | 57 +++++++++++++++++++ .../AsyncAwait/HTTPClientResponse.swift | 7 +++ 6 files changed, 228 insertions(+) create mode 100644 Examples/GetHTML/GetHTML.swift create mode 100644 Examples/GetJSON/GetJSON.swift create mode 100644 Examples/Package.swift create mode 100644 Examples/README.md create mode 100644 Examples/StreamingByteCounter/StreamingByteCounter.swift diff --git a/Examples/GetHTML/GetHTML.swift b/Examples/GetHTML/GetHTML.swift new file mode 100644 index 000000000..05fe72b8b --- /dev/null +++ b/Examples/GetHTML/GetHTML.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// TODO: remove @testable after async/await API is public +@testable import AsyncHTTPClient +import NIOCore + +#if compiler(>=5.5.2) && canImport(_Concurrency) + +@main +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct GetHTML { + static func main() async throws { + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + do { + let request = HTTPClientRequest(url: "https://apple.com") + let response = try await httpClient.execute(request, timeout: .seconds(30)) + print("HTTP head", response) + let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB + print(String(buffer: body)) + } catch { + print("request failed:", error) + } + // it is important to shutdown the httpClient after all requests are done, even if one failed + try await httpClient.shutdown() + } +} + +#endif diff --git a/Examples/GetJSON/GetJSON.swift b/Examples/GetJSON/GetJSON.swift new file mode 100644 index 000000000..87e7b04f0 --- /dev/null +++ b/Examples/GetJSON/GetJSON.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// TODO: remove @testable after async/await API is public +@testable import AsyncHTTPClient +import NIOCore +import Foundation +import NIOFoundationCompat + +#if compiler(>=5.5.2) && canImport(_Concurrency) + +struct Comic: Codable { + var num: Int + var title: String + var day: String + var month: String + var year: String + var img: String + var alt: String + var news: String + var link: String + var transcript: String +} + +@main +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct GetJSON { + static func main() async throws { + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + do { + let request = HTTPClientRequest(url: "https://xkcd.com/info.0.json") + let response = try await httpClient.execute(request, timeout: .seconds(30)) + print("HTTP head", response) + let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB + let comic = try JSONDecoder().decode(Comic.self, from: body) + dump(comic) + } catch { + print("request failed:", error) + } + // it is important to shutdown the httpClient after all requests are done, even if one failed + try await httpClient.shutdown() + } +} + +#endif diff --git a/Examples/Package.swift b/Examples/Package.swift new file mode 100644 index 000000000..8b7984238 --- /dev/null +++ b/Examples/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version:5.5 +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "async-http-client-examples", + products: [ + .executable(name: "GetHTML", targets: ["GetHTML"]), + .executable(name: "GetJSON", targets: ["GetJSON"]), + .executable(name: "StreamingByteCounter", targets: ["StreamingByteCounter"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", .branch("main")), + + // in real-world projects this would be + // .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") + .package(name: "async-http-client", path: "../"), + ], + targets: [ + // MARK: - Examples + .executableTarget( + name: "GetHTML", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOCore", package: "swift-nio"), + ], path: "GetHTML" + ), + .executableTarget( + name: "GetJSON", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ], path: "GetJSON" + ), + .executableTarget( + name: "StreamingByteCounter", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOCore", package: "swift-nio"), + ], path: "StreamingByteCounter" + ), + ] +) diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 000000000..86b3ccafa --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,11 @@ +# Examples +This folder includes a couple of Examples for `AsyncHTTPClient`. +You can run them by opening the `Package.swift` in this folder through Xcode. +In Xcode you can then select the scheme for the example you want run e.g. `GetHTML`. + +You can also run the examples from the command line by executing the follow command in this folder: +``` +swift run GetHTML +``` +To run other examples you can just replace `GetHTML` with the name of the example you want to run. + diff --git a/Examples/StreamingByteCounter/StreamingByteCounter.swift b/Examples/StreamingByteCounter/StreamingByteCounter.swift new file mode 100644 index 000000000..7c35d8243 --- /dev/null +++ b/Examples/StreamingByteCounter/StreamingByteCounter.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// TODO: remove @testable after async/await API is public +@testable import AsyncHTTPClient +import NIOCore + +#if compiler(>=5.5.2) && canImport(_Concurrency) + +@main +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct StreamingByteCounter { + static func main() async throws { + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + do { + let request = HTTPClientRequest(url: "https://apple.com") + let response = try await httpClient.execute(request, timeout: .seconds(30)) + print("HTTP head", response) + + // if defined, the content-length headers announces the size of the body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + + var receivedBytes = 0 + // asynchronously iterates over all body fragments + // this loop will automatically propagate backpressure correctly + for try await buffer in response.body { + // For this example, we are just interested in the size of the fragment + receivedBytes += buffer.readableBytes + + if let expectedBytes = expectedBytes { + // if the body size is known, we calculate a progress indicator + let progress = Double(receivedBytes)/Double(expectedBytes) + print("progress: \(Int(progress * 100))%") + } + // in case backpressure is needed, all reads will be paused until returned future is resolved + } + print("did receive \(receivedBytes) bytes") + } catch { + print("request failed:", error) + } + // it is important to shutdown the httpClient after all requests are done, even if one failed + try await httpClient.shutdown() + } +} + +#endif diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 52f03089b..97657c12a 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -46,6 +46,13 @@ public struct HTTPClientResponse { } } +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClientResponse: CustomStringConvertible { + public var description: String { + return "HTTPClientResponse(version: \(self.version), status: \(self.status), headers: \(self.headers))" + } +} + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body: AsyncSequence { public typealias Element = AsyncIterator.Element From 755012be80c6875a9e6b4e14afcd98640038234e Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Tue, 8 Feb 2022 15:28:24 +0100 Subject: [PATCH 2/5] run SwiftFormat --- Examples/GetJSON/GetJSON.swift | 2 +- Examples/Package.swift | 3 ++- Examples/StreamingByteCounter/StreamingByteCounter.swift | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Examples/GetJSON/GetJSON.swift b/Examples/GetJSON/GetJSON.swift index 87e7b04f0..24f515f10 100644 --- a/Examples/GetJSON/GetJSON.swift +++ b/Examples/GetJSON/GetJSON.swift @@ -14,8 +14,8 @@ // TODO: remove @testable after async/await API is public @testable import AsyncHTTPClient -import NIOCore import Foundation +import NIOCore import NIOFoundationCompat #if compiler(>=5.5.2) && canImport(_Concurrency) diff --git a/Examples/Package.swift b/Examples/Package.swift index 8b7984238..d9041665b 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -24,13 +24,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", .branch("main")), - + // in real-world projects this would be // .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") .package(name: "async-http-client", path: "../"), ], targets: [ // MARK: - Examples + .executableTarget( name: "GetHTML", dependencies: [ diff --git a/Examples/StreamingByteCounter/StreamingByteCounter.swift b/Examples/StreamingByteCounter/StreamingByteCounter.swift index 7c35d8243..7c1a14dac 100644 --- a/Examples/StreamingByteCounter/StreamingByteCounter.swift +++ b/Examples/StreamingByteCounter/StreamingByteCounter.swift @@ -27,20 +27,20 @@ struct StreamingByteCounter { let request = HTTPClientRequest(url: "https://apple.com") let response = try await httpClient.execute(request, timeout: .seconds(30)) print("HTTP head", response) - + // if defined, the content-length headers announces the size of the body let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) - + var receivedBytes = 0 // asynchronously iterates over all body fragments // this loop will automatically propagate backpressure correctly for try await buffer in response.body { // For this example, we are just interested in the size of the fragment receivedBytes += buffer.readableBytes - + if let expectedBytes = expectedBytes { // if the body size is known, we calculate a progress indicator - let progress = Double(receivedBytes)/Double(expectedBytes) + let progress = Double(receivedBytes) / Double(expectedBytes) print("progress: \(Int(progress * 100))%") } // in case backpressure is needed, all reads will be paused until returned future is resolved From f0832bbb541727f03a3a354752370e2b5460be77 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 9 Feb 2022 15:54:54 +0100 Subject: [PATCH 3/5] apply suggested changes from code review --- Examples/GetHTML/GetHTML.swift | 8 +------- Examples/GetJSON/GetJSON.swift | 10 +++------- Examples/Package.swift | 6 ++++++ .../StreamingByteCounter/StreamingByteCounter.swift | 8 +------- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/Examples/GetHTML/GetHTML.swift b/Examples/GetHTML/GetHTML.swift index 05fe72b8b..dfefa922b 100644 --- a/Examples/GetHTML/GetHTML.swift +++ b/Examples/GetHTML/GetHTML.swift @@ -12,14 +12,10 @@ // //===----------------------------------------------------------------------===// -// TODO: remove @testable after async/await API is public -@testable import AsyncHTTPClient +import AsyncHTTPClient import NIOCore -#if compiler(>=5.5.2) && canImport(_Concurrency) - @main -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct GetHTML { static func main() async throws { let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) @@ -36,5 +32,3 @@ struct GetHTML { try await httpClient.shutdown() } } - -#endif diff --git a/Examples/GetJSON/GetJSON.swift b/Examples/GetJSON/GetJSON.swift index 24f515f10..ae58ffeaa 100644 --- a/Examples/GetJSON/GetJSON.swift +++ b/Examples/GetJSON/GetJSON.swift @@ -12,14 +12,11 @@ // //===----------------------------------------------------------------------===// -// TODO: remove @testable after async/await API is public -@testable import AsyncHTTPClient +import AsyncHTTPClient import Foundation import NIOCore import NIOFoundationCompat -#if compiler(>=5.5.2) && canImport(_Concurrency) - struct Comic: Codable { var num: Int var title: String @@ -34,7 +31,6 @@ struct Comic: Codable { } @main -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct GetJSON { static func main() async throws { let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) @@ -43,6 +39,8 @@ struct GetJSON { let response = try await httpClient.execute(request, timeout: .seconds(30)) print("HTTP head", response) let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB + // we use an overload defined in `NIOFoundationCompat` for `decode(_:from:)` to + // efficiently decode from a `ByteBuffer` let comic = try JSONDecoder().decode(Comic.self, from: body) dump(comic) } catch { @@ -52,5 +50,3 @@ struct GetJSON { try await httpClient.shutdown() } } - -#endif diff --git a/Examples/Package.swift b/Examples/Package.swift index d9041665b..58714eb5d 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -17,6 +17,12 @@ import PackageDescription let package = Package( name: "async-http-client-examples", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + ], products: [ .executable(name: "GetHTML", targets: ["GetHTML"]), .executable(name: "GetJSON", targets: ["GetJSON"]), diff --git a/Examples/StreamingByteCounter/StreamingByteCounter.swift b/Examples/StreamingByteCounter/StreamingByteCounter.swift index 7c1a14dac..e8fc07861 100644 --- a/Examples/StreamingByteCounter/StreamingByteCounter.swift +++ b/Examples/StreamingByteCounter/StreamingByteCounter.swift @@ -12,14 +12,10 @@ // //===----------------------------------------------------------------------===// -// TODO: remove @testable after async/await API is public -@testable import AsyncHTTPClient +import AsyncHTTPClient import NIOCore -#if compiler(>=5.5.2) && canImport(_Concurrency) - @main -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct StreamingByteCounter { static func main() async throws { let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) @@ -53,5 +49,3 @@ struct StreamingByteCounter { try await httpClient.shutdown() } } - -#endif From b55bf11646362fbc3debaec374f1658d9c27dcbc Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 9 Feb 2022 16:15:35 +0100 Subject: [PATCH 4/5] remove CustomStringConvertible implementation --- .../AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 97657c12a..52f03089b 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -46,13 +46,6 @@ public struct HTTPClientResponse { } } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension HTTPClientResponse: CustomStringConvertible { - public var description: String { - return "HTTPClientResponse(version: \(self.version), status: \(self.status), headers: \(self.headers))" - } -} - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body: AsyncSequence { public typealias Element = AsyncIterator.Element From 44e6fd4606ae7c32cd8d770695dd2714d2099e7e Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 9 Feb 2022 19:36:40 +0100 Subject: [PATCH 5/5] explain each example --- Examples/README.md | 16 ++++++++++++++++ .../StreamingByteCounter.swift | 1 - 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Examples/README.md b/Examples/README.md index 86b3ccafa..849999f99 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -9,3 +9,19 @@ swift run GetHTML ``` To run other examples you can just replace `GetHTML` with the name of the example you want to run. +## [GetHTML](./GetHTML/GetHTML.swift) + +This examples sends a HTTP GET request to `https://apple.com/` and first `await`s and `print`s the HTTP Response Head. +Afterwards it buffers the full response body in memory and prints the response as a `String`. + +## [GetJSON](./GetJSON/GetJSON.swift) + +This examples sends a HTTP GET request to `https://xkcd.com/info.0.json` and first `await`s and `print`s the HTTP Response Head. +Afterwards it buffers the full response body in memory, decodes the buffer using a `JSONDecoder` and `dump`s the decoded response. + +## [StreamingByteCounter](./StreamingByteCounter/StreamingByteCounter.swift) + +This examples sends a HTTP GET request to `https://apple.com/` and first `await`s and `print`s the HTTP Response Head. +Afterwards it asynchronously iterates over all body fragments, counts the received bytes and prints a progress indicator (if the server send a content-length header). +At the end the total received bytes are printed. +Note that we drop all received fragment and therefore do **not** buffer the whole response body in-memory. diff --git a/Examples/StreamingByteCounter/StreamingByteCounter.swift b/Examples/StreamingByteCounter/StreamingByteCounter.swift index e8fc07861..dc340d14b 100644 --- a/Examples/StreamingByteCounter/StreamingByteCounter.swift +++ b/Examples/StreamingByteCounter/StreamingByteCounter.swift @@ -39,7 +39,6 @@ struct StreamingByteCounter { let progress = Double(receivedBytes) / Double(expectedBytes) print("progress: \(Int(progress * 100))%") } - // in case backpressure is needed, all reads will be paused until returned future is resolved } print("did receive \(receivedBytes) bytes") } catch {