├── .github └── workflows │ └── docs.yml ├── .gitignore ├── .swift-format ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Snippets ├── HummingbirdApp.swift ├── HummingbirdRequestContext.swift ├── advanced-async-sequences.swift ├── ahc_download.swift ├── ahc_json.swift ├── ahc_request.swift ├── ahc_setup.swift ├── beginners-guide-to-protocol-buffers-and-grpc-with-swift │ ├── TodoService.swift │ ├── todo_messages.proto │ └── todo_service.proto ├── building-swiftnio-clients-01.swift ├── environment.swift ├── getting-started-concurrency-buy-books.swift ├── getting-started-concurrency-dispatch.swift ├── getting-started-concurrency-imagecache-locks.swift ├── hummingbird-2-proxy.swift ├── hummingbird-2-routerbuilder.swift ├── hummingbird-2.swift ├── hummingbird-and-cors.swift ├── jwt-kit.swift ├── mongokitten-basics.swift ├── realtime-mongodb-app.swift ├── shared-state.swift ├── swiftpm-snippets │ ├── SnippetsExample_I.swift │ ├── SnippetsExample_II.swift │ ├── SnippetsExample_III.swift │ ├── SnippetsExample_IV.swift │ └── SnippetsExample_V.swift ├── using-swiftnio-channels.swift ├── websockets-app.swift ├── websockets-connection-manager-add-user.swift ├── websockets-connection-manager-types.swift ├── websockets.swift └── working-with-udp-in-swiftnio.swift ├── Sources ├── Articles │ ├── Documentation.docc │ │ ├── 2024 │ │ │ ├── advanced-async-sequences │ │ │ │ └── advanced-async-sequences.md │ │ │ ├── async-http-client-by-example │ │ │ │ └── async-http-client-by-example.md │ │ │ ├── beginners-guide-to-protocol-buffers-and-grpc-with-swift │ │ │ │ └── beginners-guide-to-protocol-buffers-and-grpc-with-swift.md │ │ │ ├── building-swiftnio-clients │ │ │ │ └── building-swiftnio-clients.md │ │ │ ├── developing-with-swift-in-visual-studio-code │ │ │ │ ├── developing-with-swift-in-visual-studio-code.md │ │ │ │ └── images │ │ │ │ │ ├── open-in-container.png │ │ │ │ │ ├── open-in-container~dark.png │ │ │ │ │ ├── program-output.png │ │ │ │ │ └── program-output~dark.png │ │ │ ├── getting-started-with-hummingbird │ │ │ │ └── getting-started-with-hummingbird.md │ │ │ ├── getting-started-with-mongokitten │ │ │ │ └── getting-started-with-mongokitten.md │ │ │ ├── getting-started-with-structured-concurrency-in-swift │ │ │ │ └── getting-started-with-structured-concurrency-in-swift.md │ │ │ ├── getting-started-with-swift-package-manager │ │ │ │ └── getting-started-with-swift-package-manager.md │ │ │ ├── getting-started-with-swiftpm-snippets │ │ │ │ ├── Manifest.1.swift │ │ │ │ ├── Manifest.2.swift │ │ │ │ ├── Manifest.3.swift │ │ │ │ ├── My article (1).md.txt │ │ │ │ ├── My article (2).md.txt │ │ │ │ ├── My article (3).md.txt │ │ │ │ ├── Snippets │ │ │ │ └── getting-started-with-swiftpm-snippets.md │ │ │ ├── how-to-build-a-proxy-server-with-hummingbird │ │ │ │ └── how-to-build-a-proxy-server-with-hummingbird.md │ │ │ ├── introduction-to-swift-service-lifecycle │ │ │ │ └── introduction-to-swift-service-lifecycle.md │ │ │ ├── logging-for-server-side-swift-apps │ │ │ │ └── logging-for-server-side-swift-apps.md │ │ │ ├── realtime-mongodb-updates-with-changestreams-and-websockets │ │ │ │ └── realtime-mongodb-updates-with-changestreams-and-websockets.md │ │ │ ├── structured-concurrency-and-shared-state-in-swift │ │ │ │ └── structured-concurrency-and-shared-state-in-swift.md │ │ │ ├── templating-with-swift-mustache │ │ │ │ └── templating-with-swift-mustache.md │ │ │ ├── useful-scripts-for-server-side-swift-libraries │ │ │ │ └── useful-scripts-for-server-side-swift-libraries.md │ │ │ ├── using-environment-variables-in-swift │ │ │ │ ├── images │ │ │ │ │ ├── edit-scheme-in-xcode.png │ │ │ │ │ ├── edit-scheme-in-xcode~dark.png │ │ │ │ │ ├── set-env-in-vscode.png │ │ │ │ │ ├── set-env-in-vscode~dark.png │ │ │ │ │ ├── set-env-in-xcode.png │ │ │ │ │ └── set-env-in-xcode~dark.png │ │ │ │ └── using-environment-variables-in-swift.md │ │ │ ├── using-hummingbird-request-context │ │ │ │ └── using-hummingbird-request-context.md │ │ │ ├── using-openapi-with-hummingbird │ │ │ │ └── using-openapi-with-hummingbird.md │ │ │ ├── using-swiftnio-channels │ │ │ │ └── using-swiftnio-channels.md │ │ │ ├── using-swiftnio-fundamentals │ │ │ │ └── using-swiftnio-fundamentals.md │ │ │ ├── websockets-tutorial-using-swift-and-hummingbird │ │ │ │ └── websockets-tutorial-using-swift-and-hummingbird.md │ │ │ ├── whats-new-in-hummingbird-2 │ │ │ │ └── whats-new-in-hummingbird-2.md │ │ │ └── working-with-udp-in-swiftnio │ │ │ │ └── working-with-udp-in-swiftnio.md │ │ └── 2025 │ │ │ ├── faster-github-actions-ci-for-swift-projects │ │ │ ├── faster-github-actions-ci-for-swift-projects.md │ │ │ └── images │ │ │ │ ├── delete-caches-in-github-ui.png │ │ │ │ ├── delete-caches-in-github-ui~dark.png │ │ │ │ ├── deploy-ci-initial.png │ │ │ │ ├── deploy-ci-with-cache.png │ │ │ │ ├── tests-ci-initial.png │ │ │ │ ├── tests-ci-optimized-separated-steps.png │ │ │ │ ├── tests-ci-separated-steps.png │ │ │ │ ├── tests-ci-with-cache.png │ │ │ │ ├── tests-ci-with-zstd.png │ │ │ │ ├── trigger-debug-rerun-in-github-ui.png │ │ │ │ └── trigger-debug-rerun-in-github-ui~dark.png │ │ │ ├── hummingbird-and-cors │ │ │ └── hummingbird-and-cors.md │ │ │ └── jwt-kit │ │ │ └── jwt-kit.md │ └── empty.swift └── SnippetsExample │ └── empty.swift └── scripts ├── check-broken-symlinks.sh ├── check-local-swift-dependencies.sh ├── check-unacceptable-language.sh ├── install-swift-format.sh ├── run-checks.sh ├── run-chmod.sh ├── run-swift-format.sh └── unacceptable-language.txt /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This workflow validates the package’s documentation. Because documentation building involves 2 | # compiling the package, this also checks that the package itself compiles successfully on each 3 | # supported platform. 4 | name: documentation 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | linux: 14 | runs-on: ubuntu-24.04 15 | name: Ubuntu 16 | 17 | steps: 18 | - name: Install Swift 19 | uses: tayloraswift/swift-install-action@master 20 | with: 21 | swift-prefix: "swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE" 22 | swift-id: "swift-6.0.3-RELEASE-ubuntu24.04" 23 | 24 | - name: Install Unidoc 25 | uses: tayloraswift/swift-unidoc-action@master 26 | 27 | # This clobbers everything in the current directory! 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Validate documentation 32 | run: | 33 | unidoc compile -I .. \ 34 | --swift-toolchain $SWIFT_INSTALLATION \ 35 | --ci fail-on-errors \ 36 | --package-name articles 37 | 38 | macos: 39 | runs-on: macos-15 40 | name: macOS 41 | steps: 42 | - name: Install Unidoc 43 | uses: tayloraswift/swift-unidoc-action@master 44 | 45 | - name: Checkout repository 46 | uses: actions/checkout@v3 47 | 48 | - name: Validate documentation 49 | env: 50 | DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer 51 | run: | 52 | unidoc compile -I .. \ 53 | --ci fail-on-errors \ 54 | --package-name articles \ 55 | --define DARWIN 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /.swiftpm 3 | /.vscode 4 | /.build 5 | /.build.ssgc 6 | /.ssgc 7 | /Sources/**/*.html 8 | /.build.ssgc 9 | /.index-build 10 | docc 11 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 80, 4 | "maximumBlankLines": 1, 5 | "fileScopedDeclarationPrivacy": { 6 | "accessLevel": "private" 7 | }, 8 | "tabWidth": 4, 9 | "indentation": { 10 | "spaces": 4 11 | }, 12 | "indentConditionalCompilationBlocks": false, 13 | "indentSwitchCaseLabels": false, 14 | "lineBreakAroundMultilineExpressionChainComponents": true, 15 | "lineBreakBeforeControlFlowKeywords": true, 16 | "lineBreakBeforeEachArgument": true, 17 | "lineBreakBeforeEachGenericRequirement": true, 18 | "prioritizeKeepingFunctionOutputTogether": false, 19 | "respectsExistingLineBreaks": true, 20 | "spacesAroundRangeFormationOperators": false, 21 | "multiElementCollectionTrailingCommas": true, 22 | "rules": { 23 | "AllPublicDeclarationsHaveDocumentation": false, 24 | "AlwaysUseLiteralForEmptyCollectionInit": true, 25 | "AlwaysUseLowerCamelCase": false, 26 | "AmbiguousTrailingClosureOverload": true, 27 | "BeginDocumentationCommentWithOneLineSummary": true, 28 | "DoNotUseSemicolons": true, 29 | "DontRepeatTypeInStaticProperties": true, 30 | "FileScopedDeclarationPrivacy": true, 31 | "FullyIndirectEnum": true, 32 | "GroupNumericLiterals": true, 33 | "IdentifiersMustBeASCII": true, 34 | "NeverForceUnwrap": false, 35 | "NeverUseForceTry": true, 36 | "NeverUseImplicitlyUnwrappedOptionals": true, 37 | "NoAccessLevelOnExtensionDeclaration": true, 38 | "NoAssignmentInExpressions": true, 39 | "NoBlockComments": true, 40 | "NoCasesWithOnlyFallthrough": true, 41 | "NoEmptyTrailingClosureParentheses": true, 42 | "NoLabelsInCasePatterns": true, 43 | "NoLeadingUnderscores": true, 44 | "NoParensAroundConditions": true, 45 | "NoPlaygroundLiterals": true, 46 | "NoVoidReturnOnFunctionSignature": true, 47 | "OmitExplicitReturns": true, 48 | "OneCasePerLine": true, 49 | "OneVariableDeclarationPerLine": true, 50 | "OnlyOneTrailingClosureArgument": true, 51 | "OrderedImports": true, 52 | "ReplaceForEachWithForLoop": true, 53 | "ReturnVoidInsteadOfEmptyTuple": true, 54 | "TypeNamesShouldBeCapitalized": true, 55 | "UseEarlyExits": true, 56 | "UseLetInEveryBoundCaseVariable": true, 57 | "UseShorthandTypeNames": true, 58 | "UseSingleLinePropertyGetter": true, 59 | "UseSynthesizedInitializer": true, 60 | "UseTripleSlashForDocumentationComments": true, 61 | "UseWhereClausesInForLoops": true, 62 | "ValidateDocumentationComments": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tibor Bödecs (Binary Birds Ltd.) 4 | Copyright (c) 2024 Joannis Orlandos (Unbeatable Software B.V.) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | build: 4 | swift build 5 | 6 | release: 7 | swift build -c release 8 | 9 | test: 10 | swift test --parallel 11 | 12 | test-with-coverage: 13 | swift test --parallel --enable-code-coverage 14 | 15 | clean: 16 | rm -rf .build 17 | 18 | check: 19 | ./scripts/run-checks.sh 20 | 21 | format: 22 | ./scripts/run-swift-format.sh --fix 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "swiftonserver-articles", 6 | platforms: [ 7 | .macOS(.v15), 8 | ], 9 | products: [ 10 | .library(name: "Articles", targets: ["Articles"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), 14 | .package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"), 15 | .package(url: "https://github.com/hummingbird-project/hummingbird-websocket.git", from: "2.0.0"), 16 | .package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta.3"), 17 | .package(url: "https://github.com/hummingbird-project/swift-jobs.git", from: "1.0.0-beta.6"), 18 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.21.1"), 19 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), 20 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), 21 | .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.20.0"), 22 | .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), 23 | .package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"), 24 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.5.0"), 25 | .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), 26 | .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), 27 | .package(url: "https://github.com/orlandos-nl/MongoKitten.git", from: "7.0.0"), 28 | .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), 29 | ], 30 | targets: [ 31 | .target( 32 | name: "Articles", 33 | dependencies: [ 34 | .product(name: "Hummingbird", package: "hummingbird"), 35 | .product(name: "HummingbirdRouter", package: "hummingbird"), 36 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 37 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 38 | .product(name: "Jobs", package: "swift-jobs"), 39 | .product(name: "NIOCore", package: "swift-nio"), 40 | .product(name: "NIOPosix", package: "swift-nio"), 41 | .product(name: "Logging", package: "swift-log"), 42 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), 43 | .product(name: "HummingbirdAuth", package: "hummingbird-auth"), 44 | .product(name: "HummingbirdWebSocket", package: "hummingbird-websocket"), 45 | .product(name: "HummingbirdWSCompression", package: "hummingbird-websocket"), 46 | .product(name: "Mustache", package: "swift-mustache"), 47 | .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), 48 | .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), 49 | .product(name: "NIOHTTPTypes", package: "swift-nio-extras"), 50 | .product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"), 51 | .product(name: "NIOExtras", package: "swift-nio-extras"), 52 | .product(name: "MongoKitten", package: "MongoKitten"), 53 | .product(name: "JWTKit", package: "jwt-kit"), 54 | ], 55 | packageAccess: true 56 | ), 57 | .target( 58 | name: "SnippetsExample" 59 | ), 60 | ] 61 | ) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift on server articles 2 | 3 | Articles for the [swiftonserver.com](https://swiftonserver.com/) site. 4 | 5 | ## Writing Articles 6 | 7 | To write an article for the site, you'll need to write it in markdown in the DocC documentation of the `Articles` module: https://github.com/swift-on-server/articles/tree/main/Sources/Articles/Documentation.docc/2024 8 | 9 | The folders are structured by `/`. If you're just starting with an article, the slug doesn't matter too much. It'll be tweaked before publication. 10 | 11 | Inside each article folder, you should put a markdown file with the same name. E.g. `whats-new-in-hummingbird/whats-new-in-hummingbird.md`. 12 | 13 | This is _mostly_ a regular markdown file representing your article, with two notable exceptions. 14 | 15 | ### Symbol Links 16 | 17 | Our rendering engine, [swiftinit](https://swiftinit.org), can link DocC symbol links to any dependency of our project. As such, all mentions of types or functions should use two backticks rather than one. 18 | 19 | ``` 20 | ``ByteBuffer`` 21 | ``` 22 | 23 | You can also render functions and properties that are members of types. 24 | 25 | ``` 26 | ``TaskGroup.addTask(priority:operation:)`` 27 | ``` 28 | 29 | ### Swift Snippets 30 | 31 | Rather than writing sample code in markdown code blocks, our site uses Swift Snippets to display Swift code. 32 | 33 | A Swift Snippet is a Swift source file that is compiled and can use all of the project's dependencies. The main benefit of Snippets is that that because they're compiled, any build of the Articles folder guarantees that the samples are valid and compile with the latest Swift release. 34 | 35 | Users can copy & paste source code without worry of it breaking. And when the project's dependencies change major release, CI knows if all articles are up-to-date and work. 36 | 37 | Because Swift Snippets are compiled, users can download the articles repository to run the snippet's code in Xcode or VSCode. 38 | 39 | Finally, _because_ Swift snippets are compiled, the SwiftOnServer.com website can leverage [swiftinit](https://swiftinit.org) to integrate the linked source code into their documentation engine. This allows us to provide enriched metadata for the code that is displayed. 40 | 41 | As of writing, that includes linking types, functions and properties to their corresponding documentation through an anchor, as well as on-hover tooltips. 42 | 43 | If a tutorial uses one snippet, you can create a snippet with the tutorial's slug in the [Snippets folder](https://github.com/swift-on-server/articles/tree/main/Snippets). 44 | 45 | If a tutorial uses multiple snippets, you can create a folder with your tutorial's slug instead, and put any snippets needed inside that folder. 46 | 47 | #### Using Snippets 48 | 49 | DocC uses the following format to locate a snippet: 50 | 51 | ``` 52 | @Snippet(path: "site/Snippets/hummingbird-2") 53 | ``` 54 | 55 | - `site/` is mandatory, and refers to the current Swift package by package name. 56 | - `Snippets/` is the folder where we store our snippets 57 | - Optionally, and subfolders of `Snippets/` can be added in between 58 | - `hummingbird-2` refers to the snippet file. Do not put a `.swift` extensionh ere. 59 | 60 | If you _don't_ want to render certain example code in the tutorial, you can mark the start of hidden lines of code as such: 61 | 62 | ```swift 63 | // snippet.hide 64 | ``` 65 | 66 | Then re-enable rendering as such: 67 | 68 | ```swift 69 | // snippet.show 70 | ``` 71 | 72 | [Example of hiding and showing](https://github.com/swift-on-server/articles/blob/main/Snippets/ahc_json.swift#L1-L10) 73 | 74 | You can also add a label (other than hide/show/end) to a snippet: 75 | 76 | ```swift 77 | // snippet.LABEL 78 | ``` 79 | 80 | And you can end that labeled block as such: 81 | 82 | ```swift 83 | // snippet.end 84 | ``` 85 | 86 | [Example of labels](https://github.com/swift-on-server/articles/blob/main/Snippets/building-swiftnio-clients-01.swift#L1-L6) 87 | 88 | You can refer to a labeled block (called a "slice") in your tutorial's code: 89 | 90 | ``` 91 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "imports") 92 | ``` 93 | 94 | ### Planning an Article (recommended) 95 | 96 | The recommended way to start an article is to create an _outline_ first. It's basically a bullet-point list of chapters (h1, h2, h3). 97 | 98 | I recommend adding a one-word tag per bullet-point. Some recommendations for tags: 99 | 100 | - intro 101 | - theory (used when you need to front-load information) 102 | - practice (when people get to dive into code) 103 | - conclusion (recap; what did you learn?) 104 | 105 | Here's an example outline: 106 | 107 | ```md 108 | ## Getting Started with Hummingbird 2 109 | 110 | - What is Hummingbird? [intro] 111 | - Adding the Dependency [practice] 112 | - Hummingbird and Service Lifecycle [theory] 113 | - Creating a Web Server [practice] 114 | - Running Your App [practice] 115 | - What are Routes? [theory] 116 | - Adding a Route [practice] 117 | - Responding to Requests [practice] 118 | - Where to go from here? [conclusion] 119 | ``` 120 | 121 | As a general guideline, I recommend showing only one theory block at a time, separated by practice. However, some articles are theoretical in nature. 122 | 123 | Practical blocks should preferably end with a moment of reflection, where the user can check if their code is working as expected. This can be a screenshot of a webbrowser or code block displaying the expected terminal output of the tutorial. 124 | 125 | ### Writing the Sample Code 126 | 127 | I normally start with sample code first, as programmers naturally gravitate towards code that they want to explain or discuss. 128 | 129 | Ideally you should keep the sample code constrained within snippets. As snippets are single Swift files, it pushes you to keep the topics simple to keep track of. 130 | 131 | However, Swift Snippets don't have a line limit. So feel free to add a lot of (hidden) code in one snippet file. 132 | 133 | ### Writing the Tutorial 134 | 135 | If you've written the sample code, you can copy the outline and code snippets in your editor. Since we're using DocC, you can use the DocC plugin or the Xcode "Build Documentation" tool to view your changes. SwiftInit also has a preview tool that you can use. 136 | 137 | My recommendation is not to be very critical on the first draft. Write what you want and what you think, and clean it up from there. Every time you read your article, you'll find new things that are missing, not explained, in the wrong order or simply too difficult to follow. 138 | 139 | ### Polishing 140 | 141 | Once your story makes sense, you can create a draft PR, tagging [Joannis](https://github.com/joannis) and [Tibor](https://github.com/tib). 142 | 143 | We'll try to pay attention to the following, providing suggestions to your code and text as we run into them. We generally tend to edit a lot of small phrases, to help the reader. Please don't be intimiated when we do so. 144 | 145 | Most of our checks can be done yourself. Our edits focus primarily on the following: 146 | 147 | #### Flesch Kincaid Readability Score 148 | 149 | We run a readability test over the text, mainly using the free [Hemingway Editor](https://hemingwayapp.com/). This shows us where complex words are used, that have a simpler alternative, in addition to highlighting the complexity of your article. 150 | 151 | Almost every article written is expected to start out with a poor score. However, they can be almost trivially tweaked with the suggestions provided by the editor. 152 | 153 | #### Word Choices 154 | 155 | We try to avoid words like "you", "I" or "we", and focus on an authorative voice. We believe that the articles we write are scrutinised more than enough to warrant being authoritative in our field. 156 | 157 | #### Passive Voice 158 | 159 | As part of being authoriative, we also strive to remove any and all use of [passive voice](https://www.grammarly.com/blog/passive-voice/). 160 | 161 | #### Length of Sections 162 | 163 | We try to keep the article in digestable chunks, which includes the length of sample code blocks. 164 | -------------------------------------------------------------------------------- /Snippets/HummingbirdApp.swift: -------------------------------------------------------------------------------- 1 | import Hummingbird 2 | 3 | @main struct App { 4 | static func main() async throws { 5 | // snippet.router 6 | typealias AppRequestContext = BasicRequestContext 7 | let router = Router(context: AppRequestContext.self) 8 | // snippet.end 9 | 10 | // snippet.health 11 | router.get("/health") { _, _ -> HTTPResponse.Status in 12 | return .ok 13 | } 14 | // snippet.end 15 | 16 | // snippet.basic_route 17 | router.get("/") { _, _ -> String in 18 | return "My app works!" 19 | } 20 | // snippet.end 21 | 22 | // snippet.codable_route 23 | struct MyResponse: ResponseCodable { 24 | let message: String 25 | } 26 | 27 | router.get("/message") { _, _ -> MyResponse in 28 | return MyResponse(message: "Hello, world!") 29 | } 30 | // snippet.end 31 | 32 | // snippet.run 33 | let app = Application(router: router) 34 | try await app.runService() 35 | // snippet.end 36 | } 37 | } -------------------------------------------------------------------------------- /Snippets/HummingbirdRequestContext.swift: -------------------------------------------------------------------------------- 1 | import Hummingbird 2 | import Foundation 3 | 4 | do { 5 | // snippet.custom_request_context 6 | struct CustomContext: RequestContext { 7 | var coreContext: CoreRequestContextStorage 8 | 9 | init(source: ApplicationRequestContextSource) { 10 | self.coreContext = .init(source: source) 11 | } 12 | } 13 | // snippet.end 14 | 15 | // snippet.router 16 | let router = Router(context: CustomContext.self) 17 | // snippet.end 18 | _ = router 19 | } 20 | 21 | struct User: Identifiable, Codable { 22 | var id: UUID 23 | } 24 | 25 | // snippet.custom_request_context 26 | struct CustomContext: RequestContext { 27 | var coreContext: CoreRequestContextStorage 28 | var token: String? 29 | 30 | init(source: ApplicationRequestContextSource) { 31 | self.coreContext = .init(source: source) 32 | } 33 | } 34 | // snippet.end 35 | 36 | // snippet.simple_middleware 37 | struct SimpleAuthMiddleware: RouterMiddleware { 38 | typealias Context = CustomContext 39 | 40 | func handle(_ input: Request, context: Context, next: (Request, Context) async throws -> Output) async throws -> Response { 41 | var context = context 42 | guard 43 | let token = input.headers[.authorization] 44 | else { 45 | throw HTTPError(.unauthorized) 46 | } 47 | 48 | // Note: This is still not secure. 49 | // Token verification is missing from this example 50 | context.token = token 51 | 52 | // Pass along the chain to the next handler (middleware or route handler) 53 | return try await next(input, context) 54 | } 55 | } 56 | // snippet.end 57 | 58 | // snippet.context_protocol 59 | protocol AuthContext: RequestContext { 60 | var token: String? { get set } 61 | } 62 | // snippet.end 63 | 64 | // snippet.context_protocol_middleware 65 | struct AuthMiddleware: RouterMiddleware { 66 | func handle(_ input: Request, context: Context, next: (Request, Context) async throws -> Output) async throws -> Response { 67 | var context = context 68 | guard 69 | let token = input.headers[.authorization] 70 | else { 71 | throw HTTPError(.unauthorized) 72 | } 73 | 74 | // Note: This is still not secure. 75 | // Token verification is missing from this example 76 | context.token = token 77 | 78 | // Pass along the chain to the next handler (middleware or route handler) 79 | return try await next(input, context) 80 | } 81 | } 82 | // snippet.end -------------------------------------------------------------------------------- /Snippets/advanced-async-sequences.swift: -------------------------------------------------------------------------------- 1 | let mySequence = [1, 2, 3] 2 | 3 | func iterateSequence() { 4 | // snippet.iterateSequence 5 | for element in mySequence { 6 | print(element) 7 | } 8 | // snippet.end 9 | } 10 | 11 | func manuallyIterateSequence() { 12 | // snippet.unwrappedSequenceIterator 13 | var iterator = mySequence.makeIterator() 14 | while let element = iterator.next() { 15 | print(element) 16 | } 17 | // snippet.end 18 | } 19 | 20 | func makeAsyncStream() -> AsyncStream { 21 | // snippet.makeAsyncStream 22 | let (stream, continuation) = AsyncStream.makeStream() 23 | // snippet.end 24 | for element in mySequence { 25 | continuation.yield(element) 26 | } 27 | continuation.finish() 28 | return stream 29 | } 30 | 31 | enum MyError: Error { 32 | case someIssue 33 | } 34 | 35 | func makeAsyncThrowingStream() -> AsyncThrowingStream { 36 | let (stream, continuation) = AsyncThrowingStream.makeStream() 37 | for element in mySequence { 38 | continuation.yield(element) 39 | } 40 | continuation.yield(with: .failure(MyError.someIssue)) 41 | return stream 42 | } 43 | 44 | func iterateAsyncSequence() async { 45 | let myAsyncSequence = makeAsyncStream() 46 | // snippet.iterateAsyncSequence 47 | for await element in myAsyncSequence { 48 | print(element) 49 | } 50 | // snippet.end 51 | } 52 | 53 | func manuallyIterateAsyncSequence() async { 54 | let myAsyncSequence = makeAsyncStream() 55 | // snippet.unwrappedAsyncSequenceIterator 56 | var iterator = myAsyncSequence.makeAsyncIterator() 57 | while let element = await iterator.next() { 58 | print(element) 59 | } 60 | // snippet.end 61 | } 62 | 63 | func iterateAsyncThrowingSequence() async throws { 64 | let myAsyncThrowingSequence = makeAsyncThrowingStream() 65 | // snippet.iterateAsyncThrowingSequence 66 | for try await element in myAsyncThrowingSequence { 67 | print(element) 68 | } 69 | // snippet.end 70 | } 71 | 72 | // snippet.uievent 73 | // Define all UI events that a user can send 74 | enum UIEvent: Sendable { 75 | case startDownloadTapped 76 | } 77 | 78 | // Create a stream of UI events 79 | let (stream, continuation) = AsyncStream.makeStream() 80 | // snippet.end 81 | 82 | continuation.yield(.startDownloadTapped) 83 | 84 | // snippet.appstate 85 | actor AppState { 86 | enum DownloadState { 87 | case notDownloaded 88 | case downloading 89 | case downloaded 90 | } 91 | 92 | var downloadState = DownloadState.notDownloaded 93 | let stream: AsyncStream 94 | 95 | init(stream: AsyncStream) { 96 | self.stream = stream 97 | } 98 | 99 | func handleEvents() async { 100 | for await event in stream { 101 | switch event { 102 | case .startDownloadTapped: 103 | switch downloadState { 104 | case .notDownloaded: 105 | downloadState = .downloading 106 | do { 107 | try await startDownload() 108 | downloadState = .downloaded 109 | } 110 | catch { 111 | downloadState = .notDownloaded 112 | } 113 | case .downloading, .downloaded: 114 | // Don't respond to user input 115 | continue 116 | } 117 | } 118 | } 119 | } 120 | } 121 | // snippet.end 122 | 123 | extension AppState { 124 | func startDownload() async throws { 125 | // NOOP 126 | } 127 | } 128 | 129 | actor AppState2 { 130 | var downloadState = AppState.DownloadState.notDownloaded 131 | let stream: AsyncStream 132 | 133 | init(stream: AsyncStream) { 134 | self.stream = stream 135 | } 136 | 137 | // snippet.parallel 138 | func setDownloadState(_ state: AppState.DownloadState) { 139 | downloadState = state 140 | } 141 | 142 | func handleEvents() async { 143 | await withDiscardingTaskGroup { taskGroup in 144 | for await event in stream { 145 | switch event { 146 | case .startDownloadTapped: 147 | switch downloadState { 148 | case .notDownloaded: 149 | downloadState = .downloading 150 | taskGroup.addTask { 151 | // This code runs in parallel with the loop 152 | // It doesn't block the loop from handling other events 153 | do { 154 | try await self.startDownload() 155 | await self.setDownloadState(.downloaded) 156 | } 157 | catch { 158 | await self.setDownloadState(.notDownloaded) 159 | } 160 | } 161 | case .downloading, .downloaded: 162 | // Don't respond to user input 163 | continue 164 | } 165 | } 166 | } 167 | } 168 | } 169 | // snippet.end 170 | 171 | func startDownload() async throws { 172 | // NOOP 173 | } 174 | } 175 | 176 | // snippet.delayedelementemitter 177 | struct DelayedElementEmitter: AsyncSequence { 178 | let elements: [Element] 179 | let delay: Duration 180 | 181 | init(elements: [Element], delay: Duration) { 182 | self.elements = elements 183 | self.delay = delay 184 | } 185 | } 186 | // snippet.end 187 | // snippet.delayedelementiterator 188 | extension DelayedElementEmitter { 189 | struct AsyncIterator: AsyncIteratorProtocol { 190 | var elements: [Element] 191 | let delay: Duration 192 | 193 | mutating func next() async throws -> Element? { 194 | try await Task.sleep(for: delay) 195 | guard elements.isEmpty else { 196 | return elements.removeFirst() 197 | } 198 | return nil 199 | } 200 | } 201 | 202 | func makeAsyncIterator() -> AsyncIterator { 203 | AsyncIterator( 204 | elements: elements, 205 | delay: delay 206 | ) 207 | } 208 | } 209 | // snippet.end 210 | // snippet.delayedprint 211 | let delayedEmitter = DelayedElementEmitter( 212 | elements: [1, 2, 3, 4, 5], 213 | delay: .seconds(1) 214 | ) 215 | 216 | for try await number in delayedEmitter { 217 | print(number) 218 | } 219 | // snippet.end 220 | -------------------------------------------------------------------------------- /Snippets/ahc_download.swift: -------------------------------------------------------------------------------- 1 | import AsyncHTTPClient 2 | import Foundation 3 | import NIOCore 4 | 5 | @main 6 | struct Entrypoint { 7 | static func main() async throws { 8 | // snippet.download 9 | let httpClient = HTTPClient( 10 | eventLoopGroupProvider: .singleton 11 | ) 12 | 13 | do { 14 | // snippet.POINT 15 | let delegate = try FileDownloadDelegate( 16 | // 2. 17 | path: NSTemporaryDirectory() + "600x400.png", 18 | // 3. 19 | reportProgress: { 20 | if let totalBytes = $0.totalBytes { 21 | print("Total: \(totalBytes).") 22 | } 23 | print("Downloaded: \($0.receivedBytes).") 24 | } 25 | ) 26 | 27 | // 4. 28 | let fileDownloadResponse = 29 | try await httpClient.execute( 30 | request: .init( 31 | url: "https://placehold.co/600x400.png" 32 | ), 33 | delegate: delegate 34 | ) 35 | .futureResult.get() 36 | 37 | print(fileDownloadResponse) 38 | } 39 | catch { 40 | print("\(error)") 41 | } 42 | 43 | try await httpClient.shutdown() 44 | // snippet.end 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Snippets/ahc_json.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import AsyncHTTPClient 3 | import Foundation 4 | import NIOCore 5 | import NIOFoundationCompat 6 | 7 | @main 8 | struct Entrypoint { 9 | static func main() async throws { 10 | // snippet.show 11 | // 1. 12 | struct Input: Codable { 13 | let id: Int 14 | let title: String 15 | let completed: Bool 16 | } 17 | 18 | struct Output: Codable { 19 | let json: Input 20 | } 21 | 22 | let httpClient = HTTPClient( 23 | eventLoopGroupProvider: .singleton 24 | ) 25 | do { 26 | // 2. 27 | var request = HTTPClientRequest( 28 | url: "https://httpbin.org/post" 29 | ) 30 | request.method = .POST 31 | request.headers.add(name: "content-type", value: "application/json") 32 | 33 | // 3. 34 | let input = Input( 35 | id: 1, 36 | title: "foo", 37 | completed: false 38 | ) 39 | 40 | let encoder = JSONEncoder() 41 | let buffer = try encoder.encodeAsByteBuffer( 42 | input, 43 | allocator: ByteBufferAllocator() 44 | ) 45 | request.body = .bytes(buffer) 46 | 47 | let response = try await httpClient.execute( 48 | request, 49 | timeout: .seconds(5) 50 | ) 51 | 52 | if response.status == .ok { 53 | // 4. 54 | if let contentType = response.headers.first( 55 | name: "content-type" 56 | ), contentType.contains("application/json") { 57 | // 5. 58 | let buffer = try await response.body.collect( 59 | upTo: 1024 * 1024 60 | ) 61 | 62 | // 6. 63 | let output = try JSONDecoder() 64 | .decode(Output.self, from: buffer) 65 | print(output.json.title) 66 | } 67 | } 68 | else { 69 | print("Invalid status code: \(response.status)") 70 | } 71 | } 72 | catch { 73 | print("\(error)") 74 | } 75 | 76 | try await httpClient.shutdown() 77 | // snippet.hide 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Snippets/ahc_request.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import AsyncHTTPClient 3 | import Foundation 4 | import NIOCore 5 | 6 | @main 7 | struct Entrypoint { 8 | static func main() async throws { 9 | // snippet.show 10 | let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) 11 | 12 | do { 13 | // 1. 14 | var request = HTTPClientRequest(url: "https://httpbin.org/post") 15 | // 2. 16 | request.method = .POST 17 | // 3. 18 | request.headers.add( 19 | name: "User-Agent", 20 | value: "Swift AsyncHTTPClient" 21 | ) 22 | // 4. 23 | request.body = .bytes(ByteBuffer(string: "Some data")) 24 | 25 | // 5. 26 | let response = try await httpClient.execute( 27 | request, 28 | timeout: .seconds(5) 29 | ) 30 | 31 | // 6. 32 | if response.status == .ok { 33 | // 7. 34 | let contentType = response.headers.first(name: "content-type") 35 | 36 | // 8. 37 | let buffer = try await response.body.collect(upTo: 1024 * 1024) 38 | 39 | // 9. 40 | let rawResponseBody = String(buffer: buffer) 41 | 42 | print("Content Type", contentType ?? "unknown") 43 | print(rawResponseBody) 44 | } 45 | } 46 | catch { 47 | print("\(error)") 48 | } 49 | 50 | try await httpClient.shutdown() 51 | // snippet.hide 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Snippets/ahc_setup.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import AsyncHTTPClient 3 | import Foundation 4 | import NIOCore 5 | 6 | @main 7 | struct Entrypoint { 8 | static func main() async throws { 9 | // snippet.show 10 | let httpClient = HTTPClient( 11 | // 1. 12 | eventLoopGroupProvider: .singleton, 13 | // 2. 14 | configuration: .init( 15 | // 3. 16 | redirectConfiguration: .follow( 17 | max: 3, 18 | allowCycles: false 19 | ), 20 | // 4. 21 | timeout: .init( 22 | connect: .init(.seconds(1)), 23 | read: .seconds(1), 24 | write: .seconds(1) 25 | ) 26 | ) 27 | ) 28 | 29 | // perform HTTP operations 30 | 31 | // 5. 32 | try await httpClient.shutdown() 33 | // snippet.hide 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // 1. 4 | package todos; 5 | 6 | // 2. 7 | message Empty {} 8 | 9 | // 3. 10 | message TodoID { 11 | string todoID = 1; 12 | } 13 | 14 | // 4. 15 | message Todo { 16 | optional string todoID = 1; 17 | string title = 2; 18 | bool completed = 3; 19 | } 20 | 21 | // 5. 22 | message TodoList { 23 | repeated Todo todos = 1; 24 | } 25 | -------------------------------------------------------------------------------- /Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package todos; 4 | 5 | // 1. 6 | import "todo_messages.proto"; 7 | 8 | // 2. 9 | service TodoService { 10 | // 3. 11 | rpc FetchTodos (Empty) returns (TodoList) {} 12 | // 4. 13 | rpc CreateTodo (Todo) returns (Todo) {} 14 | // 5. 15 | rpc DeleteTodo (TodoID) returns (Empty) {} 16 | // 6. 17 | rpc CompleteTodo (TodoID) returns (Todo) {} 18 | } 19 | -------------------------------------------------------------------------------- /Snippets/building-swiftnio-clients-01.swift: -------------------------------------------------------------------------------- 1 | // snippet.imports 2 | import NIOCore 3 | import NIOHTTP1 4 | import HTTPTypes 5 | import NIOHTTPTypes 6 | import NIOHTTPTypesHTTP1 7 | import NIOPosix 8 | 9 | // snippet.end 10 | 11 | // snippet.partial 12 | enum HTTPPartialResponse { 13 | case none 14 | case receiving(HTTPResponse, ByteBuffer) 15 | } 16 | // snippet.end 17 | 18 | // snippet.error 19 | enum HTTPClientError: Error { 20 | case malformedResponse, unexpectedEndOfStream 21 | } 22 | // snippet.end 23 | 24 | // snippet.client 25 | struct HTTPClient { 26 | let host: String 27 | let httpClientBootstrap: ClientBootstrap 28 | 29 | init(host: String) { 30 | // snippet.bootstrap 31 | // 1 32 | let httpClientBootstrap = ClientBootstrap( 33 | group: NIOSingletons.posixEventLoopGroup 34 | ) 35 | // 2 36 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 37 | // 3 38 | .channelInitializer { channel in 39 | // 4 40 | channel.eventLoop.makeCompletedFuture { 41 | try channel.pipeline.syncOperations.addHTTPClientHandlers() 42 | try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) 43 | } 44 | } 45 | // snippet.end 46 | 47 | self.host = host 48 | self.httpClientBootstrap = httpClientBootstrap 49 | } 50 | 51 | func request( 52 | _ path: String, 53 | method: HTTPRequest.Method = .get, 54 | headers: HTTPFields = [:] 55 | ) async throws -> (HTTPResponse, ByteBuffer) { 56 | // 1 57 | let clientChannel = 58 | try await httpClientBootstrap.connect( 59 | host: host, 60 | port: 80 61 | ) 62 | .flatMapThrowing { channel in 63 | // 2 64 | try NIOAsyncChannel( 65 | wrappingChannelSynchronously: channel, 66 | configuration: NIOAsyncChannel.Configuration( 67 | inboundType: HTTPResponsePart.self, // 3 68 | outboundType: HTTPRequestPart.self // 4 69 | ) 70 | ) 71 | } 72 | .get() // 5 73 | // snippet.end 74 | 75 | // snippet.executeThenClose 76 | // 1 77 | return try await clientChannel.executeThenClose { inbound, outbound in 78 | // 2 79 | try await outbound.write( 80 | .head( 81 | HTTPRequest( 82 | method: method, 83 | scheme: "http", 84 | authority: host, 85 | path: path, 86 | headerFields: headers 87 | ) 88 | ) 89 | ) 90 | try await outbound.write(.end(nil)) 91 | // snippet.end 92 | 93 | // snippet.httpLogic 94 | var partialResponse = HTTPPartialResponse.none 95 | 96 | // 1 97 | for try await part in inbound { 98 | // 2 99 | switch part { 100 | case .head(let head): 101 | guard case .none = partialResponse else { 102 | throw HTTPClientError.malformedResponse 103 | } 104 | 105 | let buffer = clientChannel.channel.allocator.buffer( 106 | capacity: 0 107 | ) 108 | partialResponse = .receiving(head, buffer) 109 | case .body(let buffer): 110 | guard 111 | case .receiving(let head, var existingBuffer) = 112 | partialResponse 113 | else { 114 | throw HTTPClientError.malformedResponse 115 | } 116 | 117 | existingBuffer.writeImmutableBuffer(buffer) 118 | partialResponse = .receiving(head, existingBuffer) 119 | case .end: 120 | guard 121 | case .receiving(let head, let buffer) = partialResponse 122 | else { 123 | throw HTTPClientError.malformedResponse 124 | } 125 | 126 | return (head, buffer) 127 | } 128 | } 129 | 130 | // 3 131 | throw HTTPClientError.unexpectedEndOfStream 132 | // snippet.end 133 | } 134 | } 135 | } 136 | 137 | // snippet.usage 138 | let client = HTTPClient(host: "example.com") 139 | let (response, body) = try await client.request("/") 140 | print(response) 141 | print(body.getString(at: 0, length: body.readableBytes)!) 142 | // snippet.end 143 | 144 | extension HTTPPart: @unchecked Sendable {} -------------------------------------------------------------------------------- /Snippets/environment.swift: -------------------------------------------------------------------------------- 1 | // snippet.processInfo 2 | import Foundation 3 | // snippet.end 4 | // snippet.hummingbird 5 | import Hummingbird 6 | import Logging 7 | 8 | let env = ProcessInfo.processInfo.environment 9 | 10 | let value = env["LOG_LEVEL"] ?? "trace" 11 | 12 | print(value) 13 | 14 | func buildApplication( 15 | configuration: ApplicationConfiguration 16 | ) async throws -> some ApplicationProtocol { 17 | var logger = Logger(label: "hummingbird-logger") 18 | logger.logLevel = .trace 19 | 20 | let env = Environment() 21 | // let env = try await Environment.dotEnv() 22 | let logLevel = env.get("LOG_LEVEL") 23 | 24 | if let logLevel, let logLevel = Logger.Level(rawValue: logLevel) { 25 | logger.logLevel = logLevel 26 | } 27 | 28 | let router = Router() 29 | router.get("/") { _, _ in 30 | "Hello" 31 | } 32 | 33 | let app = Application( 34 | router: router, 35 | configuration: configuration, 36 | logger: logger 37 | ) 38 | return app 39 | } 40 | // snippet.end 41 | -------------------------------------------------------------------------------- /Snippets/getting-started-concurrency-buy-books.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.UUID 2 | 3 | actor BankAccount { 4 | private var funds: Double = 35 5 | 6 | func checkBalance() -> Double { 7 | funds 8 | } 9 | } 10 | 11 | struct BookOrder: Identifiable { 12 | let id = UUID() 13 | let book: Book 14 | 15 | func delivery() async throws -> Book { 16 | // Delivery takes time!! 17 | try await Task.sleep(for: .seconds(1)) 18 | return book 19 | } 20 | } 21 | 22 | struct Book { 23 | let price: Double 24 | 25 | init(price: Double) { 26 | self.price = price 27 | } 28 | 29 | func buy() async throws -> BookOrder { 30 | BookOrder(book: self) 31 | } 32 | } 33 | 34 | actor BookStore { 35 | // snippet.asyncSequence 36 | struct Books: AsyncSequence { 37 | typealias Element = Book 38 | let database: [Book] 39 | 40 | struct AsyncIterator: AsyncIteratorProtocol { 41 | typealias Element = Book 42 | var database: [Book] 43 | var readerIndex = 0 44 | 45 | mutating func next() async -> Book? { 46 | if readerIndex == database.count { 47 | return nil 48 | } 49 | 50 | let book = database[readerIndex] 51 | readerIndex += 1 52 | return book 53 | } 54 | } 55 | 56 | func makeAsyncIterator() -> AsyncIterator { 57 | AsyncIterator(database: database) 58 | } 59 | } 60 | // snippet.end 61 | 62 | private static let onlyBookStoreInTown = BookStore() 63 | private var books: [Book] = [] 64 | 65 | init() { 66 | self.books = [ 67 | Book(price: 10), 68 | Book(price: 15), 69 | Book(price: 9.95), 70 | Book(price: 10), 71 | Book(price: 25), 72 | Book(price: 8), 73 | Book(price: 15), 74 | ] 75 | } 76 | 77 | // snippet.browseBooks 78 | func browseBooks() -> Books { 79 | Books(database: books) 80 | } 81 | // snippet.end 82 | 83 | static func discover() async -> BookStore { 84 | // Return the book store instance 85 | Self.onlyBookStoreInTown 86 | } 87 | } 88 | 89 | // snippet.buyBooks 90 | func buyBooks(from bankAccount: BankAccount) async throws -> [Book] { 91 | // Resolve this concurrently 92 | async let balance = await bankAccount.checkBalance() 93 | 94 | let store = await BookStore.discover() 95 | var budget = await balance 96 | var boughtBooks: [Book] = [] 97 | let books = await store.browseBooks() 98 | for await book in books where book.price <= budget { 99 | let order = try await book.buy() 100 | let book = try await order.delivery() 101 | budget -= book.price 102 | boughtBooks.append(book) 103 | } 104 | return boughtBooks 105 | } 106 | // snippet.end 107 | 108 | // snippet.buyBooksInBackground 109 | func buyBooksInBackground() async { 110 | let store = await BookStore.discover() 111 | for await book in await store.browseBooks() { 112 | Task { 113 | let order = try await book.buy() 114 | _ = try await order.delivery() 115 | } 116 | } 117 | } 118 | // snippet.end 119 | -------------------------------------------------------------------------------- /Snippets/getting-started-concurrency-dispatch.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | import Foundation 3 | 4 | // snippet.dispatchGlobal 5 | DispatchQueue.global() 6 | .async { 7 | // Offload some (heavy) work 8 | } 9 | // snippet.end 10 | 11 | #if os(Linux) 12 | import FoundationNetworking 13 | #endif 14 | // This is stubbed since UIImage isn't available on mac and Linux 15 | struct UIImage { 16 | let data: Data 17 | 18 | init?(data: Data) { 19 | self.data = data 20 | } 21 | } 22 | 23 | enum NetworkError: Error { 24 | case missingImage 25 | } 26 | 27 | // snippet.fetchImageCallback 28 | func fetchImage( 29 | at url: URL, 30 | completion: @Sendable @escaping (Result) -> Void 31 | ) { 32 | URLSession.shared.dataTask(with: url) { data, response, error in 33 | if let error { 34 | completion(.failure(error)) 35 | return 36 | } 37 | guard let data = data, let image = UIImage(data: data) else { 38 | completion(.failure(NetworkError.missingImage)) 39 | return 40 | } 41 | completion(.success(image)) 42 | } 43 | } 44 | // snippet.end 45 | -------------------------------------------------------------------------------- /Snippets/getting-started-concurrency-imagecache-locks.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import Foundation 3 | 4 | // This is stubbed since UIImage isn't available on mac and Linux 5 | struct UIImage {} 6 | 7 | func fetchImage( 8 | at url: URL, 9 | callback: @escaping (Result) -> Void 10 | ) { 11 | callback(.success(UIImage())) 12 | } 13 | // snippet.show 14 | final class ImageCache { 15 | private var cache: [URL: UIImage] = [:] 16 | private let lock = NSLock() 17 | 18 | func image(for url: URL) -> UIImage? { 19 | lock.lock() 20 | defer { lock.unlock() } 21 | return cache[url] 22 | } 23 | 24 | func loadImage(for url: URL) { 25 | lock.lock() 26 | defer { lock.unlock() } 27 | // This is covered by the lock 28 | if cache.keys.contains(url) { 29 | return 30 | } 31 | fetchImage(at: url) { [self] image in 32 | guard case .success(let image) = image else { return } 33 | // This is not covered by the lock 34 | cache[url] = image 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Snippets/hummingbird-2-proxy.swift: -------------------------------------------------------------------------------- 1 | // snippet.imports 2 | import AsyncHTTPClient 3 | import Hummingbird 4 | import Logging 5 | import NIOCore 6 | import NIOPosix 7 | import ServiceLifecycle 8 | // snippet.end 9 | 10 | // snippet.forwarding 11 | func forward( 12 | request: Request, 13 | targetHost: String, 14 | httpClient: HTTPClient, 15 | context: some RequestContext 16 | ) async throws -> Response { 17 | // 1. 18 | let query = request.uri.query.map { "?\($0)" } ?? "" 19 | var clientRequest = HTTPClientRequest(url: "https://\(targetHost)\(request.uri.path)\(query)") 20 | clientRequest.method = .init(request.method) 21 | clientRequest.headers = .init(request.headers) 22 | 23 | // 2. 24 | let contentLength = if let header = request.headers[.contentLength], let value = Int(header) { 25 | HTTPClientRequest.Body.Length.known(value) 26 | } else { 27 | HTTPClientRequest.Body.Length.unknown 28 | } 29 | clientRequest.body = .stream( 30 | request.body, 31 | length: contentLength 32 | ) 33 | 34 | // 3. 35 | let response = try await httpClient.execute(clientRequest, timeout: .seconds(60)) 36 | 37 | // 4. 38 | return Response( 39 | status: HTTPResponse.Status(code: Int(response.status.code), reasonPhrase: response.status.reasonPhrase), 40 | headers: HTTPFields(response.headers, splitCookie: false), 41 | body: ResponseBody(asyncSequence: response.body) 42 | ) 43 | } 44 | // snippet.end 45 | 46 | // snippet.middleware 47 | struct ProxyServerMiddleware: RouterMiddleware { 48 | var httpClient: HTTPClient = .shared 49 | let targetHost: String 50 | 51 | func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response { 52 | try await forward( 53 | request: request, 54 | targetHost: targetHost, 55 | httpClient: httpClient, 56 | context: context 57 | ) 58 | } 59 | } 60 | // snippet.end 61 | 62 | do { 63 | // snippet.middleware-router 64 | let router = Router() 65 | router.add(middleware: ProxyServerMiddleware(targetHost: "example.com")) 66 | // snippet.end 67 | 68 | // snippet.setup-router 69 | let app = Application( 70 | router: router 71 | ) 72 | // snippet.end 73 | 74 | _ = app 75 | } 76 | 77 | do { 78 | // snippet.route 79 | let router = Router() 80 | router.get("**") { request, context in 81 | try await forward( 82 | request: request, 83 | targetHost: "example.com", 84 | httpClient: .shared, 85 | context: context 86 | ) 87 | } 88 | // snippet.end 89 | 90 | _ = router 91 | } 92 | 93 | // snippet.responder 94 | struct ProxyServerResponder: HTTPResponder { 95 | let targetHost: String 96 | 97 | func respond(to request: Request, context: Context) async throws -> Response { 98 | try await forward( 99 | request: request, 100 | targetHost: targetHost, 101 | httpClient: .shared, 102 | context: context 103 | ) 104 | } 105 | } 106 | // snippet.end 107 | 108 | do { 109 | // snippet.setup-responder 110 | let app = Application( 111 | responder: ProxyServerResponder(targetHost: "example.com") 112 | ) 113 | // snippet.end 114 | 115 | _ = app 116 | } 117 | -------------------------------------------------------------------------------- /Snippets/hummingbird-2-routerbuilder.swift: -------------------------------------------------------------------------------- 1 | import Hummingbird 2 | import HummingbirdRouter 3 | 4 | let router = RouterBuilder(context: BasicRouterRequestContext.self) { 5 | TracingMiddleware() 6 | Get("test") { _, context in 7 | return context.endpointPath 8 | } 9 | Get { _, context in 10 | return context.endpointPath 11 | } 12 | Post("/test2") { _, context in 13 | return context.endpointPath 14 | } 15 | } 16 | let app = Application(responder: router) 17 | try await app.runService() 18 | -------------------------------------------------------------------------------- /Snippets/hummingbird-2.swift: -------------------------------------------------------------------------------- 1 | // snippet.end 2 | // snippet.run 3 | import ArgumentParser 4 | import Foundation 5 | import Hummingbird 6 | import Logging 7 | import NIOCore 8 | 9 | // snippet.requestDecoder 10 | // 1. 11 | struct MyRequestDecoder: RequestDecoder { 12 | func decode( 13 | _ type: T.Type, 14 | from request: Request, 15 | context: some RequestContext 16 | ) async throws -> T where T: Decodable { 17 | // 2. 18 | guard let header = request.headers[.contentType] else { 19 | throw HTTPError(.badRequest) 20 | } 21 | // 3. 22 | guard let mediaType = MediaType(from: header) else { 23 | throw HTTPError(.badRequest) 24 | } 25 | // 4. 26 | let decoder: RequestDecoder 27 | switch mediaType { 28 | case .applicationJson: 29 | decoder = JSONDecoder() 30 | case .applicationUrlEncoded: 31 | decoder = URLEncodedFormDecoder() 32 | default: 33 | throw HTTPError(.badRequest) 34 | } 35 | // 5 36 | return try await decoder.decode( 37 | type, 38 | from: request, 39 | context: context 40 | ) 41 | } 42 | } 43 | // snippet.end 44 | // snippet.requestContext 45 | // 1. 46 | protocol MyRequestContext: RequestContext { 47 | var myValue: String? { get set } 48 | } 49 | 50 | // 2. 51 | struct MyBaseRequestContext: MyRequestContext { 52 | var coreContext: CoreRequestContextStorage 53 | 54 | // 3. 55 | var myValue: String? 56 | 57 | init(source: Source) { 58 | self.coreContext = .init(source: source) 59 | } 60 | 61 | // 4. 62 | var requestDecoder: RequestDecoder { 63 | MyRequestDecoder() 64 | } 65 | } 66 | // snippet.end 67 | struct MyModel: ResponseCodable { 68 | let title: String 69 | } 70 | // snippet.controller 71 | struct MyController { 72 | // 1. 73 | func addRoutes( 74 | to group: RouterGroup 75 | ) { 76 | group 77 | .get(use: list) 78 | .post(use: create) 79 | } 80 | 81 | // 2. 82 | @Sendable 83 | func list( 84 | _ request: Request, 85 | context: Context 86 | ) async throws -> [MyModel] { 87 | [ 88 | .init(title: "foo"), 89 | .init(title: "bar"), 90 | .init(title: "baz"), 91 | ] 92 | } 93 | 94 | @Sendable 95 | func create( 96 | _ request: Request, 97 | context: Context 98 | ) async throws -> EditedResponse { 99 | // 3. 100 | // context.myValue 101 | let input = try await request.decode( 102 | as: MyModel.self, 103 | context: context 104 | ) 105 | return .init(status: .created, response: input) 106 | } 107 | } 108 | // snippet.end 109 | // snippet.buildApp 110 | func buildApplication() async throws -> some ApplicationProtocol { 111 | // 1. 112 | let router = Router(context: MyBaseRequestContext.self) 113 | 114 | // 2 115 | router.middlewares.add(LogRequestsMiddleware(.info)) 116 | router.middlewares.add(FileMiddleware()) 117 | router.middlewares.add( 118 | CORSMiddleware( 119 | allowOrigin: .originBased, 120 | allowHeaders: [.contentType], 121 | allowMethods: [.get, .post, .delete, .patch] 122 | ) 123 | ) 124 | 125 | // 3 126 | router.get("/health") { _, _ -> HTTPResponse.Status in 127 | .ok 128 | } 129 | 130 | // 4. 131 | MyController().addRoutes(to: router.group("api")) 132 | 133 | // 5. 134 | return Application( 135 | router: router, 136 | configuration: .init( 137 | address: .hostname("localhost", port: 8080) 138 | ) 139 | ) 140 | } 141 | 142 | @main 143 | struct HummingbirdArguments: AsyncParsableCommand { 144 | func run() async throws { 145 | let app = try await buildApplication() 146 | try await app.runService() 147 | } 148 | } 149 | // snippet.end 150 | -------------------------------------------------------------------------------- /Snippets/hummingbird-and-cors.swift: -------------------------------------------------------------------------------- 1 | import Hummingbird 2 | 3 | let router = Router() 4 | 5 | // snippet.middleware 6 | router.add(middleware: CORSMiddleware()) 7 | // snippet.end 8 | 9 | // snippet.custom_middleware 10 | router.add(middleware: CORSMiddleware( 11 | allowOrigin: .custom("http://example.com"), 12 | allowHeaders: [.accept, .contentType], 13 | allowMethods: [.get, .post], 14 | allowCredentials: true, 15 | maxAge: .seconds(3600)) 16 | ) 17 | // snippet.end 18 | -------------------------------------------------------------------------------- /Snippets/jwt-kit.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | import Foundation 3 | 4 | // snippet.key_collection_init 5 | let keyCollection = JWTKeyCollection() 6 | // snippet.end 7 | 8 | // snippet.key_collection_add_hmac 9 | await keyCollection.add(hmac: "secret", digestAlgorithm: .sha256) 10 | // snippet.end 11 | 12 | // snippet.payload_struct 13 | struct TestPayload: JWTPayload { 14 | var expiration: ExpirationClaim 15 | var issuer: IssuerClaim 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case expiration = "exp" 19 | case issuer = "iss" 20 | } 21 | 22 | func verify(using key: some JWTAlgorithm) throws { 23 | try self.expiration.verifyNotExpired() 24 | } 25 | } 26 | // snippet.end 27 | 28 | // snippet.jwt_sign 29 | let payload = TestPayload( 30 | expiration: .init(value: .distantFuture), 31 | issuer: "myapp.com" 32 | ) 33 | 34 | let token = try await keyCollection.sign(payload) 35 | // snippet.end 36 | 37 | // snippet.jwt_verify 38 | let verifiedPayload = try await keyCollection.verify( 39 | token, 40 | as: TestPayload.self 41 | ) 42 | // snippet.end 43 | 44 | // snippet.auth_user_role_claim 45 | struct RoleClaim: JWTClaim { 46 | var value: [String] 47 | 48 | func verifyAdmin() throws { 49 | guard value.contains("admin") else { 50 | throw JWTError.claimVerificationFailure( 51 | failedClaim: self, 52 | reason: "User is not an admin" 53 | ) 54 | } 55 | } 56 | } 57 | // snippet.end 58 | 59 | struct User { 60 | var id: Int 61 | var roles: RoleClaim 62 | } 63 | 64 | // snippet.auth_user_payload 65 | struct UserPayload: JWTPayload { 66 | let userID: Int 67 | let expiration: ExpirationClaim 68 | let roles: RoleClaim 69 | 70 | enum CodingKeys: String, CodingKey { 71 | case userID = "user_id" 72 | case expiration = "exp" 73 | case roles 74 | } 75 | 76 | func verify(using key: some JWTAlgorithm) throws { 77 | try expiration.verifyNotExpired() 78 | try roles.verifyAdmin() 79 | } 80 | 81 | init(from user: User) { 82 | self.userID = user.id 83 | self.expiration = .init(value: .init(timeIntervalSinceNow: 3600)) // Token expires in 1 hour 84 | self.roles = user.roles 85 | } 86 | } 87 | // snippet.end 88 | -------------------------------------------------------------------------------- /Snippets/mongokitten-basics.swift: -------------------------------------------------------------------------------- 1 | import MongoKitten 2 | import Foundation 3 | 4 | // snippet.connection 5 | let db = try await MongoDatabase.connect(to: "mongodb://localhost:27017/social_network") 6 | print("Connected to MongoDB!") 7 | // snippet.end 8 | 9 | // snippet.models 10 | struct Post: Codable { 11 | let _id: ObjectId 12 | let author: String 13 | let content: String 14 | let createdAt: Date 15 | 16 | init(author: String, content: String) { 17 | self._id = ObjectId() 18 | self.author = author 19 | self.content = content 20 | self.createdAt = Date() 21 | } 22 | } 23 | // snippet.end 24 | 25 | // snippet.insert 26 | func createPost(author: String, content: String) async throws { 27 | // 1. Create the post 28 | let post = Post(author: author, content: content) 29 | 30 | // 2. Get the posts collection 31 | let posts = db["posts"] 32 | 33 | // 3. Insert the post 34 | try await posts.insertEncoded(post) 35 | } 36 | // snippet.end 37 | 38 | // snippet.find-all 39 | // 1. Find all posts 40 | let allPosts = try await db["posts"] 41 | .find() 42 | .decode(Post.self) 43 | .drain() 44 | // snippet.end 45 | 46 | // snippet.find-by-author 47 | // 2. Find posts by a specific author 48 | let authorPosts = try await db["posts"] 49 | .find("author" == "swift_developer") 50 | .decode(Post.self) 51 | .drain() 52 | // snippet.end 53 | 54 | // snippet.find-recent 55 | // 3. Find recent posts, sorted by creation date 56 | let recentPosts = try await db["posts"] 57 | .find() 58 | .sort(["createdAt": .descending]) 59 | .limit(10) 60 | .decode(Post.self) 61 | .drain() 62 | // snippet.end 63 | 64 | // snippet.bson 65 | struct Person: Codable { 66 | let name: String 67 | let age: Int 68 | let tags: [String] 69 | let active: Bool 70 | } 71 | 72 | // Creating a BSON Document manually 73 | let document: Document = [ 74 | "name": "Swift Developer", 75 | "age": 25, 76 | "tags": ["swift", "mongodb", "backend"] as Document, 77 | "active": true 78 | ] 79 | 80 | // Converting between BSON and Codable 81 | let bsonDocument = try BSONEncoder().encode(document) 82 | let decodedPerson = try BSONDecoder().decode(Person.self, from: bsonDocument) 83 | // snippet.end 84 | -------------------------------------------------------------------------------- /Snippets/realtime-mongodb-app.swift: -------------------------------------------------------------------------------- 1 | import Hummingbird 2 | import HummingbirdWebSocket 3 | import MongoKitten 4 | import NIOCore 5 | import Foundation 6 | import ServiceLifecycle 7 | 8 | struct Post: Codable { 9 | let id: ObjectId 10 | let author: String 11 | let content: String 12 | let createdAt: Date 13 | 14 | init(author: String, content: String) { 15 | self.id = ObjectId() 16 | self.author = author 17 | self.content = content 18 | self.createdAt = Date() 19 | } 20 | } 21 | 22 | func createPost(author: String, content: String, in db: MongoDatabase) async throws { 23 | // 1. Create the post 24 | let post = Post(author: author, content: content) 25 | 26 | // 2. Get the posts collection 27 | let posts = db["posts"] 28 | 29 | // 3. Insert the post 30 | try await posts.insertEncoded(post) 31 | } 32 | 33 | // snippet.routes 34 | func setupRoutes(router: Router, db: MongoDatabase) { 35 | router.post("/posts") { request, context -> Response in 36 | struct CreatePostRequest: Codable { 37 | let author: String 38 | let content: String 39 | } 40 | let post = try await request.decode(as: CreatePostRequest.self, context: context) 41 | try await createPost(author: post.author, content: post.content, in: db) 42 | return Response(status: .created) 43 | } 44 | } 45 | // snippet.end 46 | 47 | // snippet.main 48 | @main 49 | struct RealtimeMongoApp { 50 | static func main() async throws { 51 | // 1. 52 | let db = try await MongoDatabase.connect(to: "mongodb://localhost/social_network") 53 | 54 | // 2. 55 | let connectionManager = ConnectionManager(database: db) 56 | 57 | 58 | let router = Router(context: BasicRequestContext.self) 59 | setupRoutes(router: router, db: db) 60 | 61 | // 4. 62 | var app = Application( 63 | router: router, 64 | server: .http1WebSocketUpgrade { request, channel, logger in 65 | return .upgrade([:]) { inbound, outbound, context in 66 | try await connectionManager.withRegisteredClient(outbound) { 67 | for try await _ in inbound { 68 | // Drop any incoming data, we don't need it 69 | // But keep the connection open 70 | } 71 | } 72 | } 73 | } 74 | ) 75 | 76 | // 5. 77 | app.addServices(connectionManager) 78 | 79 | // 6. 80 | try await app.runService() 81 | } 82 | } 83 | // snippet.end 84 | 85 | // snippet.connection-manager 86 | actor ConnectionManager { 87 | private let database: MongoDatabase 88 | private var outboundConnections: [UUID: WebSocketOutboundWriter] = [:] 89 | 90 | init(database: MongoDatabase) { 91 | self.database = database 92 | } 93 | 94 | func broadcast(_ data: Data) async { 95 | guard let text = String(data: data, encoding: .utf8) else { 96 | return 97 | } 98 | 99 | for connection in outboundConnections.values { 100 | try? await connection.write(.text(text)) 101 | } 102 | } 103 | 104 | func withRegisteredClient( 105 | _ client: WebSocketOutboundWriter, 106 | perform: () async throws -> T 107 | ) async throws -> T { 108 | let id = UUID() 109 | outboundConnections[id] = client 110 | defer { outboundConnections[id] = nil } 111 | return try await perform() 112 | } 113 | } 114 | // snippet.end 115 | 116 | // snippet.watch-changes 117 | extension ConnectionManager: Service { 118 | func run() async throws { 119 | // 1. 120 | let posts = database["posts"] 121 | 122 | // 2. 123 | let changes = try await posts.watch(type: Post.self) 124 | 125 | // 3. 126 | for try await change in changes { 127 | // 4. 128 | if change.operationType == .insert, let post = change.fullDocument { 129 | // 5. 130 | let jsonData = try JSONEncoder().encode(post) 131 | // 6. 132 | await broadcast(jsonData) 133 | } 134 | } 135 | } 136 | } 137 | // snippet.end -------------------------------------------------------------------------------- /Snippets/shared-state.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // snippet.sharedState 4 | final class SharedState: @unchecked Sendable { 5 | private var _state: Int = 0 6 | let lock = NSLock() 7 | public var state: Int { 8 | get { 9 | lock.lock() 10 | defer { lock.unlock() } 11 | return _state 12 | } 13 | set { 14 | lock.lock() 15 | defer { lock.unlock() } 16 | _state = newValue 17 | } 18 | } 19 | } 20 | // snippet.end 21 | // snippet.bankAccount 22 | actor BankAccount { 23 | var balance: Int = 0 24 | 25 | func deposit(_ amount: Int) { 26 | balance += amount 27 | } 28 | 29 | func withdraw(_ amount: Int) { 30 | balance -= amount 31 | } 32 | } 33 | 34 | let bankAccount = BankAccount() 35 | await bankAccount.deposit(100) 36 | let balance = await bankAccount.balance 37 | print(balance) // 100 38 | // snippet.end 39 | // snippet.unownedExecutor 40 | _ = bankAccount.unownedExecutor 41 | // snippet.end 42 | struct BookOrder: Identifiable { 43 | let id = UUID() 44 | let book: Book 45 | 46 | func delivery() async throws -> Book { 47 | // Delivery takes time!! 48 | try await Task.sleep(for: .seconds(1)) 49 | return book 50 | } 51 | } 52 | 53 | struct Book {} 54 | 55 | func fetchBooks(page: Int) async throws -> [Book] { 56 | // No books, this is just a demo 57 | [] 58 | } 59 | 60 | // snippet.bookStore 61 | actor BookStore: AsyncSequence { 62 | typealias AsyncIterator = AsyncThrowingStream.AsyncIterator 63 | typealias Element = Book 64 | 65 | private var page = 1 66 | private var hasReachedEnd = false 67 | private let stream: AsyncThrowingStream 68 | private let continuation: AsyncThrowingStream.Continuation 69 | 70 | init() { 71 | (stream, continuation) = AsyncThrowingStream.makeStream( 72 | bufferingPolicy: .unbounded 73 | ) 74 | } 75 | 76 | func produce() async throws { 77 | do { 78 | while !hasReachedEnd { 79 | let books = try await fetchBooks(page: page) 80 | hasReachedEnd = books.isEmpty 81 | for book in books { 82 | continuation.yield(book) 83 | } 84 | page += 1 85 | } 86 | continuation.finish() 87 | } 88 | catch { 89 | continuation.finish(throwing: error) 90 | } 91 | } 92 | 93 | // AsyncSequence required a nonisolated func here 94 | nonisolated func makeAsyncIterator() -> AsyncIterator { 95 | stream.makeAsyncIterator() 96 | } 97 | } 98 | // snippet.end 99 | // snippet.bankAccountProtocol 100 | protocol BankAccountProtocol { 101 | var balance: Int { get async } 102 | func deposit(_ amount: Int) async 103 | func withdraw(_ amount: Int) async 104 | } 105 | 106 | actor MyBankAccount: BankAccountProtocol { 107 | var balance: Int = 0 108 | 109 | func deposit(_ amount: Int) { 110 | balance += amount 111 | } 112 | 113 | func withdraw(_ amount: Int) { 114 | balance -= amount 115 | } 116 | } 117 | // snippet.end 118 | 119 | struct UIImage {} 120 | 121 | // snippet.imageCache 122 | actor ImageCache { 123 | private var cache: [URL: UIImage] = [:] 124 | 125 | func image(for url: URL) -> UIImage? { 126 | cache[url] 127 | } 128 | 129 | func setImage(_ image: UIImage, for url: URL) { 130 | cache[url] = image 131 | } 132 | 133 | func loadImage(for url: URL) async throws { 134 | if cache.keys.contains(url) { 135 | return 136 | } 137 | 138 | let image = try await fetchImage(at: url) 139 | setImage(image, for: url) 140 | } 141 | } 142 | // snippet.end 143 | 144 | // snippet.loadingAwareImageCache 145 | actor LoadingAwareImageCache { 146 | private var cache: [URL: UIImage] = [:] 147 | private var loadingURLs: Set = [] 148 | 149 | func image(for url: URL) -> UIImage? { 150 | cache[url] 151 | } 152 | 153 | func setImage(_ image: UIImage, for url: URL) { 154 | cache[url] = image 155 | } 156 | 157 | func loadImage(for url: URL) async throws { 158 | if cache.keys.contains(url), !loadingURLs.contains(url) { 159 | return 160 | } 161 | 162 | loadingURLs.insert(url) 163 | defer { loadingURLs.remove(url) } 164 | let image = try await fetchImage(at: url) 165 | setImage(image, for: url) 166 | } 167 | } 168 | // snippet.end 169 | // snippet.findImages 170 | func findImages( 171 | onImage: @Sendable (UIImage) -> Void, 172 | onCompletion: @Sendable (Error?) -> Void 173 | ) { 174 | // No actual images found, just for demonstration of the below code 175 | onCompletion(nil) 176 | } 177 | // snippet.end 178 | 179 | // snippet.captureGroups 180 | let (stream, continuation) = AsyncThrowingStream.makeStream() 181 | 182 | // Hypothetical function that lists images 183 | // Calls the callback once for each image found 184 | findImages { [continuation] image in 185 | // Captures `continuation` 186 | continuation.yield(image) 187 | } onCompletion: { [continuation] error in 188 | // Captures `continuation` 189 | // Called exactly once when done or failed 190 | if let error = error { 191 | continuation.finish(throwing: error) 192 | } 193 | else { 194 | continuation.finish() 195 | } 196 | } 197 | 198 | for try await image in stream { 199 | // Show image 200 | print(image) 201 | } 202 | // snippet.end 203 | func fetchImage( 204 | at url: URL, 205 | callback: @Sendable (Result) -> Void 206 | ) { 207 | callback(.success(UIImage())) 208 | } 209 | // snippet.continuations 210 | @Sendable func fetchImage(at url: URL) async throws -> UIImage { 211 | try await withCheckedThrowingContinuation { continuation in 212 | fetchImage(at: url) { result in 213 | switch result { 214 | case .success(let image): 215 | continuation.resume(returning: image) 216 | case .failure(let error): 217 | continuation.resume(throwing: error) 218 | } 219 | } 220 | } 221 | } 222 | // snippet.end 223 | // snippet.efficientImageCache 224 | actor EfficientImageCache { 225 | private var cache: [URL: UIImage] = [:] 226 | private var loadingURLs: Set = [] 227 | private var fetchingURLs: [(URL, CheckedContinuation)] = [] 228 | private func completeFetchingURLs( 229 | with result: Result, 230 | for url: URL 231 | ) { 232 | for (awaitingURL, continuation) in fetchingURLs where awaitingURL == url 233 | { 234 | switch result { 235 | case .success(let image): 236 | continuation.resume(returning: image) 237 | case .failure(let error): 238 | continuation.resume(throwing: error) 239 | } 240 | } 241 | fetchingURLs.removeAll { $0.0 == url } 242 | } 243 | 244 | func setImage(_ image: UIImage, for url: URL) { 245 | self.cache[url] = image 246 | } 247 | 248 | func loadImage(at url: URL) async throws -> UIImage { 249 | if let image = cache[url] { 250 | return image 251 | } 252 | 253 | if loadingURLs.contains(url) { 254 | return try await withCheckedThrowingContinuation { continuation in 255 | fetchingURLs.append((url, continuation)) 256 | } 257 | } 258 | 259 | loadingURLs.insert(url) 260 | defer { loadingURLs.remove(url) } 261 | 262 | do { 263 | let image = try await fetchImage(at: url) 264 | setImage(image, for: url) 265 | completeFetchingURLs(with: .success(image), for: url) 266 | return image 267 | } 268 | catch { 269 | completeFetchingURLs(with: .failure(error), for: url) 270 | throw error 271 | } 272 | } 273 | } 274 | // snippet.end 275 | // snippet.globalActor 276 | @globalActor actor SensorActor { 277 | static let shared = SensorActor() 278 | } 279 | // snippet.end 280 | -------------------------------------------------------------------------------- /Snippets/swiftpm-snippets/SnippetsExample_I.swift: -------------------------------------------------------------------------------- 1 | print("Hi Barbie!") 2 | -------------------------------------------------------------------------------- /Snippets/swiftpm-snippets/SnippetsExample_II.swift: -------------------------------------------------------------------------------- 1 | // This is a snippet caption. 2 | // 3 | // You can use normal features in captions, such as links to symbols like ``Int``. 4 | 5 | print("Hi Raquelle!") 6 | -------------------------------------------------------------------------------- /Snippets/swiftpm-snippets/SnippetsExample_III.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import SnippetsExample 3 | 4 | // snippet.show 5 | let _:String = """ 6 | The import statements at the top of the file are redacted from \ 7 | the rendered Snippet. 8 | 9 | // snippet.hide 10 | 11 | Snippet markers slice the source code at the token syntax level. \ 12 | This means strings resembling Snippet markers inside multiline \ 13 | string literals do not need to be escaped. However, there is no \ 14 | requirement for slices to contain complete lexical blocks. This is 15 | different from the behavior of constructs like `#if`. 16 | 17 | // snippet.show 18 | """ 19 | -------------------------------------------------------------------------------- /Snippets/swiftpm-snippets/SnippetsExample_IV.swift: -------------------------------------------------------------------------------- 1 | // snippet.hide 2 | import SnippetsExample 3 | 4 | enum Main 5 | { 6 | // snippet.show 7 | static 8 | func main() -> Int 9 | { 10 | return 2 + 2 11 | } 12 | // snippet.end 13 | } 14 | -------------------------------------------------------------------------------- /Snippets/swiftpm-snippets/SnippetsExample_V.swift: -------------------------------------------------------------------------------- 1 | /// This is a caption to associate with the first named slice. 2 | 3 | // snippet.DECLARATION 4 | func f() -> Int 5 | { 6 | // snippet.BODY 7 | return 2 + 2 8 | // snippet.EXIT 9 | } 10 | -------------------------------------------------------------------------------- /Snippets/using-swiftnio-channels.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOPosix 3 | 4 | func runServer() async throws { 5 | // snippet.bootstrap 6 | // 1. 7 | let server = try await ServerBootstrap(group: NIOSingletons.posixEventLoopGroup) 8 | .bind( // 2. 9 | host: "0.0.0.0", // 3. 10 | port: 2048 // 4. 11 | ) { channel in 12 | // 5. 13 | channel.eventLoop.makeCompletedFuture { 14 | // Add any handlers for parsing or serializing messages here 15 | // We don't need any for this echo example 16 | 17 | // 6. 18 | return try NIOAsyncChannel( 19 | wrappingChannelSynchronously: channel, 20 | configuration: NIOAsyncChannel.Configuration( 21 | inboundType: ByteBuffer.self, // We'll read the raw bytes from the socket 22 | outboundType: ByteBuffer.self // We'll also write raw bytes to the socket 23 | ) 24 | ) 25 | } 26 | } 27 | // snippet.end 28 | 29 | // We create a task group to manage the lifetime of our client connections 30 | // Each client is handled by its own structured task 31 | // snippet.acceptClients 32 | // 1. 33 | try await withThrowingDiscardingTaskGroup { group in 34 | // 2. 35 | try await server.executeThenClose { clients in 36 | // 3. 37 | for try await client in clients { 38 | // 4. 39 | group.addTask { 40 | // 5. 41 | try await handleClient(client) 42 | } 43 | } 44 | } 45 | } 46 | // snippet.end 47 | 48 | // snippet.handleClient 49 | func handleClient(_ client: NIOAsyncChannel) 50 | async throws 51 | { 52 | // 1. 53 | try await client.executeThenClose { inboundMessages, outbound in 54 | // 2. 55 | for try await inboundMessage in inboundMessages { 56 | // 3. 57 | try await outbound.write(inboundMessage) 58 | 59 | // MARK: A 60 | return 61 | } 62 | } 63 | } 64 | // snippet.end 65 | } 66 | 67 | try await runServer() -------------------------------------------------------------------------------- /Snippets/websockets-app.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import AsyncAlgorithms 3 | import Foundation 4 | import Hummingbird 5 | import HummingbirdWSCompression 6 | import HummingbirdWebSocket 7 | import Logging 8 | import ServiceLifecycle 9 | 10 | // snippet.hide 11 | struct ConnectionManager: Service { 12 | 13 | typealias OutputStream = AsyncChannel 14 | 15 | struct Connection { 16 | let name: String 17 | let inbound: WebSocketInboundStream 18 | let outbound: OutputStream 19 | } 20 | 21 | actor OutboundConnections { 22 | 23 | var outboundWriters: [String: OutputStream] 24 | 25 | init() { 26 | self.outboundWriters = [:] 27 | } 28 | 29 | func send(_ output: String) async { 30 | for outbound in outboundWriters.values { 31 | await outbound.send(.text(output)) 32 | } 33 | } 34 | 35 | func add(name: String, outbound: OutputStream) async { 36 | outboundWriters[name] = outbound 37 | await send("\(name) joined") 38 | } 39 | 40 | func remove(name: String) async { 41 | outboundWriters[name] = nil 42 | await send("\(name) left") 43 | } 44 | } 45 | 46 | let connectionStream: AsyncStream 47 | let connectionContinuation: AsyncStream.Continuation 48 | let logger: Logger 49 | 50 | init(logger: Logger) { 51 | let stream = AsyncStream.makeStream() 52 | self.connectionStream = stream.stream 53 | self.connectionContinuation = stream.continuation 54 | self.logger = logger 55 | } 56 | 57 | func run() async {} 58 | 59 | func addUser( 60 | name: String, 61 | inbound: WebSocketInboundStream, 62 | outbound: WebSocketOutboundWriter 63 | ) -> OutputStream { 64 | let outputStream = OutputStream() 65 | let connection = Connection( 66 | name: name, 67 | inbound: inbound, 68 | outbound: outputStream 69 | ) 70 | connectionContinuation.yield(connection) 71 | return outputStream 72 | } 73 | } 74 | 75 | protocol AppArguments { 76 | var hostname: String { get } 77 | var port: Int { get } 78 | } 79 | // snippet.show 80 | 81 | func buildApplication( 82 | _ arguments: some AppArguments 83 | ) async throws -> some ApplicationProtocol { 84 | // snippet.hide 85 | var logger = Logger(label: "WebSocketChat") 86 | logger.logLevel = .trace 87 | let router = Router() 88 | router.middlewares.add(LogRequestsMiddleware(.debug)) 89 | router.middlewares.add(FileMiddleware(logger: logger)) 90 | let wsRouter = Router(context: BasicWebSocketRequestContext.self) 91 | // snippet.show 92 | // ... 93 | let connectionManager = ConnectionManager(logger: logger) 94 | // ... 95 | // 1. 96 | var app = Application( 97 | router: router, 98 | server: .http1WebSocketUpgrade( 99 | webSocketRouter: wsRouter, 100 | configuration: .init(extensions: [.perMessageDeflate()]) 101 | ), 102 | configuration: .init( 103 | address: .hostname(arguments.hostname, port: arguments.port) 104 | ), 105 | logger: logger 106 | ) 107 | // 2. 108 | app.addServices(connectionManager) 109 | return app 110 | } 111 | -------------------------------------------------------------------------------- /Snippets/websockets-connection-manager-add-user.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import AsyncAlgorithms 3 | import Foundation 4 | import Hummingbird 5 | import HummingbirdWSCompression 6 | import HummingbirdWebSocket 7 | import Logging 8 | import ServiceLifecycle 9 | 10 | struct ConnectionManager: Service { 11 | 12 | // snippet.hide 13 | typealias OutputStream = AsyncChannel 14 | 15 | struct Connection { 16 | let name: String 17 | let inbound: WebSocketInboundStream 18 | let outbound: OutputStream 19 | } 20 | 21 | actor OutboundConnections { 22 | 23 | var outboundWriters: [String: OutputStream] 24 | 25 | init() { 26 | self.outboundWriters = [:] 27 | } 28 | 29 | func send(_ output: String) async { 30 | for outbound in outboundWriters.values { 31 | await outbound.send(.text(output)) 32 | } 33 | } 34 | 35 | func add(name: String, outbound: OutputStream) async { 36 | outboundWriters[name] = outbound 37 | await send("\(name) joined") 38 | } 39 | 40 | func remove(name: String) async { 41 | outboundWriters[name] = nil 42 | await send("\(name) left") 43 | } 44 | } 45 | 46 | let connectionStream: AsyncStream 47 | let connectionContinuation: AsyncStream.Continuation 48 | let logger: Logger 49 | 50 | init(logger: Logger) { 51 | let stream = AsyncStream.makeStream() 52 | self.connectionStream = stream.stream 53 | self.connectionContinuation = stream.continuation 54 | self.logger = logger 55 | } 56 | 57 | func run() async {} 58 | 59 | // snippet.show 60 | 61 | // ... 62 | func addUser( 63 | name: String, 64 | inbound: WebSocketInboundStream, 65 | outbound: WebSocketOutboundWriter 66 | ) -> OutputStream { 67 | let outputStream = OutputStream() 68 | let connection = Connection( 69 | name: name, 70 | inbound: inbound, 71 | outbound: outputStream 72 | ) 73 | connectionContinuation.yield(connection) 74 | return outputStream 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Snippets/websockets-connection-manager-types.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import AsyncAlgorithms 3 | import Foundation 4 | import Hummingbird 5 | import HummingbirdWSCompression 6 | import HummingbirdWebSocket 7 | import Logging 8 | import ServiceLifecycle 9 | 10 | // 1. 11 | struct ConnectionManager: Service { 12 | 13 | // 2. 14 | typealias OutputStream = AsyncChannel 15 | 16 | // 3. 17 | struct Connection { 18 | let name: String 19 | let inbound: WebSocketInboundStream 20 | let outbound: OutputStream 21 | } 22 | 23 | // 4. 24 | actor OutboundConnections { 25 | 26 | var outboundWriters: [String: OutputStream] 27 | 28 | init() { 29 | self.outboundWriters = [:] 30 | } 31 | 32 | func send(_ output: String) async { 33 | for outbound in outboundWriters.values { 34 | await outbound.send(.text(output)) 35 | } 36 | } 37 | 38 | func add(name: String, outbound: OutputStream) async { 39 | outboundWriters[name] = outbound 40 | await send("\(name) joined") 41 | } 42 | 43 | func remove(name: String) async { 44 | outboundWriters[name] = nil 45 | await send("\(name) left") 46 | } 47 | } 48 | 49 | // snippet.hide 50 | func run() async {} 51 | // snippet.show 52 | } 53 | -------------------------------------------------------------------------------- /Snippets/websockets.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import AsyncAlgorithms 3 | import Foundation 4 | import Hummingbird 5 | import HummingbirdWSCompression 6 | import HummingbirdWebSocket 7 | import Logging 8 | import ServiceLifecycle 9 | 10 | // snippet.ConnectionManager 11 | struct ConnectionManager: Service { 12 | 13 | // ... 14 | // snippet.hide 15 | typealias OutputStream = AsyncChannel 16 | 17 | struct Connection { 18 | let name: String 19 | let inbound: WebSocketInboundStream 20 | let outbound: OutputStream 21 | } 22 | 23 | actor OutboundConnections { 24 | 25 | var outboundWriters: [String: OutputStream] 26 | 27 | init() { 28 | self.outboundWriters = [:] 29 | } 30 | 31 | func send(_ output: String) async { 32 | for outbound in outboundWriters.values { 33 | await outbound.send(.text(output)) 34 | } 35 | } 36 | 37 | func add(name: String, outbound: OutputStream) async { 38 | outboundWriters[name] = outbound 39 | await send("\(name) joined") 40 | } 41 | 42 | func remove(name: String) async { 43 | outboundWriters[name] = nil 44 | await send("\(name) left") 45 | } 46 | } 47 | // snippet.show 48 | 49 | let connectionStream: AsyncStream 50 | let connectionContinuation: AsyncStream.Continuation 51 | let logger: Logger 52 | 53 | init(logger: Logger) { 54 | let stream = AsyncStream.makeStream() 55 | self.connectionStream = stream.stream 56 | self.connectionContinuation = stream.continuation 57 | self.logger = logger 58 | } 59 | 60 | func run() async { 61 | await withGracefulShutdownHandler { 62 | await withDiscardingTaskGroup { group in 63 | let outboundCounnections = OutboundConnections() 64 | // 1. 65 | for await connection in connectionStream { 66 | group.addTask { 67 | logger.info( 68 | "add connection", 69 | metadata: [ 70 | "name": .string(connection.name) 71 | ] 72 | ) 73 | // 2. 74 | await outboundCounnections.add( 75 | name: connection.name, 76 | outbound: connection.outbound 77 | ) 78 | 79 | do { 80 | // 3. 81 | for try await input in connection.inbound.messages( 82 | maxSize: 1_000_000 83 | ) { 84 | guard case .text(let text) = input else { 85 | continue 86 | } 87 | let output = "[\(connection.name)]: \(text)" 88 | logger.debug( 89 | "Output", 90 | metadata: [ 91 | "message": .string(output) 92 | ] 93 | ) 94 | // 4. 95 | await outboundCounnections.send(output) 96 | } 97 | } 98 | catch {} 99 | 100 | logger.info( 101 | "remove connection", 102 | metadata: [ 103 | "name": .string(connection.name) 104 | ] 105 | ) 106 | // 5. 107 | await outboundCounnections.remove(name: connection.name) 108 | connection.outbound.finish() 109 | } 110 | } 111 | group.cancelAll() 112 | } 113 | } onGracefulShutdown: { 114 | connectionContinuation.finish() 115 | } 116 | } 117 | 118 | // snippet.hide 119 | 120 | func addUser( 121 | name: String, 122 | inbound: WebSocketInboundStream, 123 | outbound: WebSocketOutboundWriter 124 | ) -> OutputStream { 125 | let outputStream = OutputStream() 126 | let connection = Connection( 127 | name: name, 128 | inbound: inbound, 129 | outbound: outputStream 130 | ) 131 | connectionContinuation.yield(connection) 132 | return outputStream 133 | } 134 | 135 | // snippet.show 136 | } 137 | // snippet.end 138 | 139 | // snippet.buildApplication 140 | func buildApplication( 141 | _ arguments: some AppArguments 142 | ) async throws -> some ApplicationProtocol { 143 | var logger = Logger(label: "WebSocketChat") 144 | logger.logLevel = .trace 145 | 146 | // 1. 147 | let router = Router() 148 | router.middlewares.add(LogRequestsMiddleware(.debug)) 149 | router.middlewares.add(FileMiddleware(logger: logger)) 150 | 151 | // 2. 152 | let connectionManager = ConnectionManager(logger: logger) 153 | // 3. 154 | let wsRouter = Router(context: BasicWebSocketRequestContext.self) 155 | wsRouter.middlewares.add(LogRequestsMiddleware(.debug)) 156 | // 4. 157 | wsRouter.ws("chat") { request, _ in 158 | guard request.uri.queryParameters["username"] != nil else { 159 | return .dontUpgrade 160 | } 161 | return .upgrade([:]) 162 | // 5. 163 | } onUpgrade: { inbound, outbound, context in 164 | guard let name = context.request.uri.queryParameters["username"] else { 165 | return 166 | } 167 | let outputStream = connectionManager.addUser( 168 | name: String(name), 169 | inbound: inbound, 170 | outbound: outbound 171 | ) 172 | for try await output in outputStream { 173 | try await outbound.write(output) 174 | } 175 | } 176 | 177 | // ... 178 | // snippet.hide 179 | var app = Application( 180 | router: router, 181 | server: .http1WebSocketUpgrade( 182 | webSocketRouter: wsRouter, 183 | configuration: .init(extensions: [.perMessageDeflate()]) 184 | ), 185 | configuration: .init( 186 | address: .hostname(arguments.hostname, port: arguments.port) 187 | ), 188 | logger: logger 189 | ) 190 | // 7. 191 | app.addServices(connectionManager) 192 | return app 193 | // snippet.show 194 | } 195 | // snippet.end 196 | 197 | // snippet.entrypoint 198 | // 1. 199 | protocol AppArguments { 200 | var hostname: String { get } 201 | var port: Int { get } 202 | } 203 | 204 | // 2. 205 | @main 206 | struct HummingbirdArguments: AppArguments, AsyncParsableCommand { 207 | @Option(name: .shortAndLong) 208 | var hostname: String = "127.0.0.1" 209 | 210 | @Option(name: .shortAndLong) 211 | var port: Int = 8080 212 | 213 | func run() async throws { 214 | // 3. 215 | let app = try await buildApplication(self) 216 | try await app.runService() 217 | } 218 | } 219 | // snippet.end 220 | -------------------------------------------------------------------------------- /Snippets/working-with-udp-in-swiftnio.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOPosix 3 | 4 | // snippet.bootstrap 5 | // 1. 6 | let server = try await DatagramBootstrap(group: NIOSingletons.posixEventLoopGroup) 7 | // 2. 8 | .bind(host: "0.0.0.0", port: 2048) 9 | .flatMapThrowing { channel in 10 | // 3. 11 | return try NIOAsyncChannel( 12 | wrappingChannelSynchronously: channel, 13 | configuration: NIOAsyncChannel.Configuration( 14 | // 4. 15 | inboundType: AddressedEnvelope.self, 16 | outboundType: AddressedEnvelope.self 17 | ) 18 | ) 19 | } 20 | .get() 21 | // snippet.end 22 | 23 | // snippet.packets 24 | try await server.executeThenClose { inbound, outbound in 25 | for try await var packet in inbound { 26 | // 1. 27 | guard let string = packet.data.readString(length: packet.data.readableBytes) else { 28 | continue 29 | } 30 | 31 | // 2. 32 | let response = ByteBuffer(string: String(string.reversed())) 33 | 34 | // 3. 35 | try await outbound.write(AddressedEnvelope(remoteAddress: packet.remoteAddress, data: response)) 36 | } 37 | } 38 | // snippet.end 39 | -------------------------------------------------------------------------------- /Sources/Articles/Documentation.docc/2024/async-http-client-by-example/async-http-client-by-example.md: -------------------------------------------------------------------------------- 1 | # AsyncHTTPClient by example 2 | 3 | Swift ``/AsyncHTTPClient`` is an HTTP client library built on top of SwiftNIO. It provides a solid solution for efficiently managing HTTP requests by leveraging the Swift Concurrency model, thus simplifying networking tasks for developers. 4 | 5 | The library's asynchronous and non-blocking request methods ensure that network operations do not hinder the responsiveness of the application. Additionally, the library offers TLS support, automatic HTTP/2 over HTTPS and several other convenient features. 6 | 7 | The AsyncHTTPClient library is a comprehensive tool for seamless HTTP communication for server-side Swift applications. Throughout this article, we'll delve into practical [examples](https://github.com/swift-on-server/async-http-client-by-example-sample) to showcase the capabilities of this library. 8 | 9 | 10 | ## Setting up & configuring AsyncHTTPClient 11 | 12 | Starting with this article, you can utilize a foundational code example as a starting point for integrating the Swift AsyncHTTPClient library into your Swift projects. 13 | 14 | Now, open the `Package.swift` file in your project directory and add AsyncHTTPClient as a dependency: 15 | 16 | ```swift 17 | // swift-tools-version: 5.10 18 | import PackageDescription 19 | 20 | let package = Package( 21 | name: "async-http-client-by-example-sample", 22 | platforms: [ 23 | .macOS(.v14), 24 | ], 25 | dependencies: [ 26 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") 27 | ], 28 | targets: [ 29 | .executableTarget( 30 | name: "async-http-client-by-example-sample", 31 | dependencies: [ 32 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 33 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 34 | ] 35 | ), 36 | ] 37 | ) 38 | ``` 39 | 40 | In the `main.swift` file, import the AsyncHTTPClient library and initialize an HTTPClient instance for future use: 41 | 42 | @Snippet(path: "site/Snippets/ahc_setup") 43 | 44 | 1. Specify the event loop group provider as ``HTTPClient/EventLoopGroupProvider.singleton``, which manages the underlying ``EventLoopGroup`` for asynchronous operations. 45 | 2. The ``HTTPClient/Configuration`` parameter is set, defining various aspects of the ``HTTPClient``'s behavior. 46 | 3. ``HTTPClient/Configuration/RedirectConfiguration`` is specified to follow redirects up to a maximum of 3 times and disallow redirect cycles. 47 | 4. Set ``HTTPClient/Configuration/Timeout``s for different phases of the HTTP request process, such as connection establishment, reading, and writing. 48 | 5. Cleanup by calling the ``HTTPClient/shutdown() -> ()`` method on the HTTPClient instance. 49 | 50 | Please be aware that it is essential to properly terminate the HTTP client after executing requests. Forgetting to invoke the ``HTTPClient/shutdown() -> ()`` method may cause the library to issue a warning about a potential memory leak when compiling the application in debug mode. 51 | 52 | 53 | ## Performing HTTP requests 54 | 55 | An HTTP request includes the method, a URL, headers providing supplementary details, and optionally, a body containing data transmitted to the server. Conversely, HTTP responses contain a status code, headers providing further details, and a body containing the actual content of the response. Together, these components facilitate the exchange of data between clients and servers over the HTTP protocol. 56 | 57 | Below is an illustration of how to employ the HTTP request and response objects using the AsyncHTTPClient library in Swift: 58 | 59 | @Snippet(path: "site/Snippets/ahc_request") 60 | 61 | 1. A new ``HTTPClientRequest`` object is created targeting the specified URL. 62 | 2. The HTTP request method is set to POST. 63 | 3. A `user-agent` header with the value `"Swift AsyncHTTPClient"` is added to the request. 64 | 4. The request body is set to contain the string "Some data". 65 | 5. The request is executed with a custom timeout of 5 seconds. 66 | 6. If the response status is `.ok` (`200`), further processing is performed. 67 | 7. The `content-type` of the response is retrieved from the headers. 68 | 8. The response body is collected asynchronously into a ``ByteBuffer``, up to a maximum of 1 MiB in size. 69 | 9. The raw response body is converted into a ``String`` for further processing. 70 | 71 | Any errors encountered during the execution of the request are caught and printed. If the response body exceeds the 1 MiB limit, a ``NIOTooManyBytesError`` error will occur. 72 | 73 | Finally, the HTTP client is shut down to release associated resources. 74 | 75 | ## JSON requests 76 | 77 | JSON requests involve sending and receiving data formatted in JSON to a server. REST API is a style for building networked apps where resources are managed using regular HTTP methods, and the data is encoded and decoded using the JSON format. 78 | 79 | The following code snippet demonstrates how to encode request bodies and decode response bodies using JSON objects: 80 | 81 | @Snippet(path: "site/Snippets/ahc_json") 82 | 83 | 1. Two ``Codable`` structures are defined: `Input` for the data to be sent and `Output` for receiving the JSON response. 84 | 2. An HTTP request is created using a POST method and a `content-type: application/json` header. 85 | 3. The `Input` data is encoded into JSON data using a ``ByteBuffer`` and set as the request body. 86 | 4. If the response status is ok and the content type is JSON, the response body is processed. 87 | 5. The response body chunks are collected asynchronously using ``AsyncSequence.collect(upTo:)`` 88 | 6. The buffer containing the JSON data response is decoded as an `Output` structure using ``JSONDecoder.decode(_:from:) (_, ByteBuffer)``. 89 | 90 | The code snippet above demonstrates how to use Swift's Codable protocol to handle JSON data in HTTP communication. It defines structures for input and output data, sends a POST request with JSON payload, and processes the response by decoding JSON into a designated output structure. 91 | 92 | ## File downloads 93 | 94 | The AsyncHTTPClient library provides support for file downloads using the ``FileDownloadDelegate``. This feature enables asynchronous streaming of downloaded data while simultaneously reporting the download progress, as demonstrated in the following example: 95 | 96 | @Snippet(path: "site/Snippets/ahc_download") 97 | 98 | 1. A ``FileDownloadDelegate`` is created to manage file downloads. 99 | 2. Specify the download destination path. 100 | 3. A progress reporting function is provided to monitor the download progress. 101 | 4. The file download request is executed using the request URL and the delegate. 102 | 103 | Running this example will display the download progress, indicating the received bytes and the total bytes, with the same information also available within the `fileDownloadResponse` object. 104 | 105 | There are many more configuration options available for the Swift AsyncHTTPClient library. It is also possible to create custom delegate objects; additional useful examples and code snippets are provided in the project's [README on GitHub](https://github.com/swift-server/async-http-client). 106 | 107 | -------------------------------------------------------------------------------- /Sources/Articles/Documentation.docc/2024/building-swiftnio-clients/building-swiftnio-clients.md: -------------------------------------------------------------------------------- 1 | # Building a SwiftNIO HTTP client 2 | 3 | HTTP clients are a common first networking application to build. HTTP is a well known and simple to understand protocol, making it an excellent start. 4 | 5 | In the previous tutorial, , you learned how to use SwiftNIO to build a simple TCP echo server. In this tutorial, you'll build a simple HTTP client using SwiftNIO. 6 | 7 | We'll use the ``/NIOHTTP1`` package for parsing and serializing HTTP messages. In addition, SwiftNIO's structured concurrency is used to manage the lifecycle of our client. 8 | 9 | By the end of this tutorial, you'll know how to configure a SwiftNIO Channel's pipeline, and are able to send HTTP requests to a server. 10 | 11 | [Download the Samples](https://github.com/swift-on-server/building-swiftnio-clients-sample) to get started. It has a dev container for a quick start. 12 | 13 | > Note: This tutorial will emit some ``Sendable`` warnings. These are expected, and should be resolved in a production ready client implementation. However, for the purposes of this tutorial, ignore them. 14 | 15 | ## Creating a Client Channel 16 | 17 | In SwiftNIO, Channels are created through a bootstrap. For TCP clients, you'd generally use a ``ClientBootstrap``. There are alternative clients as well, such as Apple's Transport Services for Apple platforms. In addition, the ``/NIOHTTP1`` module is used to simplify the process of creating a client channel. 18 | 19 | Add these dependencies to your executable target in your `Package.swift` file: 20 | 21 | ```swift 22 | .executableTarget( 23 | name: "swift-nio-part-3", 24 | dependencies: [ 25 | .product(name: "NIO", package: "swift-nio"), 26 | .product(name: "NIOHTTP1", package: "swift-nio"), 27 | ] 28 | ), 29 | ``` 30 | 31 | Now, let's create a ``ClientBootstrap`` and configure it to use the ``/NIOHTTP1`` module's handlers. First, import the necessary modules: 32 | 33 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "imports") 34 | 35 | Then, create a ``ClientBootstrap``: 36 | 37 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "bootstrap") 38 | 39 | This code prepares a template for creating a client channel. Let's break it down: 40 | 41 | 1. Create a ``ClientBootstrap`` using the `NIOSingletons.posixEventLoopGroup` as the event loop group. This is a shared event loop group that can be reused across multiple components of our application. 42 | 2. NIO Channels can have options set on them. Here, the `SO_REUSEADDR` option is set to `1` to allow the reuse of local addresses. 43 | 3. Then, provide an initializer that is used to configure the pipeline of newly created channels. 44 | 4. Finally, the `channelInitializer` adds the necessary HTTP client handlers to the channel's pipeline. This uses a helper function provided by NIOHTTP1. 45 | 46 | ## Creating Types 47 | 48 | Before creating the HTTP client, it's necessary to add a few types that are needed for processing HTTP requests and responses. 49 | 50 | When a `connect` fails, NIO already throws an error. There is no need to catch or represent those. However, the HTTP Client might encounter errors when processing the response. Create an enum to represent these errors: 51 | 52 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "error") 53 | 54 | Finally, add an enum to represent the state of processing the response: 55 | 56 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "partial") 57 | 58 | The enum if pretty simple, and is not representative of a _mature_ HTTP client implementation such as [AsyncHTTPClient](https://github.com/swift-server/async-http-client). However, it's enough to get started with building a (TCP) client. 59 | 60 | ## Implementing the HTTP Client 61 | 62 | Now that the necessary types have been created, create the `HTTPClient` type with a simple function that sends a request and returns the response. 63 | 64 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "client") 65 | 66 | Let's break it down: 67 | 68 | 1. Use the `httpClientBootstrap` to create a new client channel. This returns an ``EventLoopFuture`` containing a regular NIO ``Channel``. By using ``EventLoopFuture/flatMapThrowing(_:)`` to transform the result of this future, it's possible to convert the ``EventLoopFuture`` into a ``NIOAsyncChannel``. 69 | 2. In order to use structured concurrency, it's necessary to wrap the ``Channel`` in an ``NIOAsyncChannel``. The inbound and outbound types must be ``Sendable``, and need to be configured to match the pipeline's input and output. This is based on the handlers added in the bootstrap's `channelInitializer`. 70 | 3. The ``NIOAsyncChannel`` is configured to receive ``HTTPClientResponsePart`` objects. This is the type that the HTTP client will receive from the server. 71 | 4. The ``NIOAsyncChannel`` is configured to send `SendableHTTPClientRequestPart` objects. This is the type that the HTTP client will send to the server. 72 | 5. The `get()` method is called to *await* for the result of the ``EventLoopFuture``. 73 | 74 | ### Sending a Request 75 | 76 | In place of the TODO comment, add the code to send a request and process the response. First, create a ``HTTPRequestHead``. Note that this function does not currently support sending a body with the request. Do so by adding the following code: 77 | 78 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "executeThenClose") 79 | 80 | This is a structured concurrency block that sends the request: 81 | 82 | 1. The `executeThenClose` method is used to obtain a read and write half of the channel. This function returns the result of it's trailing closure. 83 | 2. The writer called `outbound` is used to send the request's part - the head and 'end'. This is also where the request's body would be sent. 84 | 85 | Below that, receive and process the response parts as such: 86 | 87 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "httpLogic") 88 | 89 | This sets up a state variable to keep track of the response parts received. It then 90 | processes the response parts as they come in: 91 | 92 | 1. A `for` loop is used to iterate over the response parts. This is a structured concurrency block that will continue to run until the channel is closed by the remote, an error is thrown, or a `return` statement ends the function. 93 | 2. The `part` is matched against the ``HTTPClientResponsePart`` enum. If the part is a head, it's stored in the `partialResponse` variable. If the part is a body, it's appended to the buffer in the `partialResponse` variable. If the part is an end, the `partialResponse` is returned. 94 | 3. If the loop ends without a return, an error is thrown, since the code was unable to receive a complete response. 95 | 96 | ## Using the Client 97 | 98 | Now that the HTTP client is complete, it's time to use it. Add the following code to the `main.swift` file: 99 | 100 | @Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "usage") 101 | 102 | This creates a client and sends a GET request to `example.com`. The response is then printed to the console. 103 | 104 | If everything is set up correctly, you should see roughly the following output: 105 | 106 | ``` 107 | HTTPResponseHead { version: HTTP/1.1, status: 200 OK, headers: [("Accept-Ranges", "bytes"), ("Age", "464157"), ("Cache-Control", "max-age=604800"), ("Content-Type", "text/html; charset=UTF-8"), ("Date", "Wed, 07 Feb 2024 21:22:33 GMT"), ("Etag", "\"3147526947\""), ("Expires", "Wed, 14 Feb 2024 21:22:33 GMT"), ("Last-Modified", "Thu, 17 Oct 2019 07:18:26 GMT"), ("Server", "ECS (dce/26CD)"), ("Vary", "Accept-Encoding"), ("X-Cache", "HIT"), ("Content-Length", "1256")] } 108 | 109 | 110 | 111 | Example Domain 112 | 113 | 114 | 115 | 116 |