├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ └── swift.yml ├── .gitignore ├── .swiftlint.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── HTTPHeadersCoding │ ├── HTTPHeadersDecoder.swift │ └── HTTPHeadersEncoder.swift ├── HTTPPathCoding │ ├── Array+getShapeForTemplate.swift │ ├── HTTPPathDecoder.swift │ ├── HTTPPathEncoder.swift │ ├── HTTPPathSegment.swift │ └── HTTPPathToken.swift ├── QueryCoding │ ├── QueryDecoder.swift │ └── QueryEncoder.swift ├── ShapeCoding │ ├── DateISO8601Extensions.swift │ ├── DecodingErrorExtension.swift │ ├── MutableShape.swift │ ├── RawShape.swift │ ├── ShapeCodingKey.swift │ ├── ShapeDecoder+unbox.swift │ ├── ShapeDecoder.swift │ ├── ShapeDecoderDelegate.swift │ ├── ShapeDecodingStorage.swift │ ├── ShapeElement.swift │ ├── ShapeKeyedDecodingContainer+KeyedDecodingContainerProtocol.swift │ ├── ShapeKeyedDecodingContainer.swift │ ├── ShapeKeyedEncodingContainer.swift │ ├── ShapeSingleValueEncodingContainer.swift │ ├── ShapeSingleValueEncodingContainerDelegate.swift │ ├── ShapeUnkeyedDecodingContainer.swift │ ├── ShapeUnkeyedEncodingContainer.swift │ ├── StandardDecodingOptions.swift │ ├── StandardEncodingOptions.swift │ ├── StandardShapeDecoderDelegate.swift │ ├── StandardShapeParser.swift │ └── StandardShapeSingleValueEncodingContainerDelegate.swift ├── SmokeHTTPClient │ ├── AsyncResponseInvocationStrategy.swift │ ├── BodyHTTPRequestInput.swift │ ├── GlobalDispatchQueueAsyncResponseInvocationStrategy.swift │ ├── HTTPClientCoreInvocationReporting.swift │ ├── HTTPClientDelegate.swift │ ├── HTTPClientInnerRetryInvocationReporting.swift │ ├── HTTPClientInvocationContext.swift │ ├── HTTPClientInvocationDelegate.swift │ ├── HTTPClientInvocationReporting.swift │ ├── HTTPClientReportingConfiguration.swift │ ├── HTTPClientRetryConfiguration.swift │ ├── HTTPError.swift │ ├── HTTPInvocationClient.swift │ ├── HTTPOperationsClient +executeAsyncRetriableWithOutput.swift │ ├── HTTPOperationsClient +executeAsyncWithOutput.swift │ ├── HTTPOperationsClient +executeAsyncWithoutOutput.swift │ ├── HTTPOperationsClient +executeSyncRetriableWithOutput.swift │ ├── HTTPOperationsClient +executeSyncRetriableWithoutOutput.swift │ ├── HTTPOperationsClient +executeSyncWithOutput.swift │ ├── HTTPOperationsClient +executeSyncWithoutOutput.swift │ ├── HTTPOperationsClient+executeAsEventLoopFutureRetriableWithOutput.swift │ ├── HTTPOperationsClient+executeAsEventLoopFutureRetriableWithoutOutput.swift │ ├── HTTPOperationsClient+executeAsEventLoopFutureWithOutput.swift │ ├── HTTPOperationsClient+executeAsEventLoopFutureWithoutOutput.swift │ ├── HTTPOperationsClient+executeAsyncRetriableWithoutOutput.swift │ ├── HTTPOperationsClient+executeRetriableWithOutput.swift │ ├── HTTPOperationsClient+executeRetriableWithoutOutput.swift │ ├── HTTPOperationsClient+executeWithOutput.swift │ ├── HTTPOperationsClient+executeWithoutOutput.swift │ ├── HTTPOperationsClient.swift │ ├── HTTPRequestComponents.swift │ ├── HTTPRequestInput.swift │ ├── HTTPRequestInputProtocol.swift │ ├── HTTPResponseComponents.swift │ ├── HTTPResponseOutputProtocol.swift │ ├── HttpClientError.swift │ ├── InvocationTraceContext.swift │ ├── MockCoreInvocationReporting.swift │ ├── MockHTTPClient.swift │ ├── MockHTTPInvocationClient.swift │ ├── MockInvocationTraceContext.swift │ ├── NoHTTPRequestInput.swift │ ├── QueryHTTPRequestInput.swift │ ├── RetriableOutwardsRequestLatencyAggregator.swift │ ├── SameThreadAsyncResponseInvocationStrategy.swift │ ├── StandardHTTPClientCoreInvocationReporting.swift │ ├── StandardHTTPClientInvocationReporting.swift │ └── TestEventLoopProvider.swift └── _SmokeHTTPClientConcurrency │ └── Export.swift └── Tests ├── HTTPHeadersCodingTests ├── HTTPHeadersCodingTestInput.swift └── HTTPHeadersEncoderTests.swift ├── HTTPPathCodingTests ├── GetShapeForTemplateTests.swift ├── HTTPPathCoderTestInput.swift ├── HTTPPathEncoderTests.swift ├── HTTPPathSegmentTests.swift └── HTTPPathTokenTests.swift ├── QueryCodingTests ├── QueryCodingTestInput.swift └── QueryEncoderTests.swift ├── ShapeCodingTests ├── ShapeSingleValueEncodingContainerTests.swift └── StandardShapeParserTests.swift └── SmokeHTTPClientTests ├── MockHTTPClientInvocationClientTests.swift └── SmokeHTTPClientTests.swift /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | run-codeql-linux: 11 | name: Run CodeQL on Linux 12 | runs-on: ubuntu-latest 13 | container: swift:5.8 14 | permissions: 15 | security-events: write 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v2 23 | with: 24 | languages: swift 25 | 26 | - name: Build 27 | run: swift build 28 | 29 | - name: Perform CodeQL Analysis 30 | uses: github/codeql-action/analyze@v2 31 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | LatestVersionBuild: 11 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04, ubuntu-20.04] 15 | swift: ["5.9"] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: swift-actions/setup-swift@v1.25.0 19 | with: 20 | swift-version: ${{ matrix.swift }} 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: swift build -c release 24 | - name: Run tests 25 | run: swift test 26 | OlderVersionBuild: 27 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: [ubuntu-20.04] 31 | swift: ["5.8.1", "5.7.3"] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: swift-actions/setup-swift@v1.25.0 35 | with: 36 | swift-version: ${{ matrix.swift }} 37 | - uses: actions/checkout@v2 38 | - name: Build 39 | run: swift build -c release 40 | - name: Run tests 41 | run: swift test 42 | SwiftLint: 43 | name: SwiftLint version 3.2.1 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v1 47 | - name: GitHub Action for SwiftLint 48 | uses: norio-nomura/action-swiftlint@3.2.1 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | .build/ 4 | .swiftpm/ 5 | *.xcodeproj 6 | *~ 7 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - void_return 4 | - class_delegate_protocol 5 | - weak_delegate 6 | - type_name 7 | - generic_type_name 8 | included: 9 | - Sources 10 | line_length: 150 11 | function_body_length: 12 | warning: 50 13 | error: 75 14 | function_parameter_count: 15 | warning: 8 16 | error: 10 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/amzn/smoke-http/issues), or [recently closed](https://github.com/amzn/smoke-http/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/amzn/smoke-http/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/amzn/smoke-http/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Smoke Framework HTTP client 2 | Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "78db67e5bf4a8543075787f228e8920097319281", 10 | "version": "1.18.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-atomics", 15 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", 19 | "version": "1.1.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", 28 | "version": "1.0.4" 29 | } 30 | }, 31 | { 32 | "package": "swift-distributed-tracing", 33 | "repositoryURL": "https://github.com/apple/swift-distributed-tracing.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "ba07967bb775ed8aa73c46ab731c85b9fb613305", 37 | "version": "1.0.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-log", 42 | "repositoryURL": "https://github.com/apple/swift-log.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "32e8d724467f8fe623624570367e3d50c5638e46", 46 | "version": "1.5.2" 47 | } 48 | }, 49 | { 50 | "package": "swift-metrics", 51 | "repositoryURL": "https://github.com/apple/swift-metrics.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "34025104068262db0cc998ace178975c5ff4f36b", 55 | "version": "2.4.0" 56 | } 57 | }, 58 | { 59 | "package": "swift-nio", 60 | "repositoryURL": "https://github.com/apple/swift-nio.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "6213ba7a06febe8fef60563a4a7d26a4085783cf", 64 | "version": "2.54.0" 65 | } 66 | }, 67 | { 68 | "package": "swift-nio-extras", 69 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "0e0d0aab665ff1a0659ce75ac003081f2b1c8997", 73 | "version": "1.19.0" 74 | } 75 | }, 76 | { 77 | "package": "swift-nio-http2", 78 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "a8ccf13fa62775277a5d56844878c828bbb3be1a", 82 | "version": "1.27.0" 83 | } 84 | }, 85 | { 86 | "package": "swift-nio-ssl", 87 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "e866a626e105042a6a72a870c88b4c531ba05f83", 91 | "version": "2.24.0" 92 | } 93 | }, 94 | { 95 | "package": "swift-nio-transport-services", 96 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "41f4098903878418537020075a4d8a6e20a0b182", 100 | "version": "1.17.0" 101 | } 102 | }, 103 | { 104 | "package": "swift-service-context", 105 | "repositoryURL": "https://github.com/apple/swift-service-context.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "ce0141c8f123132dbd02fd45fea448018762df1b", 109 | "version": "1.0.0" 110 | } 111 | } 112 | ] 113 | }, 114 | "version": 1 115 | } 116 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"). 6 | // You may not use this file except in compliance with the License. 7 | // A copy of the License is located at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // or in the "license" file accompanying this file. This file is distributed 12 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | // express or implied. See the License for the specific language governing 14 | // permissions and limitations under the License. 15 | 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "smoke-http", 20 | platforms: [ 21 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13) 22 | ], 23 | products: [ 24 | .library( 25 | name: "SmokeHTTPClient", 26 | targets: ["SmokeHTTPClient"]), 27 | .library( 28 | name: "_SmokeHTTPClientConcurrency", 29 | targets: ["_SmokeHTTPClientConcurrency"]), 30 | .library( 31 | name: "QueryCoding", 32 | targets: ["QueryCoding"]), 33 | .library( 34 | name: "HTTPHeadersCoding", 35 | targets: ["HTTPHeadersCoding"]), 36 | .library( 37 | name: "HTTPPathCoding", 38 | targets: ["HTTPPathCoding"]), 39 | .library( 40 | name: "ShapeCoding", 41 | targets: ["ShapeCoding"]), 42 | ], 43 | dependencies: [ 44 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.33.0"), 45 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.0"), 46 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 47 | .package(url: "https://github.com/apple/swift-metrics.git", "1.0.0"..<"3.0.0"), 48 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.6.4"), 49 | .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"), 50 | ], 51 | targets: [ 52 | .target( 53 | name: "SmokeHTTPClient", dependencies: [ 54 | .product(name: "Logging", package: "swift-log"), 55 | .product(name: "Metrics", package: "swift-metrics"), 56 | .product(name: "NIO", package: "swift-nio"), 57 | .product(name: "NIOHTTP1", package: "swift-nio"), 58 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 59 | .product(name: "NIOSSL", package: "swift-nio-ssl"), 60 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 61 | .product(name: "Tracing", package: "swift-distributed-tracing"), 62 | .target(name: "HTTPHeadersCoding"), 63 | ]), 64 | .target( 65 | name: "_SmokeHTTPClientConcurrency", dependencies: [ 66 | .target(name: "SmokeHTTPClient"), 67 | ]), 68 | .target( 69 | name: "QueryCoding", dependencies: [ 70 | .target(name: "ShapeCoding"), 71 | ]), 72 | .target( 73 | name: "HTTPHeadersCoding", dependencies: [ 74 | .target(name: "ShapeCoding"), 75 | ]), 76 | .target( 77 | name: "HTTPPathCoding", dependencies: [ 78 | .target(name: "ShapeCoding"), 79 | ]), 80 | .target( 81 | name: "ShapeCoding", dependencies: [ 82 | .product(name: "Logging", package: "swift-log"), 83 | ]), 84 | .testTarget( 85 | name: "SmokeHTTPClientTests", dependencies: [ 86 | .target(name: "SmokeHTTPClient"), 87 | ]), 88 | .testTarget( 89 | name: "ShapeCodingTests", dependencies: [ 90 | .target(name: "ShapeCoding"), 91 | ]), 92 | .testTarget( 93 | name: "QueryCodingTests", dependencies: [ 94 | .target(name: "QueryCoding"), 95 | ]), 96 | .testTarget( 97 | name: "HTTPHeadersCodingTests", dependencies: [ 98 | .target(name: "HTTPHeadersCoding"), 99 | ]), 100 | .testTarget( 101 | name: "HTTPPathCodingTests", dependencies: [ 102 | .target(name: "HTTPPathCoding"), 103 | ]), 104 | ] 105 | ) 106 | -------------------------------------------------------------------------------- /Sources/HTTPHeadersCoding/HTTPHeadersDecoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPHeadersDecoder.swift 15 | // HTTPHeadersCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | /** 22 | Decode HTTP Headers into Swift types. 23 | */ 24 | public struct HTTPHeadersDecoder { 25 | private let options: StandardDecodingOptions 26 | private let userInfo: [CodingUserInfoKey: Any] 27 | 28 | public typealias KeyDecodingStrategy = ShapeKeyDecodingStrategy 29 | public typealias KeyDecodeTransformStrategy = ShapeKeyDecodeTransformStrategy 30 | 31 | /// The strategy to use for decoding maps. 32 | public enum MapDecodingStrategy { 33 | /// The decoder will expect a header for 34 | /// each entry of the map. This is the default. 35 | /// ie. ["theMap.Key": "Value"] --> StackOutput(theMap: ["Key": "Value"]) 36 | /// Matches the encoding strategy `HTTPHeadersEncoder.MapDecodingStrategy.singleHeader`. 37 | case singleHeader 38 | 39 | /// The decoder will expect separate headers for the key and value 40 | /// of each entry of the map, specified as a list. 41 | /// ie. ["theMap.1.KeyTag": "Key", "theMap.1.ValueTag": "Value"] -> StackOutput(theMap: ["Key": "Value"]) 42 | /// Matches the encoding strategy `HTTPHeadersEncoder.MapDecodingStrategy.separateHeadersWith`. 43 | case separateHeadersWith(keyTag: String, valueTag: String) 44 | 45 | var shapeMapDecodingStrategy: ShapeMapDecodingStrategy { 46 | switch self { 47 | case .singleHeader: 48 | return .singleShapeEntry 49 | case let .separateHeadersWith(keyTag: keyTag, valueTag: valueTag): 50 | return .separateShapeEntriesWith(keyTag: keyTag, valueTag: valueTag) 51 | } 52 | } 53 | } 54 | 55 | /** 56 | Initializer. 57 | 58 | - Parameters: 59 | - keyDecodingStrategy: the `KeyDecodingStrategy` to use for decoding. 60 | - mapDecodingStrategy: the `MapDecodingStrategy` to use for decoding. 61 | - keyDecodeTransformStrategy: the `KeyDecodeTransformStrategy` to use for decoding. 62 | */ 63 | public init(userInfo: [CodingUserInfoKey: Any] = [:], 64 | keyDecodingStrategy: KeyDecodingStrategy = .useAsShapeSeparator("-"), 65 | mapDecodingStrategy: MapDecodingStrategy = .singleHeader, 66 | keyDecodeTransformStrategy: KeyDecodeTransformStrategy = .none) { 67 | self.options = StandardDecodingOptions( 68 | shapeKeyDecodingStrategy: keyDecodingStrategy, 69 | shapeMapDecodingStrategy: mapDecodingStrategy.shapeMapDecodingStrategy, 70 | shapeListDecodingStrategy: .collapseListWithIndex, 71 | shapeKeyDecodeTransformStrategy: keyDecodeTransformStrategy) 72 | self.userInfo = userInfo 73 | } 74 | 75 | /** 76 | Decodes an array that represents a set of HTTP Headers into an 77 | instance of the specified type. 78 | 79 | - Parameters: 80 | - type: The type of the value to decode. 81 | - headers: The headers to decode. 82 | - returns: A value of the requested type. 83 | - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted. 84 | - throws: An error if any value throws an error during decoding. 85 | */ 86 | public func decode(_ type: T.Type, from headers: [(String, String?)]) throws -> T { 87 | let stackValue = try StandardShapeParser.parse(with: headers, decoderOptions: options) 88 | 89 | let decoder = ShapeDecoder( 90 | decoderValue: stackValue, 91 | isRoot: true, 92 | userInfo: userInfo, 93 | delegate: StandardShapeDecoderDelegate(options: options)) 94 | 95 | guard let value = try decoder.unbox(stackValue, as: type, isRoot: true) else { 96 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], 97 | debugDescription: "The given data did not contain a top-level value.")) 98 | } 99 | 100 | return value 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/HTTPHeadersCoding/HTTPHeadersEncoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPHeadersEncoder.swift 15 | // HTTPHeadersCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | /// 22 | /// Encode Swift types into HTTP Headers. 23 | /// 24 | /// Nested types, arrays and dictionaries are serialized into header keys using 25 | /// key concatination 26 | /// Array entries are indicated by a 1-based index 27 | /// ie. HeadersInput(theArray: ["Value1", "Value2"]) --> ["theArray1": "Value1", "theArray2": "Value2"] 28 | /// Dictionary entries are indicated by the attribute keys 29 | /// ie. HeadersInput(theMap: [foo: "Value1", bar: "Value2"]) --> ["theMapfoo": "Value1", "theMapbar": "Value2"] 30 | /// Nested type attributes are indicated by the attribute keys 31 | /// ie. HeadersInput(theType: TheType(foo: "Value1", bar: "Value2")) --> ["theArrayfoo": "Value1", "theArraybar": "Value2"] 32 | public class HTTPHeadersEncoder { 33 | public typealias KeyEncodingStrategy = ShapeKeyEncodingStrategy 34 | public typealias KeyEncodeTransformStrategy = ShapeKeyEncodeTransformStrategy 35 | 36 | internal let options: StandardEncodingOptions 37 | 38 | /// The strategy to use for encoding maps. 39 | public enum MapEncodingStrategy { 40 | /// The output will contain a single header for 41 | /// each entry of the map. This is the default. 42 | /// ie. HeadersInput(theMap: ["Key": "Value"]) --> ["theMap.Key": "Value"] 43 | /// Matches the decoding strategy `HTTPHeadersDecoder.MapEncodingStrategy.singleHeader`. 44 | case singleHeader 45 | 46 | /// The output will contain separate headers for the key and value 47 | /// of each entry of the map, specified as a list. 48 | /// ie. HeadersInput(theMap: ["Key": "Value"]) --> ["theMap.1.KeyTag": "Key", "theMap.1.ValueTag": "Value"] 49 | /// Matches the decoding strategy `HTTPHeadersDecoder.MapEncodingStrategy.separateHeadersWith`. 50 | case separateHeadersWith(keyTag: String, valueTag: String) 51 | 52 | var shapeMapEncodingStrategy: ShapeMapEncodingStrategy { 53 | switch self { 54 | case .singleHeader: 55 | return .singleShapeEntry 56 | case let .separateHeadersWith(keyTag: keyTag, valueTag: valueTag): 57 | return .separateShapeEntriesWith(keyTag: keyTag, valueTag: valueTag) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | Initializer. 64 | 65 | - Parameters: 66 | - keyEncodingStrategy: the `KeyEncodingStrategy` to use for encoding. 67 | By default uses `.useAsShapeSeparator("-")`. 68 | - mapEncodingStrategy: the `MapEncodingStrategy` to use for encoding. 69 | By default uses `.singleHeader`. 70 | - KeyEncodeTransformStrategy: the `KeyEncodeTransformStrategy` to use for transforming keys. 71 | By default uses `.none`. 72 | */ 73 | public init(keyEncodingStrategy: KeyEncodingStrategy = .useAsShapeSeparator("-"), 74 | mapEncodingStrategy: MapEncodingStrategy = .singleHeader, 75 | keyEncodeTransformStrategy: KeyEncodeTransformStrategy = .none) { 76 | self.options = StandardEncodingOptions( 77 | shapeKeyEncodingStrategy: keyEncodingStrategy, 78 | shapeMapEncodingStrategy: mapEncodingStrategy.shapeMapEncodingStrategy, 79 | shapeListEncodingStrategy: .expandListWithIndex, 80 | shapeKeyEncodeTransformStrategy: keyEncodeTransformStrategy) 81 | } 82 | 83 | /** 84 | Encode the provided value. 85 | 86 | - Parameters: 87 | - value: The value to be encoded 88 | - allowedCharacterSet: The allowed character set for header values. If nil, 89 | all characters are allowed. 90 | - userInfo: The user info to use for this encoding. 91 | */ 92 | public func encode(_ value: T, 93 | allowedCharacterSet: CharacterSet? = nil, 94 | userInfo: [CodingUserInfoKey: Any] = [:]) throws -> [(String, String?)] { 95 | let delegate = StandardShapeSingleValueEncodingContainerDelegate(options: options) 96 | let container = ShapeSingleValueEncodingContainer( 97 | userInfo: userInfo, 98 | codingPath: [], 99 | delegate: delegate, 100 | allowedCharacterSet: allowedCharacterSet, 101 | defaultValue: nil) 102 | try value.encode(to: container) 103 | 104 | var elements: [(String, String?)] = [] 105 | try container.getSerializedElements(nil, isRoot: true, elements: &elements) 106 | 107 | // The headers need to be sorted into canonical form 108 | let sortedElements = elements.sorted { (left, right) in left.0.lowercased() < right.0.lowercased() } 109 | 110 | return sortedElements 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/HTTPPathCoding/Array+getShapeForTemplate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // Array+getShapeForTemplate.swift 15 | // HTTPPathCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | public extension Array where Element == String { 22 | func getShapeForTemplate( 23 | templateSegments: [HTTPPathSegment], 24 | decoderOptions: StandardDecodingOptions = StandardDecodingOptions( 25 | shapeKeyDecodingStrategy: .useAsShapeSeparator("."), 26 | shapeMapDecodingStrategy: .singleShapeEntry, 27 | shapeListDecodingStrategy: .collapseListWithIndex, 28 | shapeKeyDecodeTransformStrategy: .none)) throws -> Shape { 29 | // reverse the arrays so we can use popLast to iterate in the forwards direction 30 | var remainingPathSegments = Array(self.reversed()) 31 | var remainingTemplateSegments = [HTTPPathSegment](templateSegments.reversed()) 32 | var variables: [(String, String?)] = [] 33 | 34 | // iterate through the path elements 35 | while let templateSegment = remainingTemplateSegments.popLast() { 36 | guard let pathSegment = remainingPathSegments.popLast() else { 37 | throw HTTPPathDecoderErrors.pathDoesNotMatchTemplate("Path has fewer segments than template.") 38 | } 39 | 40 | try templateSegment.parse(value: pathSegment, 41 | variables: &variables, 42 | remainingSegmentValues: &remainingPathSegments, 43 | isLastSegment: remainingTemplateSegments.isEmpty) 44 | } 45 | 46 | guard remainingPathSegments.isEmpty else { 47 | throw HTTPPathDecoderErrors.pathDoesNotMatchTemplate("Path has more segments than template.") 48 | } 49 | 50 | let stackValue = try StandardShapeParser.parse(with: variables, 51 | decoderOptions: decoderOptions) 52 | 53 | return stackValue 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/HTTPPathCoding/HTTPPathDecoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathDecoder.swift 15 | // HTTPPathCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | public enum HTTPPathDecoderErrors: Error { 22 | case pathDoesNotMatchTemplate(String) 23 | } 24 | 25 | /** 26 | Decode HTTP path strings into Swift types. 27 | */ 28 | public struct HTTPPathDecoder { 29 | private let options: StandardDecodingOptions 30 | private let userInfo: [CodingUserInfoKey: Any] 31 | 32 | public typealias KeyDecodingStrategy = ShapeKeyDecodingStrategy 33 | public typealias KeyDecodeTransformStrategy = ShapeKeyDecodeTransformStrategy 34 | 35 | /** 36 | Initializer. 37 | 38 | - Parameters: 39 | - keyDecodingStrategy: the `KeyDecodingStrategy` to use for decoding. 40 | - keyDecodeTransformStrategy: the `KeyDecodeTransformStrategy` to use for decoding. 41 | */ 42 | public init(userInfo: [CodingUserInfoKey: Any] = [:], 43 | keyDecodingStrategy: KeyDecodingStrategy = .useAsShapeSeparator("."), 44 | keyDecodeTransformStrategy: KeyDecodeTransformStrategy = .none) { 45 | self.options = StandardDecodingOptions( 46 | shapeKeyDecodingStrategy: keyDecodingStrategy, 47 | shapeMapDecodingStrategy: .singleShapeEntry, 48 | shapeListDecodingStrategy: .collapseListWithIndex, 49 | shapeKeyDecodeTransformStrategy: keyDecodeTransformStrategy) 50 | self.userInfo = userInfo 51 | } 52 | 53 | /** 54 | Decodes a string that represents a HTTP path into an 55 | instance of the specified type. 56 | 57 | - Parameters: 58 | - type: The type of the value to decode. 59 | - path: The HTTP path to decode. 60 | - withTemplate: The path template to use to decode the path from. 61 | - returns: A value of the requested type. 62 | - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or 63 | if the given string is not a valid HTTP path. 64 | - throws: An error if any value throws an error during decoding. 65 | */ 66 | public func decode(_ type: T.Type, from path: String, 67 | withTemplate template: String) throws -> T { 68 | let pathSegments = HTTPPathSegment.getPathSegmentsForPath(uri: path) 69 | let templateSegments = try HTTPPathSegment.tokenize(template: template) 70 | let shape = try pathSegments.getShapeForTemplate(templateSegments: templateSegments, 71 | decoderOptions: options) 72 | 73 | let decoder = ShapeDecoder( 74 | decoderValue: shape, 75 | isRoot: true, 76 | userInfo: userInfo, 77 | delegate: StandardShapeDecoderDelegate(options: options)) 78 | 79 | guard let value = try decoder.unbox(shape, as: type, isRoot: true) else { 80 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], 81 | debugDescription: "The given data did not contain a top-level value.")) 82 | } 83 | 84 | return value 85 | } 86 | 87 | /** 88 | Decodes a decoded Shape into an instance of the specified type. 89 | 90 | - Parameters: 91 | - type: The type of the value to decode. 92 | - pathShape: Shape constructed from the path. 93 | - returns: A value of the requested type. 94 | - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or 95 | if the given string is not a valid HTTP path. 96 | - throws: An error if any value throws an error during decoding. 97 | */ 98 | public func decode(_ type: T.Type, fromShape pathShape: Shape) throws -> T { 99 | let decoder = ShapeDecoder( 100 | decoderValue: pathShape, 101 | isRoot: true, 102 | userInfo: userInfo, 103 | delegate: StandardShapeDecoderDelegate(options: options)) 104 | 105 | guard let value = try decoder.unbox(pathShape, as: type, isRoot: true) else { 106 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], 107 | debugDescription: "The given data did not contain a top-level value.")) 108 | } 109 | 110 | return value 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/HTTPPathCoding/HTTPPathEncoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathEncoder.swift 15 | // HTTPPathCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | /// 22 | /// Encode Swift types into HTTP paths. 23 | /// 24 | /// Nested types, arrays and dictionaries are serialized into path tokens using a '.' notation. 25 | /// Array entries are indicated by a 1-based index 26 | /// ie. PathInput(theArray: ["Value1", "Value2"]) --> \base\{theArray.1}\{theArray.2}--> \base\Value1\Value2 27 | /// Nested type attributes are indicated by the attribute keys 28 | /// ie. PathInput(theType: TheType(foo: "Value1", bar: "Value2")) --> \base\{theType.1}\{theType.2}--> \base\Value1\Value2 29 | /// Dictionary entries are indicated based on the provided `MapEncodingStrategy` 30 | public class HTTPPathEncoder { 31 | internal let options: StandardEncodingOptions 32 | 33 | public typealias KeyEncodingStrategy = ShapeKeyEncodingStrategy 34 | public typealias KeyEncodeTransformStrategy = ShapeKeyEncodeTransformStrategy 35 | 36 | /** 37 | Initializer. 38 | 39 | - Parameters: 40 | - keyEncodingStrategy: the `KeyEncodingStrategy` to use for encoding. 41 | By default uses `.useAsShapeSeparator(".")`. 42 | - KeyEncodeTransformStrategy: the `KeyEncodeTransformStrategy` to use for transforming keys. 43 | By default uses `.none`. 44 | */ 45 | public init(keyEncodingStrategy: KeyEncodingStrategy = .useAsShapeSeparator("."), 46 | keyEncodeTransformStrategy: KeyEncodeTransformStrategy = .none) { 47 | self.options = StandardEncodingOptions( 48 | shapeKeyEncodingStrategy: keyEncodingStrategy, 49 | shapeMapEncodingStrategy: .singleShapeEntry, 50 | shapeListEncodingStrategy: .expandListWithIndex, 51 | shapeKeyEncodeTransformStrategy: keyEncodeTransformStrategy) 52 | } 53 | 54 | /** 55 | Encode the provided value. 56 | 57 | - Parameters: 58 | - value: The value to be encoded 59 | - withTemplate: The path template to use to encode the value into. 60 | - userInfo: The user info to use for this encoding. 61 | */ 62 | public func encode(_ value: T, 63 | withTemplate template: String, 64 | userInfo: [CodingUserInfoKey: Any] = [:]) throws -> String { 65 | let resultPrefix: String 66 | if let first = template.first, first == "/" { 67 | resultPrefix = "/" 68 | } else { 69 | resultPrefix = "" 70 | } 71 | 72 | let delegate = StandardShapeSingleValueEncodingContainerDelegate(options: options) 73 | let container = ShapeSingleValueEncodingContainer( 74 | userInfo: userInfo, 75 | codingPath: [], 76 | delegate: delegate, 77 | allowedCharacterSet: nil, 78 | defaultValue: nil) 79 | try value.encode(to: container) 80 | 81 | var elements: [(String, String?)] = [] 82 | try container.getSerializedElements(nil, isRoot: true, elements: &elements) 83 | 84 | var mappedElements: [String: String] = [:] 85 | elements.forEach { element in 86 | if let value = element.1 { 87 | mappedElements[element.0] = value 88 | } 89 | } 90 | 91 | let pathSegments = try HTTPPathSegment.tokenize(template: template) 92 | 93 | return try resultPrefix + pathSegments.map { segment in 94 | return try getSegmentAsString(segment: segment, mappedElements: mappedElements) 95 | }.joined(separator: "/") 96 | } 97 | 98 | func getSegmentAsString(segment: HTTPPathSegment, 99 | mappedElements: [String: String]) throws -> String { 100 | let pathElements = segment.tokens 101 | let mappedPathElements: [String] = try pathElements.map { element in 102 | switch element { 103 | case .string(let value): 104 | return value 105 | case .variable(let value, _): 106 | guard let substitutedValue = mappedElements[value] else { 107 | let debugDescription = "Type did not have a value at \(value) for path." 108 | let context = DecodingError.Context(codingPath: [], 109 | debugDescription: debugDescription) 110 | throw DecodingError.valueNotFound(String.self, context) 111 | } 112 | 113 | return substitutedValue 114 | } 115 | } 116 | 117 | return mappedPathElements.joined(separator: "") 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/HTTPPathCoding/HTTPPathToken.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathToken.swift 15 | // HTTPPathCoding 16 | // 17 | 18 | import Foundation 19 | 20 | public enum HTTPPathToken { 21 | case string(String) 22 | case variable(name: String, multiSegment: Bool) 23 | 24 | static let startCharacter: Character = "{" 25 | static let endCharacter: Character = "}" 26 | 27 | public var isMultiSegment: Bool { 28 | switch self { 29 | case .string: 30 | return false 31 | case .variable(name: _, multiSegment: let multiSegment): 32 | return multiSegment 33 | } 34 | } 35 | 36 | public static func tokenize(template: String) throws -> [HTTPPathToken] { 37 | var remainingTemplate = template 38 | var inVariable = false 39 | var tokens: [HTTPPathToken] = [] 40 | 41 | var hasGreedyToken = false 42 | repeat { 43 | let nextSplit = inVariable ? endCharacter : startCharacter 44 | let components = remainingTemplate.split(separator: nextSplit, maxSplits: 1, 45 | omittingEmptySubsequences: false) 46 | 47 | let thisToken = components[0] 48 | let futureTokens = components.count > 1 ? String(components[1]) : "" 49 | 50 | if !thisToken.isEmpty { 51 | if inVariable { 52 | // can only have greedy tokens at the end 53 | if hasGreedyToken { 54 | throw HTTPPathErrors.hasInvalidMultiSegmentTokens 55 | } 56 | 57 | let tokenName: String 58 | let multiSegment: Bool 59 | if let last = thisToken.last, last == "+" { 60 | tokenName = String(thisToken.dropLast()) 61 | multiSegment = true 62 | hasGreedyToken = true 63 | } else { 64 | tokenName = String(thisToken) 65 | multiSegment = false 66 | } 67 | tokens.append(.variable(name: tokenName, multiSegment: multiSegment)) 68 | } else { 69 | tokens.append(.string(String(thisToken.lowercased()))) 70 | } 71 | } else if !tokens.isEmpty { 72 | throw HTTPPathErrors.hasAdjoiningVariables 73 | } 74 | 75 | inVariable = !inVariable 76 | remainingTemplate = futureTokens 77 | } while !remainingTemplate.isEmpty 78 | 79 | return tokens 80 | } 81 | } 82 | 83 | extension HTTPPathToken: Equatable { 84 | public static func == (lhs: HTTPPathToken, rhs: HTTPPathToken) -> Bool { 85 | switch (lhs, rhs) { 86 | case let (.string(leftString), .string(rightString)): 87 | return leftString == rightString 88 | case let (.variable(leftName, leftMultiSegment), .variable(rightName, rightMultiSegment)): 89 | return leftName == rightName && leftMultiSegment == rightMultiSegment 90 | default: 91 | return false 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/QueryCoding/QueryDecoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // QueryDecoder.swift 15 | // QueryCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | /** 22 | Decode query strings into Swift types. 23 | */ 24 | public struct QueryDecoder { 25 | private let options: StandardDecodingOptions 26 | private let userInfo: [CodingUserInfoKey: Any] 27 | 28 | public typealias KeyDecodingStrategy = ShapeKeyDecodingStrategy 29 | public typealias KeyDecodeTransformStrategy = ShapeKeyDecodeTransformStrategy 30 | 31 | /// The strategy to use for decoding maps. 32 | public enum MapDecodingStrategy { 33 | /// The decoder will expect a query entry for 34 | /// each entry of the map. This is the default. 35 | /// ie. ?theMap.Key=Value --> StackOutput(theMap: ["Key": "Value"]) 36 | /// Matches the encoding strategy `QueryEncoder.MapDecodingStrategy.singleQueryEntry`. 37 | case singleQueryEntry 38 | 39 | /// The decoder will expect separate query entries for the key and value 40 | /// of each entry of the map, specified as a list. 41 | /// ie. ?theMap.1.KeyTag=Key&theMap.1.ValueTag=Value -> StackOutput(theMap: ["Key": "Value"]) 42 | /// Matches the encoding strategy `QueryEncoder.MapDecodingStrategy.separateQueryEntriesWith`. 43 | case separateQueryEntriesWith(keyTag: String, valueTag: String) 44 | 45 | var shapeMapDecodingStrategy: ShapeMapDecodingStrategy { 46 | switch self { 47 | case .singleQueryEntry: 48 | return .singleShapeEntry 49 | case let .separateQueryEntriesWith(keyTag: keyTag, valueTag: valueTag): 50 | return .separateShapeEntriesWith(keyTag: keyTag, valueTag: valueTag) 51 | } 52 | } 53 | } 54 | 55 | /// The strategy to use when decoding lists. 56 | public enum ListDecodingStrategy { 57 | /// The index of the item in the list will be used as 58 | /// the tag for each individual item. This is the default strategy. 59 | /// ie. ?theList.1=Value -> ShapeOutput(theList: ["Value"]) 60 | case collapseListWithIndex 61 | 62 | /// The item tag will used as as the tag in addition to the index of the item in the list. 63 | /// ie. ?theList.ItemTag.1=Value -> ShapeOutput(theList: ["Value"]) 64 | case collapseListWithIndexAndItemTag(itemTag: String) 65 | 66 | var shapeListDecodingStrategy: ShapeListDecodingStrategy { 67 | switch self { 68 | case .collapseListWithIndex: 69 | return .collapseListWithIndex 70 | case let .collapseListWithIndexAndItemTag(itemTag: itemTag): 71 | return .collapseListWithIndexAndItemTag(itemTag: itemTag) 72 | } 73 | } 74 | } 75 | 76 | let queryPrefix: Character = "?" 77 | let valuesSeparator: Character = "&" 78 | let equalsSeparator: Character = "=" 79 | 80 | /** 81 | Initializer. 82 | 83 | - Parameters: 84 | - keyDecodingStrategy: the `KeyDecodingStrategy` to use for decoding. 85 | - mapDecodingStrategy: the `MapDecodingStrategy` to use for decoding. 86 | - keyDecodeTransformStrategy: the `KeyDecodeTransformStrategy` to use for decoding. 87 | */ 88 | public init(userInfo: [CodingUserInfoKey: Any] = [:], 89 | keyDecodingStrategy: KeyDecodingStrategy = .useAsShapeSeparator("."), 90 | mapDecodingStrategy: MapDecodingStrategy = .singleQueryEntry, 91 | listDecodingStrategy: ListDecodingStrategy = .collapseListWithIndex, 92 | keyDecodeTransformStrategy: KeyDecodeTransformStrategy = .none) { 93 | self.options = StandardDecodingOptions( 94 | shapeKeyDecodingStrategy: keyDecodingStrategy, 95 | shapeMapDecodingStrategy: mapDecodingStrategy.shapeMapDecodingStrategy, 96 | shapeListDecodingStrategy: listDecodingStrategy.shapeListDecodingStrategy, 97 | shapeKeyDecodeTransformStrategy: keyDecodeTransformStrategy) 98 | self.userInfo = userInfo 99 | } 100 | 101 | /** 102 | Decodes a string that represents an query string into an 103 | instance of the specified type. 104 | 105 | - Parameters: 106 | - type: The type of the value to decode. 107 | - query: The query string to decode. 108 | - returns: A value of the requested type. 109 | - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or 110 | if the given string is not a valid query. 111 | - throws: An error if any value throws an error during decoding. 112 | */ 113 | public func decode(_ type: T.Type, from query: String) throws -> T { 114 | // if the query string starts with a '?' 115 | let valuesString: String 116 | 117 | if let first = query.first, first == queryPrefix { 118 | valuesString = String(query.dropFirst()) 119 | } else { 120 | valuesString = query 121 | } 122 | 123 | let values = valuesString.split(separator: valuesSeparator, 124 | omittingEmptySubsequences: true) 125 | 126 | let entries: [(String, String?)] = values.map { value in String(value).separateOn(character: equalsSeparator) } 127 | 128 | let stackValue = try StandardShapeParser.parse(with: entries, decoderOptions: options) 129 | 130 | let decoder = ShapeDecoder( 131 | decoderValue: stackValue, 132 | isRoot: true, 133 | userInfo: userInfo, 134 | delegate: StandardShapeDecoderDelegate(options: options)) 135 | 136 | guard let value = try decoder.unbox(stackValue, as: type, isRoot: true) else { 137 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], 138 | debugDescription: "The given data did not contain a top-level value.")) 139 | } 140 | 141 | return value 142 | } 143 | } 144 | 145 | private extension String { 146 | func separateOn(character separator: Character) -> (String, String?) { 147 | let components = self.split(separator: separator, maxSplits: 1, omittingEmptySubsequences: true) 148 | 149 | let before = String(components[0]) 150 | let after: String? 151 | 152 | if components.count > 1 { 153 | after = String(components[1]) 154 | } else { 155 | after = nil 156 | } 157 | 158 | return (before, after) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/QueryCoding/QueryEncoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // QueryEncoder.swift 15 | // QueryCoding 16 | // 17 | 18 | import Foundation 19 | import ShapeCoding 20 | 21 | /// 22 | /// Encode Swift types into query strings. 23 | /// 24 | /// Nested types, arrays and dictionaries are serialized into query keys the `QueryKeyEncodingStrategy`. 25 | /// Array entries are indicated by a 1-based index 26 | /// ie. QueryInput(theArray: ["Value1", "Value2"]) --> ?theArray.1=Value1&theArray.2=Value2 27 | /// Nested type attributes are indicated by the attribute keys 28 | /// ie. QueryInput(theType: TheType(foo: "Value1", bar: "Value2")) --> ?theType.foo=Value1&theType.bar=Value2 29 | /// Dictionary entries are indicated based on the provided `MapEncodingStrategy` 30 | public class QueryEncoder { 31 | public typealias KeyEncodingStrategy = ShapeKeyEncodingStrategy 32 | public typealias KeyEncodeTransformStrategy = ShapeKeyEncodeTransformStrategy 33 | 34 | internal let options: StandardEncodingOptions 35 | 36 | /// The strategy to use for encoding maps. 37 | public enum MapEncodingStrategy { 38 | /// The output will contain a single header for 39 | /// each entry of the map. This is the default. 40 | /// ie. QueryInput(theMap: ["Key": "Value"]) --> ?theMap.Key=Value 41 | /// Matches the decoding strategy `QueryDecoder.MapEncodingStrategy.singleQueryEntry`. 42 | case singleQueryEntry 43 | 44 | /// The output will contain separate headers for the key and value 45 | /// of each entry of the map, specified as a list. 46 | /// ie. QueryInput(theMap: ["Key": "Value"]) --> ?theMap.1.KeyTag=Key&theMap.1.ValueTag=Value 47 | /// Matches the decoding strategy `QueryDecoder.MapEncodingStrategy.separateQueryEntriesWith`. 48 | case separateQueryEntriesWith(keyTag: String, valueTag: String) 49 | 50 | var shapeMapEncodingStrategy: ShapeMapEncodingStrategy { 51 | switch self { 52 | case .singleQueryEntry: 53 | return .singleShapeEntry 54 | case let .separateQueryEntriesWith(keyTag: keyTag, valueTag: valueTag): 55 | return .separateShapeEntriesWith(keyTag: keyTag, valueTag: valueTag) 56 | } 57 | } 58 | } 59 | 60 | /// The strategy to use when encoding lists. 61 | public enum ListEncodingStrategy { 62 | /// The index of the item in the list will be used as 63 | /// the tag for each individual item. This is the default strategy. 64 | /// ie. ShapeOutput(theList: ["Value"]) --> ?theList.1=Value 65 | case expandListWithIndex 66 | 67 | /// The item tag will used as as the tag in addition to the index of the item in the list. 68 | /// ie. ShapeOutput(theList: ["Value"]) --> ?theList.ItemTag.1=Value 69 | case expandListWithIndexAndItemTag(itemTag: String) 70 | 71 | var shapeListEncodingStrategy: ShapeListEncodingStrategy { 72 | switch self { 73 | case .expandListWithIndex: 74 | return .expandListWithIndex 75 | case let .expandListWithIndexAndItemTag(itemTag: itemTag): 76 | return .expandListWithIndexAndItemTag(itemTag: itemTag) 77 | } 78 | } 79 | } 80 | 81 | /** 82 | Initializer. 83 | 84 | - Parameters: 85 | - keyEncodingStrategy: the `KeyEncodingStrategy` to use for encoding. 86 | By default uses `.useAsShapeSeparator(".")`. 87 | - mapEncodingStrategy: the `MapEncodingStrategy` to use for encoding. 88 | By default uses `.singleQueryEntry`. 89 | - KeyEncodeTransformStrategy: the `KeyEncodeTransformStrategy` to use for transforming keys. 90 | By default uses `.none`. 91 | */ 92 | public init(keyEncodingStrategy: KeyEncodingStrategy = .useAsShapeSeparator("."), 93 | mapEncodingStrategy: MapEncodingStrategy = .singleQueryEntry, 94 | listEncodingStrategy: ListEncodingStrategy = .expandListWithIndex, 95 | keyEncodeTransformStrategy: KeyEncodeTransformStrategy = .none) { 96 | self.options = StandardEncodingOptions( 97 | shapeKeyEncodingStrategy: keyEncodingStrategy, 98 | shapeMapEncodingStrategy: mapEncodingStrategy.shapeMapEncodingStrategy, 99 | shapeListEncodingStrategy: listEncodingStrategy.shapeListEncodingStrategy, 100 | shapeKeyEncodeTransformStrategy: keyEncodeTransformStrategy) 101 | } 102 | 103 | /** 104 | Encode the provided value. 105 | 106 | - Parameters: 107 | - value: The value to be encoded 108 | - allowedCharacterSet: The allowed character set for query values. If nil, 109 | all characters are allowed. 110 | - userInfo: The user info to use for this encoding. 111 | */ 112 | public func encode(_ value: T, 113 | allowedCharacterSet: CharacterSet? = nil, 114 | userInfo: [CodingUserInfoKey: Any] = [:]) throws -> String { 115 | let delegate = StandardShapeSingleValueEncodingContainerDelegate(options: options) 116 | let container = ShapeSingleValueEncodingContainer( 117 | userInfo: userInfo, 118 | codingPath: [], 119 | delegate: delegate, 120 | allowedCharacterSet: allowedCharacterSet, 121 | defaultValue: nil) 122 | try value.encode(to: container) 123 | 124 | var elements: [(String, String?)] = [] 125 | try container.getSerializedElements(nil, isRoot: true, elements: &elements) 126 | 127 | // The query elements need to be sorted into canonical form 128 | let sortedElements = elements.sorted { (left, right) in left.0.lowercased() < right.0.lowercased() } 129 | 130 | return sortedElements.map { (key, value) in 131 | if let theString = value { 132 | return "\(key)=\(theString)" 133 | } else { 134 | return key 135 | } 136 | }.joined(separator: "&") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/DateISO8601Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DateISO8601Extensions.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | private func iso8601DateFormatter() -> DateFormatter { 21 | let formatter = DateFormatter() 22 | formatter.calendar = Calendar(identifier: .iso8601) 23 | formatter.locale = Locale(identifier: "en_US_POSIX") 24 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 25 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" 26 | return formatter 27 | } 28 | 29 | extension Date { 30 | var iso8601: String { 31 | return iso8601DateFormatter().string(from: self) 32 | } 33 | } 34 | 35 | extension String { 36 | var dateFromISO8601: Date? { 37 | return iso8601DateFormatter().date(from: self) // "Mar 22, 2017, 10:22 AM" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/DecodingErrorExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DecodingErrorExtension.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | internal extension DecodingError { 21 | /// Returns a `.typeMismatch` error describing the expected type. 22 | /// 23 | /// - parameter path: The path of `CodingKey`s taken to decode a value of this type. 24 | /// - parameter expectation: The type expected to be encountered. 25 | /// - parameter reality: The value that was encountered instead of the expected type. 26 | /// - returns: A `DecodingError` with the appropriate path and debug description. 27 | static func typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError { 28 | let description = "Expected to decode \(expectation) but found \(typeDescription(of: reality)) instead." 29 | return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) 30 | } 31 | 32 | /// Returns a description of the type of `value` appropriate for an error message. 33 | /// 34 | /// - parameter value: The value whose type to describe. 35 | /// - returns: A string describing `value`. 36 | static func typeDescription(of value: Any) -> String { 37 | if value is [Any] { 38 | return "an array" 39 | } else if value is [AnyHashable: Any] { 40 | return "a dictionary" 41 | } else { 42 | return "\(type(of: value))" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/MutableShape.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // MutableShape.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /// An enumeration of possible types of a shape that can be muted. 21 | public enum MutableShape { 22 | case dictionary(MutableShapeDictionary) 23 | case string(String) 24 | case null 25 | 26 | /** 27 | Finalizes this MutableShape as a Shape. 28 | */ 29 | public func asShape() -> Shape { 30 | switch self { 31 | case .dictionary(let innerDictionary): 32 | return innerDictionary.asShape() 33 | case .string(let value): 34 | return .string(value) 35 | case .null: 36 | return .null 37 | } 38 | } 39 | } 40 | 41 | /// An enumeration of possible types of a shape. 42 | public enum Shape: Equatable { 43 | case dictionary([String: Shape]) 44 | case string(String) 45 | case null 46 | } 47 | 48 | /// An enumeration of possible types of MutableShapes that can have nested shapes. 49 | public enum NestableMutableShape { 50 | case dictionary(MutableShapeDictionary) 51 | 52 | /** 53 | Finalizes this MutableShape as a Shape. 54 | */ 55 | public func asShape() -> Shape { 56 | switch self { 57 | case .dictionary(let innerDictionary): 58 | return innerDictionary.asShape() 59 | } 60 | } 61 | } 62 | 63 | /// A MutableShape type for a dictionary of MutableShapes 64 | public class MutableShapeDictionary { 65 | private var values: [String: MutableShape] = [:] 66 | 67 | /** 68 | Initializer with an empty dictionary. 69 | */ 70 | public init() { 71 | 72 | } 73 | 74 | /** 75 | Get a value from the current state of the dictionary. 76 | 77 | - Parameters: 78 | - key: the key of the value to retrieve 79 | - Returns: the value of the provided key or nil if there is no such value 80 | */ 81 | public subscript(key: String) -> MutableShape? { 82 | get { 83 | return values[key] 84 | } 85 | set(newValue) { 86 | values[key] = newValue 87 | } 88 | } 89 | 90 | /** 91 | Finalizes this MutableShapeDictionary as a Shape. 92 | */ 93 | public func asShape() -> Shape { 94 | let transformedValues: [String: Shape] = values.mapValues { value in 95 | return value.asShape() 96 | } 97 | 98 | return .dictionary(transformedValues) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/RawShape.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // RawShape.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | An enumeration of possible types of a shape that retains the original decoded structure. 22 | Compared to the `Shape` enumeration, there a difference in how arrays are stored - `Shape` 23 | stores arrays as a dictionary of values keyed by a 1-based index. `RawShape` separates 24 | the definition of dictionaries and arrays, each in their original form. 25 | */ 26 | public enum RawShape: Equatable, Codable { 27 | case dictionary([String: RawShape]) 28 | case array([RawShape]) 29 | case string(String) 30 | 31 | public init(from decoder: Decoder) throws { 32 | do { 33 | let theDictionary = try [String: RawShape](from: decoder) 34 | 35 | self = .dictionary(theDictionary) 36 | } catch { 37 | do { 38 | let theArray = try [RawShape](from: decoder) 39 | 40 | self = .array(theArray) 41 | } catch { 42 | let theString = try String(from: decoder) 43 | 44 | self = .string(theString) 45 | } 46 | } 47 | } 48 | 49 | public func encode(to encoder: Encoder) throws { 50 | switch self { 51 | case .string(let string): 52 | try string.encode(to: encoder) 53 | case .array(let array): 54 | try array.encode(to: encoder) 55 | case .dictionary(let map): 56 | try map.encode(to: encoder) 57 | } 58 | } 59 | 60 | /// Converts an instance of this enumeration to its corresponding `Shape`. 61 | public var asShape: Shape { 62 | switch self { 63 | case .string(let string): 64 | return .string(string) 65 | case .dictionary(let dictionary): 66 | let transformedDictionary = dictionary.mapValues { $0.asShape } 67 | 68 | return .dictionary(transformedDictionary) 69 | case .array(let array): 70 | var transformedDictionary: [String: Shape] = [:] 71 | // map each value in the array to the dictionary keyed by its 1-based index 72 | array.enumerated().forEach { (entry) in 73 | transformedDictionary["\(entry.offset + 1)"] = entry.element.asShape 74 | } 75 | 76 | return .dictionary(transformedDictionary) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeCodingKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeCodingKey.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | public struct ShapeCodingKey: CodingKey { 21 | public var stringValue: String 22 | public var intValue: Int? 23 | 24 | public init?(stringValue: String) { 25 | self.stringValue = stringValue 26 | self.intValue = nil 27 | } 28 | 29 | public init?(intValue: Int) { 30 | self.stringValue = "\(intValue)" 31 | self.intValue = intValue 32 | } 33 | 34 | public init(stringValue: String, intValue: Int?) { 35 | self.stringValue = stringValue 36 | self.intValue = intValue 37 | } 38 | 39 | public init(index: Int) { 40 | self.stringValue = "\(index)" 41 | self.intValue = index 42 | } 43 | 44 | static let `super` = ShapeCodingKey(stringValue: "super")! 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeDecoderDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeDecoderDelegate.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | Delegate type to provide custom logic for a ShapeDecoder. 22 | */ 23 | public protocol ShapeDecoderDelegate { 24 | 25 | /** 26 | Extension point that can provide custom logic for providing the entries 27 | for a keyed container that is being created. 28 | 29 | - Parameters: 30 | - parentContainer: the entries of the parent container. 31 | - containerKey: the key corresponding to the container the 32 | entries are being retrieved for. 33 | - isRoot: if this container represents the root of the type being decoded. 34 | - codingPath: the coding path of the kayed container being created. 35 | - Returns: the dictionary of entries for the keyed container being created. 36 | */ 37 | func getEntriesForKeyedContainer(parentContainer: [String: Shape], 38 | containerKey: CodingKey, 39 | isRoot: Bool, 40 | codingPath: [CodingKey]) throws -> [String: Shape] 41 | 42 | /** 43 | Extension point that can provide custom logic for providing the entries 44 | for a keyed container that is being created. This overload variant is 45 | called when there isn't a parent keyed container. 46 | 47 | - Parameters: 48 | - wrapping: the raw keyed entries of the container being created. 49 | - isRoot: if this container represents the root of the type being decoded. 50 | - codingPath: the coding path of the kayed container being created. 51 | - Returns: the dictionary of entries for the keyed container being created. 52 | */ 53 | func getEntriesForKeyedContainer(wrapping: [String: Shape], 54 | isRoot: Bool, 55 | codingPath: [CodingKey]) throws -> [String: Shape] 56 | 57 | /** 58 | Extension point that can provide custom logic for providing the entries 59 | for an unkeyed container that is being created. 60 | 61 | - Parameters: 62 | - parentContainer: the entries of the parent container. 63 | - containerKey: the key corresponding to the container the 64 | entries are being retrieved for. 65 | - isRoot: if this container represents the root of the type being decoded. 66 | - codingPath: the coding path of the kayed container being created. 67 | - Returns: the array of entries for the keyed container being created. 68 | */ 69 | func getEntriesForUnkeyedContainer(parentContainer: [String: Shape], 70 | containerKey: CodingKey, 71 | isRoot: Bool, 72 | codingPath: [CodingKey]) throws -> [Shape] 73 | 74 | /** 75 | Extension point that can provide custom logic for providing the entries 76 | for an unkeyed container that is being created. This overload variant is 77 | called when there isn't a parent keyed container. 78 | 79 | - Parameters: 80 | - wrapping: the raw keyed entries of the container being created. 81 | - isRoot: if this container represents the root of the type being decoded. 82 | - codingPath: the coding path of the kayed container being created. 83 | - Returns: the dictionary of entries for the keyed container being created. 84 | */ 85 | func getEntriesForUnkeyedContainer(wrapping: [String: Shape], 86 | isRoot: Bool, 87 | codingPath: [CodingKey]) throws -> [Shape] 88 | 89 | /** 90 | Extension point that retrieves the Shape instance from a dictionary 91 | of container entries for the specified key. 92 | 93 | - Parameters: 94 | - parentContainer: the entries of the parent container. 95 | - containerKey: the key corresponding to the entry the 96 | Shape is being retrieved for. 97 | - Returns: Shape for the specified key or nil if there is no such Shape. 98 | */ 99 | func getNestedShape(parentContainer: [String: Shape], 100 | containerKey: CodingKey) throws -> Shape? 101 | } 102 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeDecodingStorage.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeDecodingStorage.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /// Helper class to store a stack of the current MutableShape to use when decoding a shape. 21 | class ShapeDecodingStorage { 22 | private(set) internal var shapes: [NestableMutableShape] = [] 23 | 24 | /// Initializer with no shapes. 25 | public init() {} 26 | 27 | /// The current number of shapes 28 | public var count: Int { 29 | return self.shapes.count 30 | } 31 | 32 | /// Retreive the current top shape 33 | public var topShape: NestableMutableShape? { 34 | return self.shapes.last 35 | } 36 | 37 | /// Push a new stack into the stack 38 | public func push(shape: NestableMutableShape) { 39 | self.shapes.append(shape) 40 | } 41 | 42 | /// Pop the top shape off the stack. 43 | public func popShape() { 44 | precondition(self.shapes.count > 0, "Empty shape stack.") 45 | self.shapes.removeLast() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeElement.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeElement.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | Errors that can be thrown when encoding a shape. 22 | */ 23 | public enum ShapeEncoderError: Error { 24 | case typeNotShapeCompatible(String) 25 | } 26 | 27 | /** 28 | Protocol that elements of a shape must conform to. 29 | */ 30 | public protocol ShapeElement { 31 | /** 32 | Function that gathers the serialized elements that are either this element or contained within this element. 33 | 34 | - Parameters: 35 | - key: the key for this element if any. 36 | - isRoot: if this element is the root of the type being encoded. 37 | - elements: the array to append elements from this element to. 38 | */ 39 | func getSerializedElements(_ key: String?, isRoot: Bool, elements: inout [(String, String?)]) throws 40 | 41 | /** 42 | Function to return the `RawShape` instance that represents this `ShapeElement`. 43 | 44 | - Returns: the corresponding `RawShape` instance. 45 | */ 46 | func asRawShape() throws -> RawShape 47 | } 48 | 49 | /// Conform String to the `ShapeElement` protocol such that it returns itself as 50 | extension String: ShapeElement { 51 | public func getSerializedElements(_ key: String?, isRoot: Bool, elements: inout [(String, String?)]) throws { 52 | if let key = key { 53 | elements.append((key, self)) 54 | } else { 55 | throw ShapeEncoderError.typeNotShapeCompatible("String cannot be used as a shape element without a key") 56 | } 57 | } 58 | 59 | public func asRawShape() throws -> RawShape { 60 | return .string(self) 61 | } 62 | } 63 | 64 | /** 65 | Enumeration of possible values of a container. 66 | */ 67 | public enum ContainerValueType { 68 | /// A single value that conforms to the `ShapeElement` protocol 69 | case singleValue(ShapeElement) 70 | /// an unkeyed container that has a list of values that conform to the `ShapeElement` protocol 71 | case unkeyedContainer([ShapeElement]) 72 | /// a keyed container that has a dictionary of values that conform to the `ShapeElement` protocol 73 | case keyedContainer([String: ShapeElement]) 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeKeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeKeyedDecodingContainer.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | // MARK: Decoding Containers 21 | internal struct ShapeKeyedDecodingContainer { 22 | typealias Key = K 23 | 24 | let decoder: ShapeDecoder 25 | let container: [String: Shape] 26 | private(set) public var codingPath: [CodingKey] 27 | 28 | // MARK: - Initialization 29 | 30 | /// Initializes `self` by referencing the given decoder and container. 31 | internal init(referencing decoder: ShapeDecoder, 32 | wrapping container: [String: Shape], 33 | isRoot: Bool) throws { 34 | self.decoder = decoder 35 | self.codingPath = decoder.codingPath 36 | self.container = try decoder.delegate.getEntriesForKeyedContainer( 37 | wrapping: container, 38 | isRoot: isRoot, 39 | codingPath: decoder.codingPath) 40 | } 41 | 42 | internal init(referencing decoder: ShapeDecoder, 43 | containerKey: CodingKey, 44 | parentContainer: [String: Shape], 45 | isRoot: Bool) throws { 46 | self.decoder = decoder 47 | self.codingPath = decoder.codingPath 48 | self.container = try decoder.delegate.getEntriesForKeyedContainer( 49 | parentContainer: parentContainer, 50 | containerKey: containerKey, 51 | isRoot: isRoot, 52 | codingPath: decoder.codingPath) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeKeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeKeyedEncodingContainer.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | internal struct ShapeKeyedEncodingContainer: KeyedEncodingContainerProtocol { 21 | typealias Key = K 22 | 23 | private let enclosingContainer: ShapeSingleValueEncodingContainer 24 | 25 | init(enclosingContainer: ShapeSingleValueEncodingContainer) { 26 | self.enclosingContainer = enclosingContainer 27 | } 28 | 29 | // MARK: - Swift.KeyedEncodingContainerProtocol Methods 30 | 31 | var codingPath: [CodingKey] { 32 | return enclosingContainer.codingPath 33 | } 34 | 35 | func encodeNil(forKey key: Key) throws { 36 | enclosingContainer.addToKeyedContainer(key: key, value: "") 37 | } 38 | 39 | func encode(_ value: Bool, forKey key: Key) throws { 40 | enclosingContainer.addToKeyedContainer(key: key, value: value ? "true" : "false") 41 | } 42 | 43 | func encode(_ value: Int, forKey key: Key) throws { 44 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 45 | } 46 | 47 | func encode(_ value: Int8, forKey key: Key) throws { 48 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 49 | } 50 | 51 | func encode(_ value: Int16, forKey key: Key) throws { 52 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 53 | } 54 | 55 | func encode(_ value: Int32, forKey key: Key) throws { 56 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 57 | } 58 | 59 | func encode(_ value: Int64, forKey key: Key) throws { 60 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 61 | } 62 | 63 | func encode(_ value: UInt, forKey key: Key) throws { 64 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 65 | } 66 | 67 | func encode(_ value: UInt8, forKey key: Key) throws { 68 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 69 | } 70 | 71 | func encode(_ value: UInt16, forKey key: Key) throws { 72 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 73 | } 74 | 75 | func encode(_ value: UInt32, forKey key: Key) throws { 76 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 77 | } 78 | 79 | func encode(_ value: UInt64, forKey key: Key) throws { 80 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 81 | } 82 | 83 | func encode(_ value: Float, forKey key: Key) throws { 84 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 85 | } 86 | 87 | func encode(_ value: Double, forKey key: Key) throws { 88 | enclosingContainer.addToKeyedContainer(key: key, value: String(value)) 89 | } 90 | 91 | func encode(_ value: String, forKey key: Key) throws { 92 | let encodedValue: String 93 | if let allowedCharacterSet = enclosingContainer.allowedCharacterSet, 94 | let percentEncoded = value.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) { 95 | encodedValue = percentEncoded 96 | } else { 97 | encodedValue = value 98 | } 99 | 100 | enclosingContainer.addToKeyedContainer(key: key, value: encodedValue) 101 | } 102 | 103 | func encode(_ value: T, forKey key: Key) throws where T: Encodable { 104 | let nestedContainer = createNestedContainer(for: key) 105 | 106 | try nestedContainer.encode(value) 107 | } 108 | 109 | func nestedContainer(keyedBy type: NestedKey.Type, 110 | forKey key: Key) -> KeyedEncodingContainer { 111 | let nestedContainer = createNestedContainer(for: key, defaultValue: .keyedContainer([:])) 112 | 113 | let nestedKeyContainer = ShapeKeyedEncodingContainer(enclosingContainer: nestedContainer) 114 | 115 | return KeyedEncodingContainer(nestedKeyContainer) 116 | } 117 | 118 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 119 | let nestedContainer = createNestedContainer(for: key, defaultValue: .unkeyedContainer([])) 120 | 121 | let nestedKeyContainer = ShapeUnkeyedEncodingContainer(enclosingContainer: nestedContainer) 122 | 123 | return nestedKeyContainer 124 | } 125 | 126 | func superEncoder() -> Encoder { return createNestedContainer(for: ShapeCodingKey.super) } 127 | func superEncoder(forKey key: Key) -> Encoder { return createNestedContainer(for: key) } 128 | 129 | // MARK: - 130 | 131 | private func createNestedContainer(for key: NestedKey, 132 | defaultValue: ContainerValueType? = nil) 133 | -> ShapeSingleValueEncodingContainer { 134 | let nestedContainer = ShapeSingleValueEncodingContainer( 135 | userInfo: enclosingContainer.userInfo, 136 | codingPath: enclosingContainer.codingPath + [key], 137 | delegate: enclosingContainer.delegate, 138 | allowedCharacterSet: enclosingContainer.allowedCharacterSet, 139 | defaultValue: defaultValue) 140 | enclosingContainer.addToKeyedContainer(key: key, value: nestedContainer) 141 | 142 | return nestedContainer 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeSingleValueEncodingContainerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"). 5 | // You may not use this file except in compliance with the License. 6 | // A copy of the License is located at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // or in the "license" file accompanying this file. This file is distributed 11 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | // express or implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | // 15 | // ShapeSingleValueEncodingContainerDelegate.swift 16 | // ShapeCoding 17 | // 18 | 19 | import Foundation 20 | 21 | /** 22 | Delegate class that provides custom logic for a ShapeSingleValueEncodingContainer. 23 | */ 24 | public protocol ShapeSingleValueEncodingContainerDelegate { 25 | /** 26 | Function that gathers the serialized elements for an encoding container. 27 | 28 | - Parameters: 29 | - containerValue: the value of the container if any 30 | - key: the key of the container if any. 31 | - isRoot: if this container is the root of the type being encoded. 32 | - elements: the array to append elements from this container to. 33 | */ 34 | func serializedElementsForEncodingContainer( 35 | containerValue: ContainerValueType?, 36 | key: String?, 37 | isRoot: Bool, 38 | elements: inout [(String, String?)]) throws 39 | 40 | /** 41 | Function to return the `RawShape` instance that represents the provider `ContainerValueType`. 42 | 43 | - Parameters: 44 | - containerValue: the containerValue to return the `RawShape` for. 45 | - Returns: the corresponding `RawShape` instance. 46 | */ 47 | func rawShapeForEncodingContainer(containerValue: ContainerValueType?) throws -> RawShape 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/ShapeUnkeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // ShapeUnkeyedEncodingContainer.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | internal struct ShapeUnkeyedEncodingContainer: UnkeyedEncodingContainer { 21 | private let enclosingContainer: ShapeSingleValueEncodingContainer 22 | 23 | init(enclosingContainer: ShapeSingleValueEncodingContainer) { 24 | self.enclosingContainer = enclosingContainer 25 | } 26 | 27 | // MARK: - Swift.UnkeyedEncodingContainer Methods 28 | 29 | var codingPath: [CodingKey] { 30 | return enclosingContainer.codingPath 31 | } 32 | 33 | var count: Int { return enclosingContainer.unkeyedContainerCount } 34 | 35 | func encodeNil() throws { 36 | enclosingContainer.addToUnkeyedContainer(value: "") } 37 | 38 | func encode(_ value: Bool) throws { 39 | enclosingContainer.addToUnkeyedContainer(value: value ? "true" : "false") } 40 | 41 | func encode(_ value: Int) throws { 42 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 43 | } 44 | 45 | func encode(_ value: Int8) throws { 46 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 47 | } 48 | 49 | func encode(_ value: Int16) throws { 50 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 51 | } 52 | 53 | func encode(_ value: Int32) throws { 54 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 55 | } 56 | 57 | func encode(_ value: Int64) throws { 58 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 59 | } 60 | 61 | func encode(_ value: UInt) throws { 62 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 63 | } 64 | 65 | func encode(_ value: UInt8) throws { 66 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 67 | } 68 | 69 | func encode(_ value: UInt16) throws { 70 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 71 | } 72 | 73 | func encode(_ value: UInt32) throws { 74 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 75 | } 76 | 77 | func encode(_ value: UInt64) throws { 78 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 79 | } 80 | 81 | func encode(_ value: Float) throws { 82 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 83 | } 84 | 85 | func encode(_ value: Double) throws { 86 | enclosingContainer.addToUnkeyedContainer(value: String(value)) 87 | } 88 | 89 | func encode(_ value: String) throws { 90 | let encodedValue: String 91 | if let allowedCharacterSet = enclosingContainer.allowedCharacterSet, 92 | let percentEncoded = value.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) { 93 | encodedValue = percentEncoded 94 | } else { 95 | encodedValue = value 96 | } 97 | 98 | enclosingContainer.addToUnkeyedContainer(value: encodedValue) 99 | } 100 | 101 | func encode(_ value: T) throws where T: Encodable { 102 | try createNestedContainer().encode(value) 103 | } 104 | 105 | func nestedContainer(keyedBy type: NestedKey.Type) -> KeyedEncodingContainer { 106 | let nestedContainer = createNestedContainer(defaultValue: .keyedContainer([:])) 107 | 108 | let nestedKeyContainer = ShapeKeyedEncodingContainer(enclosingContainer: nestedContainer) 109 | 110 | return KeyedEncodingContainer(nestedKeyContainer) 111 | } 112 | 113 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 114 | let nestedContainer = createNestedContainer(defaultValue: .unkeyedContainer([])) 115 | 116 | let nestedKeyContainer = ShapeUnkeyedEncodingContainer(enclosingContainer: nestedContainer) 117 | 118 | return nestedKeyContainer 119 | } 120 | 121 | func superEncoder() -> Encoder { return createNestedContainer() } 122 | 123 | // MARK: - 124 | 125 | private func createNestedContainer(defaultValue: ContainerValueType? = nil) 126 | -> ShapeSingleValueEncodingContainer { 127 | let index = enclosingContainer.unkeyedContainerCount 128 | 129 | let nestedContainer = ShapeSingleValueEncodingContainer( 130 | userInfo: enclosingContainer.userInfo, 131 | codingPath: enclosingContainer.codingPath + [ShapeCodingKey(index: index)], 132 | delegate: enclosingContainer.delegate, 133 | allowedCharacterSet: enclosingContainer.allowedCharacterSet, 134 | defaultValue: defaultValue) 135 | enclosingContainer.addToUnkeyedContainer(value: nestedContainer) 136 | 137 | return nestedContainer 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/StandardDecodingOptions.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardDecodingOptions.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /// The strategy to use for decoding shape keys. 21 | public enum ShapeKeyDecodingStrategy { 22 | /// The decoder will spilt shape keys on the specified character to indicate a 23 | /// nested structure that could include nested types, dictionaries and arrays. This is the default. 24 | /// 25 | /// Array entries are indicated by a 1-based index 26 | /// ie. ["theArray.1": "Value1", "theArray.2": "Value2] --> ShapeOutput(theArray: ["Value1", "Value2"]) 27 | /// Nested type attributes are indicated by the attribute keys 28 | /// ie. ["theArray.foo": "Value1", "theArray.bar": "Value2] --> ShapeOutput(theType: TheType(foo: "Value1", bar: "Value2")) 29 | /// Dictionary entries are indicated based on the provided `ShapeMapDecodingStrategy` 30 | /// Matches the encoding strategy `ShapeKeyDecodingStrategy.useAsShapeSeparator`. 31 | case useAsShapeSeparator(Character) 32 | 33 | /// The decoder will spilt the shape keys on the expected name of an attribute to indicate a 34 | /// nested structure that could include nested types, dictionaries and arrays. 35 | /// 36 | /// Array entries are indicated by a 1-based index 37 | /// ie. ["theArray1": "Value1", "theArray2": "Value2] --> ShapeOutput(theArray: ["Value1", "Value2"]) 38 | /// Nested type attributes are indicated by the attribute keys 39 | /// ie. ["theTypefoo": "Value1", "theTypebar": "Value2] --> ShapeOutput(theType: TheType(foo: "Value1", bar: "Value2")) 40 | /// Dictionaries have dynamic membership based on what is present in the payload and therefore have no expected 41 | /// attribute names. Dictionaries cannot use this strategy to indicate a nested structure beyond their own attributes. 42 | /// ie. ["theMapfoo": "Value1", "theMapbar": "Value2] --> ShapeOutput(theMap: [foo: "Value1", bar: "Value2"]) 43 | /// The above transformation is possible with this strategy. This strategy cannot be used if the map's attributes 44 | /// contains nested attributes. In these scenarios, a specific shape separator will need to be used. 45 | /// Matches the encoding strategy `ShapeKeyDecodingStrategy.noSeparator`. 46 | case useShapePrefix 47 | 48 | /// The decoder will decode shape keys into the attributes 49 | /// of the provided type. No nested types, lists or dictionaries are possible. 50 | case flatStructure 51 | } 52 | 53 | /// The strategy to use for decoding maps. 54 | public enum ShapeMapDecodingStrategy { 55 | /// The decoder will expect a single shape entry for 56 | /// each entry of the map. This is the default. 57 | /// ie. ["theMap.Key": "Value"] --> ShapeOutput(theMap: ["Key": "Value"]) 58 | case singleShapeEntry 59 | 60 | /// The decoder will expect separate entries for the key and value 61 | /// of each entry of the map, specified as a list. 62 | /// ie. ["theMap.1.KeyTag": "Key", "theMap.1.ValueTag": "Value"] -> ShapeOutput(theMap: ["Key": "Value"]) 63 | case separateShapeEntriesWith(keyTag: String, valueTag: String) 64 | } 65 | 66 | 67 | /// The strategy to use when decoding lists. 68 | public enum ShapeListDecodingStrategy { 69 | /// The index of the item in the list will be used as 70 | /// the tag for each individual item. This is the default strategy. 71 | /// ie. ["theList.1": "Value"] -> ShapeOutput(theList: ["Value"]) 72 | case collapseListWithIndex 73 | 74 | /// The item tag will used as as the tag in addition to the index of the item in the list. 75 | /// ie. ["theList.ItemTag.1": "Value"] -> ShapeOutput(theList: ["Value"]) 76 | case collapseListWithIndexAndItemTag(itemTag: String) 77 | } 78 | 79 | 80 | /// The strategy to use for transforming shape keys. 81 | public enum ShapeKeyDecodeTransformStrategy { 82 | /// The shape keys will not be transformed. 83 | case none 84 | 85 | /// The first character of shape keys will be uncapitialized. 86 | case uncapitalizeFirstCharacter 87 | 88 | /// The shape key will be transformed using the provided function. 89 | case custom((String) -> String) 90 | } 91 | 92 | /// The standard decoding options to use in conjunction with 93 | /// StandardShapeDecoderDelegate. 94 | public struct StandardDecodingOptions { 95 | public let shapeKeyDecodingStrategy: ShapeKeyDecodingStrategy 96 | public let shapeMapDecodingStrategy: ShapeMapDecodingStrategy 97 | public let shapeListDecodingStrategy: ShapeListDecodingStrategy 98 | public let shapeKeyDecodeTransformStrategy: ShapeKeyDecodeTransformStrategy 99 | 100 | public init(shapeKeyDecodingStrategy: ShapeKeyDecodingStrategy, 101 | shapeMapDecodingStrategy: ShapeMapDecodingStrategy, 102 | shapeListDecodingStrategy: ShapeListDecodingStrategy, 103 | shapeKeyDecodeTransformStrategy: ShapeKeyDecodeTransformStrategy) { 104 | self.shapeKeyDecodingStrategy = shapeKeyDecodingStrategy 105 | self.shapeMapDecodingStrategy = shapeMapDecodingStrategy 106 | self.shapeListDecodingStrategy = shapeListDecodingStrategy 107 | self.shapeKeyDecodeTransformStrategy = shapeKeyDecodeTransformStrategy 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/StandardEncodingOptions.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardEncodingOptions.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /// The strategy to use for encoding shape keys. 21 | public enum ShapeKeyEncodingStrategy { 22 | /// The encoder will concatinate attribute keys specified character to indicate a 23 | /// nested structure that could include nested types, dictionaries and arrays. This is the default. 24 | /// 25 | /// Array entries are indicated by a 1-based index 26 | /// ie. ShapeOutput(theArray: ["Value1", "Value2"]) --> ["theArray.1": "Value1", "theArray.2": "Value2] 27 | /// Nested type attributes are indicated by the attribute keys 28 | /// ie. ShapeOutput(theType: TheType(foo: "Value1", bar: "Value2")) ?theType.foo=Value1&theType.bar=Value2 29 | /// Dictionary entries are indicated based on the provided `ShapeMapEncodingStrategy` 30 | /// Matches the decoding strategy `ShapeKeyDecodingStrategy.useAsShapeSeparator`. 31 | case useAsShapeSeparator(Character) 32 | 33 | /// The encoder will concatinate attribute keys with no separator. 34 | /// Matches the decoding strategy `ShapeKeyDecodingStrategy.useShapePrefix`. 35 | case noSeparator 36 | 37 | /// Get the separator string to use for this strategy 38 | var separatorString: String { 39 | switch self { 40 | case .useAsShapeSeparator(let character): 41 | return "\(character)" 42 | case .noSeparator: 43 | return "" 44 | } 45 | } 46 | } 47 | 48 | /// The strategy to use for encoding maps. 49 | public enum ShapeMapEncodingStrategy { 50 | /// The output will contain a single shape entry for 51 | /// each entry of the map. This is the default. 52 | /// ie. ShapeOutput(theMap: ["Key": "Value"]) --> ["theMap.Key": "Value"] 53 | case singleShapeEntry 54 | 55 | /// The output will contain separate entries for the key and value 56 | /// of each entry of the map, specified as a list. 57 | /// ie. ShapeOutput(theMap: ["Key": "Value"]) --> ["theMap.1.KeyTag": "Key", "theMap.1.ValueTag": "Value"] 58 | case separateShapeEntriesWith(keyTag: String, valueTag: String) 59 | } 60 | 61 | /// The strategy to use when encoding lists. 62 | public enum ShapeListEncodingStrategy { 63 | /// The index of the item in the list will be used as 64 | /// the tag for each individual item. This is the default strategy. 65 | /// ie. ShapeOutput(theList: ["Value"]) --> ["theList.1": "Value"] 66 | case expandListWithIndex 67 | 68 | /// The item tag will used as as the tag in addition to the index of the item in the list. 69 | /// ie. ShapeOutput(theList: ["Value"]) --> ["theList.ItemTag.1": "Value"] 70 | case expandListWithIndexAndItemTag(itemTag: String) 71 | } 72 | 73 | /// The strategy to use for transforming shape keys. 74 | public enum ShapeKeyEncodeTransformStrategy { 75 | /// The shape keys will not be transformed. 76 | case none 77 | 78 | /// The first character of shape keys will be capitialized. 79 | case capitalizeFirstCharacter 80 | 81 | /// The shape key will be transformed using the provided function. 82 | case custom((String) -> String) 83 | } 84 | 85 | /// The standard encoding options to use in conjunction with 86 | /// StandardShapeSingleValueEncodingContainerDelegate. 87 | public struct StandardEncodingOptions { 88 | public let shapeKeyEncodingStrategy: ShapeKeyEncodingStrategy 89 | public let shapeMapEncodingStrategy: ShapeMapEncodingStrategy 90 | public let shapeListEncodingStrategy: ShapeListEncodingStrategy 91 | public let shapeKeyEncodeTransformStrategy: ShapeKeyEncodeTransformStrategy 92 | 93 | public init(shapeKeyEncodingStrategy: ShapeKeyEncodingStrategy, 94 | shapeMapEncodingStrategy: ShapeMapEncodingStrategy, 95 | shapeListEncodingStrategy: ShapeListEncodingStrategy, 96 | shapeKeyEncodeTransformStrategy: ShapeKeyEncodeTransformStrategy) { 97 | self.shapeKeyEncodingStrategy = shapeKeyEncodingStrategy 98 | self.shapeMapEncodingStrategy = shapeMapEncodingStrategy 99 | self.shapeListEncodingStrategy = shapeListEncodingStrategy 100 | self.shapeKeyEncodeTransformStrategy = shapeKeyEncodeTransformStrategy 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/StandardShapeParser.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardShapeParser.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /// Parses a [String: String] into an Shape structure. 21 | public struct StandardShapeParser { 22 | let storage = ShapeDecodingStorage() 23 | var rootShape: NestableMutableShape? 24 | var codingPath: [CodingKey] = [] 25 | 26 | let decoderOptions: StandardDecodingOptions 27 | 28 | private init(decoderOptions: StandardDecodingOptions) { 29 | self.decoderOptions = decoderOptions 30 | } 31 | 32 | /// Parses an array of entries into an Shape structure. 33 | public static func parse(with entries: [(String, String?)], decoderOptions: StandardDecodingOptions) throws -> Shape { 34 | 35 | var parser = StandardShapeParser(decoderOptions: decoderOptions) 36 | try parser.parse(shapeName: nil, with: entries) 37 | 38 | return parser.rootShape?.asShape() ?? .null 39 | } 40 | 41 | mutating func parse(shapeName: String?, with entries: [(String, String?)]) throws { 42 | // create a dictionary for the array 43 | let mutableShapeDictionary = MutableShapeDictionary() 44 | 45 | // either this is the root shape or 46 | // add it to the current shape 47 | if rootShape == nil { 48 | rootShape = .dictionary(mutableShapeDictionary) 49 | } else { 50 | try addChildMutableShape(shapeName: shapeName, mutableShape: .dictionary(mutableShapeDictionary)) 51 | } 52 | 53 | // add as the new top shape 54 | storage.push(shape: .dictionary(mutableShapeDictionary)) 55 | 56 | switch decoderOptions.shapeKeyDecodingStrategy { 57 | case .flatStructure, .useShapePrefix: 58 | try parseWithoutShapeSeparator(with: entries) 59 | case .useAsShapeSeparator(let separatorCharacter): 60 | try parseWithShapeSeparator(with: entries, 61 | separatorCharacter: separatorCharacter) 62 | } 63 | 64 | // remove the top shape 65 | storage.popShape() 66 | } 67 | 68 | mutating func parseWithoutShapeSeparator(with entries: [(String, String?)]) throws { 69 | try entries.forEach { try addEntry($0) } 70 | } 71 | 72 | private func transformKey(_ untransformedKey: String) -> String { 73 | let key: String 74 | switch decoderOptions.shapeKeyDecodeTransformStrategy { 75 | case .none: 76 | key = untransformedKey 77 | case .uncapitalizeFirstCharacter: 78 | if untransformedKey.count > 0 { 79 | key = untransformedKey.prefix(1).lowercased() 80 | + untransformedKey.dropFirst() 81 | } else { 82 | key = "" 83 | } 84 | case .custom(let transform): 85 | key = transform(untransformedKey) 86 | } 87 | 88 | return key 89 | } 90 | 91 | mutating func parseWithShapeSeparator(with entries: [(String, String?)], 92 | separatorCharacter: Character) throws { 93 | var nonNestedShapeEntries: [(String, String?)] = [] 94 | var nestedShapeEntries: [String: [(String, String?)]] = [:] 95 | 96 | entries.forEach { entry in 97 | let components = entry.0.split(separator: separatorCharacter, maxSplits: 1, omittingEmptySubsequences: true) 98 | 99 | // if this is part of a nested shape 100 | if components.count > 1 { 101 | // add to the nested shape 102 | let shapeName = String(components[0]) 103 | let nestedEntryName = String(components[1]) 104 | if var currentShape = nestedShapeEntries[shapeName] { 105 | currentShape.append((nestedEntryName, entry.1)) 106 | nestedShapeEntries[shapeName] = currentShape 107 | } else { 108 | nestedShapeEntries[shapeName] = [(nestedEntryName, entry.1)] 109 | } 110 | } else { 111 | nonNestedShapeEntries.append(entry) 112 | } 113 | } 114 | 115 | // add any non nested shape entries as normal 116 | try nonNestedShapeEntries.forEach { try addEntry($0) } 117 | 118 | // iterate through the nested shape entries 119 | try nestedShapeEntries.forEach { shape in 120 | let key = transformKey(shape.key) 121 | 122 | try parse(shapeName: key, with: shape.value) 123 | } 124 | } 125 | 126 | mutating func addEntry(_ entry: (String, String?)) throws { 127 | let key = transformKey(entry.0) 128 | 129 | if let value = entry.1 { 130 | guard let removedPercentEncoding = value.removingPercentEncoding else { 131 | throw DecodingError.dataCorrupted(DecodingError.Context( 132 | codingPath: codingPath, 133 | debugDescription: "Unable to remove percent encoding from value '\(value)'")) 134 | } 135 | try addChildMutableShape(shapeName: key, mutableShape: .string(removedPercentEncoding)) 136 | } else { 137 | try addChildMutableShape(shapeName: key, mutableShape: .null) 138 | } 139 | } 140 | 141 | /// Add a child value to the shape 142 | mutating func addChildMutableShape(shapeName: String?, mutableShape: MutableShape) throws { 143 | if let topShape = storage.topShape { 144 | switch topShape { 145 | case .dictionary(let dictionary): 146 | guard let fieldName = shapeName else { 147 | throw DecodingError.dataCorrupted(DecodingError.Context( 148 | codingPath: codingPath, 149 | debugDescription: "Attempted to add to dictionary without a field name.")) 150 | } 151 | 152 | // add to the existing dictionary 153 | dictionary[fieldName] = mutableShape 154 | } 155 | } else { 156 | throw DecodingError.dataCorrupted(DecodingError.Context( 157 | codingPath: codingPath, 158 | debugDescription: "Attempted to add a child value without an enclosing shape")) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sources/ShapeCoding/StandardShapeSingleValueEncodingContainerDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardShapeSingleValueEncodingContainerDelegate.swift 15 | // ShapeCoding 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | A delegate type conforming to ShapeSingleValueEncodingContainerDelegate 22 | that will encode a shape using the options contained in StandardEncodingOptions. 23 | */ 24 | public struct StandardShapeSingleValueEncodingContainerDelegate: 25 | ShapeSingleValueEncodingContainerDelegate { 26 | public let options: StandardEncodingOptions 27 | 28 | public init(options: StandardEncodingOptions) { 29 | self.options = options 30 | } 31 | 32 | public func serializedElementsForEncodingContainer( 33 | containerValue: ContainerValueType?, 34 | key: String?, 35 | isRoot: Bool, 36 | elements: inout [(String, String?)]) throws { 37 | // the encoding process must have placed a value in this container 38 | guard let containerValue = containerValue else { 39 | fatalError("Attempted to access uninitialized container.") 40 | } 41 | 42 | let separatorString = options.shapeKeyEncodingStrategy.separatorString 43 | 44 | switch containerValue { 45 | case .singleValue(let value): 46 | // get the serialized elements from the container value 47 | return try value.getSerializedElements(key, isRoot: false, elements: &elements) 48 | case .unkeyedContainer(let values): 49 | if let key = key { 50 | // for each of the values 51 | try values.enumerated().forEach { (index, value) in 52 | let innerkey: String 53 | 54 | if !isRoot, case let .expandListWithIndexAndItemTag(itemTag: itemTag) = options.shapeListEncodingStrategy { 55 | innerkey = "\(key)\(separatorString)\(itemTag)\(separatorString)\(index + 1)" 56 | } else { 57 | innerkey = "\(key)\(separatorString)\(index + 1)" 58 | } 59 | 60 | // get the serialized elements from this value 61 | try value.getSerializedElements(innerkey, isRoot: false, elements: &elements) 62 | } 63 | } else { 64 | throw ShapeEncoderError.typeNotShapeCompatible("Lists cannot be used as a shape element without a key") 65 | } 66 | case .keyedContainer(let values): 67 | let sortedValues = values.sorted { (left, right) in left.key < right.key } 68 | 69 | try sortedValues.enumerated().forEach { entry in 70 | let innerKey: String 71 | let index = entry.offset 72 | let keyToUse: String 73 | 74 | let untransformedKey = entry.element.key 75 | switch options.shapeKeyEncodeTransformStrategy { 76 | case .none: 77 | innerKey = untransformedKey 78 | case .capitalizeFirstCharacter: 79 | if untransformedKey.count > 0 { 80 | innerKey = untransformedKey.prefix(1).capitalized 81 | + untransformedKey.dropFirst() 82 | } else { 83 | innerKey = "" 84 | } 85 | case .custom(let transform): 86 | innerKey = transform(untransformedKey) 87 | } 88 | 89 | // if this isn't the root and using the separateShapeEntriesWith strategy 90 | if !isRoot, case let .separateShapeEntriesWith(keyTag: keyTag, valueTag: valueTag) = options.shapeMapEncodingStrategy { 91 | let keyElementKey: String 92 | if let baseKey = key { 93 | keyElementKey = "\(baseKey)\(separatorString)\(index + 1)\(separatorString)\(keyTag)" 94 | keyToUse = "\(baseKey)\(separatorString)\(index + 1)\(separatorString)\(valueTag)" 95 | } else { 96 | keyElementKey = "\(index + 1)\(separatorString)\(keyTag)" 97 | keyToUse = "\(index + 1)\(separatorString)\(valueTag)" 98 | } 99 | 100 | // add an element for the key 101 | elements.append((keyElementKey, innerKey)) 102 | } else { 103 | if let baseKey = key { 104 | keyToUse = "\(baseKey)\(separatorString)\(innerKey)" 105 | } else { 106 | keyToUse = innerKey 107 | } 108 | } 109 | 110 | // get the serialized elements from this value 111 | try entry.element.value.getSerializedElements(keyToUse, isRoot: false, elements: &elements) 112 | } 113 | } 114 | } 115 | 116 | public func rawShapeForEncodingContainer(containerValue: ContainerValueType?) throws -> RawShape { 117 | // the encoding process must have placed a value in this container 118 | guard let containerValue = containerValue else { 119 | fatalError("Attempted to access uninitialized container.") 120 | } 121 | 122 | switch containerValue { 123 | case .singleValue(let value): 124 | // get the raw shape for container value 125 | return try value.asRawShape() 126 | case .unkeyedContainer(let values): 127 | let transformedArray = try values.map { try $0.asRawShape() } 128 | 129 | return .array(transformedArray) 130 | case .keyedContainer(let values): 131 | let transformedDictionary = try values.mapValues { try $0.asRawShape() } 132 | 133 | return .dictionary(transformedDictionary) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/AsyncResponseInvocationStrategy.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // AsyncResponseInvocationStrategy.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | A strategy protocol that manages how to invocate the asynchronous completion handler 22 | for response from the HTTPClient. 23 | */ 24 | public protocol AsyncResponseInvocationStrategy { 25 | associatedtype OutputType 26 | 27 | /** 28 | Function to handle the invocation of the response completion handler given 29 | the specified response. 30 | 31 | - Parameters: 32 | - response: The Result to invocate the completion handler with. 33 | - completion: The completion handler to invocate. 34 | */ 35 | func invokeResponse(response: OutputType, 36 | completion: @escaping (OutputType) -> ()) 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/BodyHTTPRequestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // BodyHTTPRequestInput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | HTTP Request Input that only has a body. 22 | */ 23 | public struct BodyHTTPRequestInput: HTTPRequestInputProtocol { 24 | public let queryEncodable: BodyType? 25 | public let pathEncodable: BodyType? 26 | public let bodyEncodable: BodyType? 27 | public let additionalHeadersEncodable: BodyType? 28 | public let pathPostfix: String? 29 | 30 | public init(encodable: BodyType) { 31 | self.queryEncodable = nil 32 | self.pathEncodable = nil 33 | self.bodyEncodable = encodable 34 | self.additionalHeadersEncodable = nil 35 | self.pathPostfix = nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/GlobalDispatchQueueAsyncResponseInvocationStrategy.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // GlobalDispatchQueueAsyncResponseInvocationStrategy.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | An AsyncResponseInvocationStrategy that will invocate the completion handler on 22 | DispatchQueue.global(). 23 | */ 24 | public struct GlobalDispatchQueueAsyncResponseInvocationStrategy: AsyncResponseInvocationStrategy { 25 | let queue = DispatchQueue.global() 26 | 27 | public init() { 28 | 29 | } 30 | 31 | public func invokeResponse(response: OutputType, 32 | completion: @escaping (OutputType) -> ()) { 33 | queue.async { 34 | completion(response) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientCoreInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientCoreInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import NIO 21 | 22 | public protocol OutputRequestRecord { 23 | var requestLatency: TimeInterval { get } 24 | } 25 | 26 | public protocol RetriableOutputRequestRecord { 27 | var outputRequests: [OutputRequestRecord] { get } 28 | } 29 | 30 | public protocol RetryAttemptRecord { 31 | var retryWait: TimeInterval { get } 32 | } 33 | 34 | /** 35 | Provide the ability to record the info about the outward requests for a particular invocation reporting instance. 36 | 37 | This is really a stop-gap measure until distributed tracing comes along and we can do this in a more standardised way. 38 | */ 39 | public protocol OutwardsRequestAggregator { 40 | 41 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord, onCompletion: @escaping () -> ()) 42 | 43 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord, onCompletion: @escaping () -> ()) 44 | 45 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord, onCompletion: @escaping () -> ()) 46 | 47 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 48 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord) 49 | 50 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 51 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord) 52 | 53 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 54 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord) 55 | } 56 | 57 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 58 | extension OutwardsRequestAggregator { 59 | 60 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord) async { 61 | return await withCheckedContinuation { cont in 62 | recordOutwardsRequest(outputRequestRecord: outputRequestRecord) { 63 | cont.resume(returning: ()) 64 | } 65 | } 66 | } 67 | 68 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord) async { 69 | return await withCheckedContinuation { cont in 70 | recordRetryAttempt(retryAttemptRecord: retryAttemptRecord) { 71 | cont.resume(returning: ()) 72 | } 73 | } 74 | } 75 | 76 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord) async { 77 | return await withCheckedContinuation { cont in 78 | recordRetriableOutwardsRequest(retriableOutwardsRequest: retriableOutwardsRequest) { 79 | cont.resume(returning: ()) 80 | } 81 | } 82 | } 83 | } 84 | #endif 85 | 86 | public extension OutwardsRequestAggregator { 87 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 88 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord, onCompletion: @escaping () -> ()) { 89 | recordOutwardsRequest(outputRequestRecord: outputRequestRecord) 90 | 91 | onCompletion() 92 | } 93 | 94 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 95 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord, onCompletion: @escaping () -> ()) { 96 | recordRetryAttempt(retryAttemptRecord: retryAttemptRecord) 97 | 98 | onCompletion() 99 | } 100 | 101 | @available(swift, deprecated: 2.0, message: "Not thread-safe") 102 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord, onCompletion: @escaping () -> ()) { 103 | recordRetriableOutwardsRequest(retriableOutwardsRequest: retriableOutwardsRequest) 104 | 105 | onCompletion() 106 | } 107 | } 108 | 109 | public protocol HTTPClientInvocationAttributes { 110 | /// The `Logging.Logger` to use for logging for this invocation. 111 | var logger: Logging.Logger { get } 112 | 113 | /// The internal Request Id associated with this invocation. 114 | var internalRequestId: String { get } 115 | 116 | var eventLoop: EventLoop? { get } 117 | 118 | var outwardsRequestAggregator: OutwardsRequestAggregator? { get } 119 | } 120 | 121 | /** 122 | A context related to reporting on the invocation of the HTTPClient. This represents the 123 | core requirements for invocation reporting. 124 | 125 | The HTTPClientCoreInvocationReporting protocol can exposed by higher level clients that manage the 126 | metrics requirements of the HTTPClientInvocationReporting protocol. 127 | */ 128 | public protocol HTTPClientCoreInvocationReporting: HTTPClientInvocationAttributes { 129 | associatedtype TraceContextType: InvocationTraceContext 130 | 131 | /// The trace context associated with this invocation. 132 | var traceContext: TraceContextType { get } 133 | } 134 | 135 | public extension HTTPClientCoreInvocationReporting { 136 | // The attribute is being added as a non-breaking change, so add a default implementation that replicates existing behaviour 137 | var eventLoop: EventLoop? { 138 | return nil 139 | } 140 | 141 | // The attribute is being added as a non-breaking change, so add a default implementation that replicates existing behaviour 142 | var outwardsRequestAggregator: OutwardsRequestAggregator? { 143 | return nil 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientInnerRetryInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientInnerRetryInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import Metrics 21 | import NIO 22 | 23 | /** 24 | When using retry wrappers, the `HTTPClient` itself shouldn't record any metrics. 25 | */ 26 | internal struct HTTPClientInnerRetryInvocationReporting: HTTPClientInvocationReporting { 27 | let internalRequestId: String 28 | let traceContext: TraceContextType 29 | let logger: Logging.Logger 30 | let eventLoop: EventLoop? 31 | let outwardsRequestAggregator: OutwardsRequestAggregator? 32 | let successCounter: Metrics.Counter? = nil 33 | let failure5XXCounter: Metrics.Counter? = nil 34 | let failure4XXCounter: Metrics.Counter? = nil 35 | let retryCountRecorder: Metrics.Recorder? = nil 36 | let latencyTimer: Metrics.Timer? = nil 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientInvocationContext.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientInvocationContext.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import Metrics 21 | 22 | private let outgoingRequestId = "outgoingRequestId" 23 | private let outgoingEndpointKey = "outgoingEndpoint" 24 | private let outgoingOperationKey = "outgoingOperation" 25 | 26 | /** 27 | A context related to the invocation of the HTTPClient. 28 | */ 29 | public struct HTTPClientInvocationContext { 30 | public let reporting: InvocationReportingType 31 | public let handlerDelegate: HandlerDelegateType 32 | 33 | public init(reporting: InvocationReportingType, 34 | handlerDelegate: HandlerDelegateType) { 35 | self.reporting = reporting 36 | self.handlerDelegate = handlerDelegate 37 | } 38 | } 39 | 40 | extension HTTPClientInvocationContext { 41 | func withOutgoingDecoratedLogger(endpoint endpointOptional: URL?, outgoingOperation outgoingOperationOptional: String?) 42 | -> HTTPClientInvocationContext, HandlerDelegateType> { 43 | var outwardInvocationLogger = reporting.logger 44 | outwardInvocationLogger[metadataKey: outgoingRequestId] = "\(UUID().uuidString)" 45 | 46 | if let endpoint = endpointOptional { 47 | outwardInvocationLogger[metadataKey: outgoingEndpointKey] = "\(endpoint.absoluteString)" 48 | } 49 | 50 | if let outgoingOperation = outgoingOperationOptional { 51 | outwardInvocationLogger[metadataKey: outgoingOperationKey] = "\(outgoingOperation)" 52 | } 53 | 54 | let wrappingInvocationReporting = StandardHTTPClientInvocationReporting( 55 | internalRequestId: reporting.internalRequestId, 56 | traceContext: reporting.traceContext, 57 | logger: outwardInvocationLogger, 58 | eventLoop: reporting.eventLoop, 59 | outwardsRequestAggregator: reporting.outwardsRequestAggregator, 60 | successCounter: reporting.successCounter, 61 | failure5XXCounter: reporting.failure5XXCounter, 62 | failure4XXCounter: reporting.failure4XXCounter, 63 | retryCountRecorder: reporting.retryCountRecorder, 64 | latencyTimer: reporting.latencyTimer) 65 | return HTTPClientInvocationContext, 66 | HandlerDelegateType>(reporting: wrappingInvocationReporting, 67 | handlerDelegate: handlerDelegate) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientInvocationDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientInvocationDelegate.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import AsyncHTTPClient 20 | import NIOHTTP1 21 | import Logging 22 | 23 | public struct HTTPRequestParameters { 24 | /// The content type of the payload being sent. 25 | public let contentType: String 26 | /// The endpoint url to request a response from. 27 | public let endpointUrl: URL 28 | /// The path to request a response from. 29 | public let endpointPath: String 30 | /// The http method to use for the request. 31 | public let httpMethod: HTTPMethod 32 | /// The request body data to use. 33 | public let bodyData: Data 34 | /// Any additional headers to add 35 | public let additionalHeaders: [(String, String)] 36 | 37 | /** 38 | Initializer. 39 | 40 | - Parameters: 41 | - contentType: The endpoint url to request a response from. 42 | - endpointUrl: The endpoint url to request a response from. 43 | - endpointPath: The path to request a response from. 44 | - httpMethod: The http method to use for the request. 45 | - bodyData: The request body data to use. 46 | - additionalHeaders: Any additional headers to add 47 | */ 48 | public init(contentType: String, 49 | endpointUrl: URL, 50 | endpointPath: String, 51 | httpMethod: HTTPMethod, 52 | bodyData: Data, 53 | additionalHeaders: [(String, String)]) { 54 | self.contentType = contentType 55 | self.endpointUrl = endpointUrl 56 | self.endpointPath = endpointPath 57 | self.httpMethod = httpMethod 58 | self.bodyData = bodyData 59 | self.additionalHeaders = additionalHeaders 60 | } 61 | } 62 | 63 | public protocol HTTPClientInvocationDelegate { 64 | var specifyContentHeadersForZeroLengthBody: Bool { get } 65 | 66 | func addClientSpecificHeaders( 67 | parameters: HTTPRequestParameters, 68 | invocationReporting: InvocationReportingType) -> [(String, String)] 69 | 70 | func handleErrorResponses( 71 | response: HTTPClient.Response, responseBodyData: Data?, 72 | invocationReporting: InvocationReportingType) -> HTTPClientError? 73 | } 74 | 75 | public struct DefaultHTTPClientInvocationDelegate: HTTPClientInvocationDelegate { 76 | public let specifyContentHeadersForZeroLengthBody: Bool 77 | 78 | public init(specifyContentHeadersForZeroLengthBody: Bool = true) { 79 | self.specifyContentHeadersForZeroLengthBody = specifyContentHeadersForZeroLengthBody 80 | } 81 | 82 | public func addClientSpecificHeaders( 83 | parameters: HTTPRequestParameters, 84 | invocationReporting: InvocationReportingType) -> [(String, String)] { 85 | return [] 86 | } 87 | 88 | public func handleErrorResponses( 89 | response: HTTPClient.Response, responseBodyData: Data?, 90 | invocationReporting: InvocationReportingType) -> HTTPClientError? { 91 | return nil 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import Metrics 21 | 22 | private let timeIntervalToMilliseconds: Double = 1000 23 | 24 | /** 25 | A context related to the metrics on the invocation of a SmokeAWS operation. 26 | */ 27 | public protocol HTTPClientInvocationMetrics { 28 | 29 | /// The `Metrics.Counter` to record the success of this invocation. 30 | var successCounter: Metrics.Counter? { get } 31 | 32 | /// The `Metrics.Counter` to record the failure of this invocation. 33 | var failure5XXCounter: Metrics.Counter? { get } 34 | 35 | /// The `Metrics.Counter` to record the failure of this invocation. 36 | var failure4XXCounter: Metrics.Counter? { get } 37 | 38 | /// The `Metrics.Recorder` to record the number of retries that occurred as part of this invocation. 39 | var retryCountRecorder: Metrics.Recorder? { get } 40 | 41 | /// The `Metrics.Recorder` to record the duration of this invocation. 42 | var latencyTimer: Metrics.Timer? { get } 43 | } 44 | 45 | /** 46 | A context related to reporting on the invocation of the HTTPClient. This interface extends the 47 | `HTTPClientCoreInvocationReporting` protocol by adding metrics defined by the `HTTPClientInvocationMetrics` protocol. 48 | */ 49 | public typealias HTTPClientInvocationReporting = HTTPClientInvocationMetrics & HTTPClientCoreInvocationReporting 50 | 51 | internal extension TimeInterval { 52 | var milliseconds: Int { 53 | return Int(self * timeIntervalToMilliseconds) 54 | } 55 | } 56 | 57 | internal extension Int { 58 | var millisecondsToTimeInterval: TimeInterval { 59 | return TimeInterval(self) / timeIntervalToMilliseconds 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientReportingConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientReportingConfiguration.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | public struct HTTPClientReportingConfiguration { 21 | 22 | // TODO: Remove non-inclusive language 23 | public enum MatchingOperations { 24 | case all 25 | case whitelist(Set) 26 | case allowlist(Set) 27 | case blacklist(Set) 28 | case blocklist(Set) 29 | case none 30 | } 31 | 32 | private let successCounterMatchingOperations: MatchingOperations 33 | private let failure5XXCounterMatchingOperations: MatchingOperations 34 | private let failure4XXCounterMatchingOperations: MatchingOperations 35 | private let retryCountRecorderMatchingOperations: MatchingOperations 36 | private let latencyTimerMatchingOperations: MatchingOperations 37 | 38 | public init(successCounterMatchingOperations: MatchingOperations, 39 | failure5XXCounterMatchingOperations: MatchingOperations, 40 | failure4XXCounterMatchingOperations: MatchingOperations, 41 | retryCountRecorderMatchingOperations: MatchingOperations, 42 | latencyTimerMatchingOperations: MatchingOperations) { 43 | self.successCounterMatchingOperations = successCounterMatchingOperations 44 | self.failure5XXCounterMatchingOperations = failure5XXCounterMatchingOperations 45 | self.failure4XXCounterMatchingOperations = failure4XXCounterMatchingOperations 46 | self.retryCountRecorderMatchingOperations = retryCountRecorderMatchingOperations 47 | self.latencyTimerMatchingOperations = latencyTimerMatchingOperations 48 | } 49 | 50 | public init(matchingOperations: MatchingOperations = .all) { 51 | self.successCounterMatchingOperations = matchingOperations 52 | self.failure5XXCounterMatchingOperations = matchingOperations 53 | self.failure4XXCounterMatchingOperations = matchingOperations 54 | self.retryCountRecorderMatchingOperations = matchingOperations 55 | self.latencyTimerMatchingOperations = matchingOperations 56 | } 57 | 58 | public static var none: Self { 59 | return .init(matchingOperations: .none) 60 | } 61 | 62 | public static var all: Self { 63 | return .init(matchingOperations: .all) 64 | } 65 | 66 | public static func onlyForOperations(_ operations: Set) -> Self { 67 | return .init(matchingOperations: .whitelist(operations)) 68 | } 69 | 70 | public static func exceptForOperations(_ operations: Set) -> Self { 71 | return .init(matchingOperations: .blacklist(operations)) 72 | } 73 | 74 | public func reportSuccessForOperation(_ operation: OperationIdentifer) -> Bool { 75 | return isMatchingOperation(operation, matchingOperations: successCounterMatchingOperations) 76 | } 77 | 78 | public func reportFailure5XXForOperation(_ operation: OperationIdentifer) -> Bool { 79 | return isMatchingOperation(operation, matchingOperations: failure5XXCounterMatchingOperations) 80 | } 81 | 82 | public func reportFailure4XXForOperation(_ operation: OperationIdentifer) -> Bool { 83 | return isMatchingOperation(operation, matchingOperations: failure4XXCounterMatchingOperations) 84 | } 85 | 86 | public func reportRetryCountForOperation(_ operation: OperationIdentifer) -> Bool { 87 | return isMatchingOperation(operation, matchingOperations: retryCountRecorderMatchingOperations) 88 | } 89 | 90 | public func reportLatencyForOperation(_ operation: OperationIdentifer) -> Bool { 91 | return isMatchingOperation(operation, matchingOperations: latencyTimerMatchingOperations) 92 | } 93 | 94 | private func isMatchingOperation(_ operation: OperationIdentifer, matchingOperations: MatchingOperations) -> Bool { 95 | switch matchingOperations { 96 | case .all: 97 | return true 98 | case .whitelist(let allowlist), .allowlist(let allowlist): 99 | return allowlist.contains(operation) 100 | case .blacklist(let blocklist), .blocklist(let blocklist): 101 | return !blocklist.contains(operation) 102 | case .none: 103 | return false 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPClientRetryConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientRetryConfiguration.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /// Type alias for a retry interval. 21 | public typealias RetryInterval = UInt32 22 | 23 | /** 24 | Retry configuration for the requests made by a HTTPClient. 25 | */ 26 | public struct HTTPClientRetryConfiguration { 27 | // Number of retries to be attempted 28 | public let numRetries: Int 29 | // First interval of retry in millis 30 | public let baseRetryInterval: RetryInterval 31 | // Max amount of cumulative time to attempt retries in millis 32 | public let maxRetryInterval: RetryInterval 33 | // Exponential backoff for each retry 34 | public let exponentialBackoff: Double 35 | // Ramdomized backoff 36 | public let jitter: Bool 37 | 38 | /** 39 | Initializer. 40 | 41 | - Parameters: 42 | - numRetries: number of retries to be attempted. 43 | - baseRetryInterval: first interval of retry in millis. 44 | - maxRetryInterval: max amount of cumulative time to attempt retries in millis 45 | - exponentialBackoff: exponential backoff for each retry 46 | - jitter: ramdomized backoff 47 | */ 48 | public init(numRetries: Int, baseRetryInterval: RetryInterval, maxRetryInterval: RetryInterval, 49 | exponentialBackoff: Double, jitter: Bool = true) { 50 | self.numRetries = numRetries 51 | self.baseRetryInterval = baseRetryInterval 52 | self.maxRetryInterval = maxRetryInterval 53 | self.exponentialBackoff = exponentialBackoff 54 | self.jitter = jitter 55 | } 56 | 57 | public func getRetryInterval(retriesRemaining: Int) -> RetryInterval { 58 | let msInterval = RetryInterval(Double(baseRetryInterval) * pow(exponentialBackoff, Double(numRetries - retriesRemaining))) 59 | let boundedMsInterval = min(maxRetryInterval, msInterval) 60 | 61 | if jitter { 62 | if boundedMsInterval > 0 { 63 | return RetryInterval.random(in: 0 ..< boundedMsInterval) 64 | } else { 65 | return 0 66 | } 67 | } 68 | 69 | return boundedMsInterval 70 | } 71 | 72 | /// Default try configuration with 5 retries starting at 500 ms interval. 73 | public static var `default` = HTTPClientRetryConfiguration(numRetries: 5, baseRetryInterval: 500, 74 | maxRetryInterval: 10000, exponentialBackoff: 2) 75 | 76 | /// Retry Configuration with no retries. 77 | public static var noRetries = HTTPClientRetryConfiguration(numRetries: 0, baseRetryInterval: 0, 78 | maxRetryInterval: 0, exponentialBackoff: 0) 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPError.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPError.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import NIOHTTP1 20 | 21 | /** 22 | Errors that can be thrown as part of the SmokeHTTPClient library. 23 | 24 | For more nuanced errors specific to a use-case, provide an extension off this enum. 25 | */ 26 | public enum HTTPError: Error { 27 | // 3xx 28 | case movedPermanently(location: String) 29 | 30 | // 4xx 31 | case badRequest(String) 32 | case badResponse(String) 33 | case unauthorized(String) 34 | 35 | // 5xx 36 | case connectionError(String) 37 | case connectionFailure(cause: Swift.Error) 38 | case internalServerError(String) 39 | case invalidRequest(String) 40 | 41 | // Other 42 | case validationError(reason: String) 43 | case deserializationError(cause: Swift.Error) 44 | case unknownError(String) 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPOperationsClient +executeSyncWithOutput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPOperationsClient+executeSyncWithOutput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import NIO 20 | import NIOHTTP1 21 | import NIOSSL 22 | import NIOTLS 23 | import Logging 24 | 25 | public extension HTTPOperationsClient { 26 | /** 27 | Submits a request that will return a response body to this client synchronously. 28 | 29 | - Parameters: 30 | - endpointPath: The endpoint path for this request. 31 | - httpMethod: The http method to use for this request. 32 | - input: the input body data to send with this request. 33 | - invocationContext: context to use for this invocation. 34 | - Returns: the response body. 35 | - Throws: If an error occurred during the request. 36 | */ 37 | func executeSyncWithOutput( 38 | endpointOverride: URL? = nil, 39 | endpointPath: String, 40 | httpMethod: HTTPMethod, 41 | operation: String? = nil, 42 | input: InputType, 43 | invocationContext: HTTPClientInvocationContext) throws -> OutputType 44 | where InputType: HTTPRequestInputProtocol, 45 | OutputType: HTTPResponseOutputProtocol { 46 | let requestComponents = try clientDelegate.encodeInputAndQueryString( 47 | input: input, 48 | httpPath: endpointPath, 49 | invocationReporting: invocationContext.reporting) 50 | let endpoint = getEndpoint(endpointOverride: endpointOverride, path: requestComponents.pathWithQuery) 51 | let wrappingInvocationContext = invocationContext.withOutgoingDecoratedLogger(endpoint: endpoint, outgoingOperation: operation) 52 | 53 | return try executeSyncWithOutputWithWrappedInvocationContext( 54 | endpointOverride: endpointOverride, 55 | requestComponents: requestComponents, 56 | httpMethod: httpMethod, 57 | invocationContext: wrappingInvocationContext) 58 | } 59 | 60 | /** 61 | Submits a request that will return a response body to this client synchronously. To be called when the `InvocationContext` has already been wrapped with an outgoingRequestId aware Logger. 62 | 63 | - Parameters: 64 | - endpointPath: The endpoint path for this request. 65 | - httpMethod: The http method to use for this request. 66 | - input: the input body data to send with this request. 67 | - invocationContext: context to use for this invocation. 68 | - Returns: the response body. 69 | - Throws: If an error occurred during the request. 70 | */ 71 | internal func executeSyncWithOutputWithWrappedInvocationContext( 73 | endpointOverride: URL? = nil, 74 | requestComponents: HTTPRequestComponents, 75 | httpMethod: HTTPMethod, 76 | invocationContext: HTTPClientInvocationContext) throws -> OutputType 77 | where OutputType: HTTPResponseOutputProtocol { 78 | 79 | var responseResult: Result? 80 | let completedSemaphore = DispatchSemaphore(value: 0) 81 | 82 | let completion: (Result) -> () = { result in 83 | responseResult = result 84 | completedSemaphore.signal() 85 | } 86 | 87 | _ = try executeAsyncWithOutputWithWrappedInvocationContext( 88 | endpointOverride: endpointOverride, 89 | requestComponents: requestComponents, 90 | httpMethod: httpMethod, 91 | completion: completion, 92 | // the completion handler can be safely executed on a SwiftNIO thread 93 | asyncResponseInvocationStrategy: SameThreadAsyncResponseInvocationStrategy>(), 94 | invocationContext: invocationContext) 95 | 96 | let logger = invocationContext.reporting.logger 97 | logger.trace("Waiting for response from \(endpointOverride?.host ?? endpointHostName) ...") 98 | completedSemaphore.wait() 99 | 100 | guard let result = responseResult else { 101 | throw HTTPError.connectionError("Http request was closed without returning a response.") 102 | } 103 | 104 | logger.trace("Got response from \(endpointOverride?.host ?? endpointHostName) - response received: \(result)") 105 | 106 | switch result { 107 | case .failure(let error): 108 | throw error 109 | case .success(let response): 110 | return response 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPOperationsClient +executeSyncWithoutOutput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPOperationsClient+executeSyncWithoutOutput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import NIO 20 | import NIOHTTP1 21 | import NIOSSL 22 | import NIOTLS 23 | import Logging 24 | 25 | public extension HTTPOperationsClient { 26 | 27 | /** 28 | Submits a request that will not return a response body to this client synchronously. 29 | 30 | - Parameters: 31 | - endpointPath: The endpoint path for this request. 32 | - httpMethod: The http method to use for this request. 33 | - input: the input body data to send with this request. 34 | - invocationContext: context to use for this invocation. 35 | - Throws: If an error occurred during the request. 36 | */ 37 | func executeSyncWithoutOutput( 38 | endpointOverride: URL? = nil, 39 | endpointPath: String, 40 | httpMethod: HTTPMethod, 41 | operation: String? = nil, 42 | input: InputType, 43 | invocationContext: HTTPClientInvocationContext) throws 44 | where InputType: HTTPRequestInputProtocol { 45 | let requestComponents = try clientDelegate.encodeInputAndQueryString( 46 | input: input, 47 | httpPath: endpointPath, 48 | invocationReporting: invocationContext.reporting) 49 | let endpoint = getEndpoint(endpointOverride: endpointOverride, path: requestComponents.pathWithQuery) 50 | let wrappingInvocationContext = invocationContext.withOutgoingDecoratedLogger(endpoint: endpoint, outgoingOperation: operation) 51 | 52 | try executeSyncWithoutOutputWithWrappedInvocationContext( 53 | endpointOverride: endpointOverride, 54 | requestComponents: requestComponents, 55 | httpMethod: httpMethod, 56 | invocationContext: wrappingInvocationContext) 57 | } 58 | 59 | /** 60 | Submits a request that will not return a response body to this client synchronously. To be called when the `InvocationContext` has already been wrapped with an outgoingRequestId aware Logger. 61 | 62 | - Parameters: 63 | - endpointPath: The endpoint path for this request. 64 | - httpMethod: The http method to use for this request. 65 | - input: the input body data to send with this request. 66 | - invocationContext: context to use for this invocation. 67 | - Throws: If an error occurred during the request. 68 | */ 69 | internal func executeSyncWithoutOutputWithWrappedInvocationContext( 71 | endpointOverride: URL? = nil, 72 | requestComponents: HTTPRequestComponents, 73 | httpMethod: HTTPMethod, 74 | invocationContext: HTTPClientInvocationContext) throws { 75 | var responseError: HTTPClientError? 76 | let completedSemaphore = DispatchSemaphore(value: 0) 77 | 78 | let completion: (HTTPClientError?) -> () = { error in 79 | if let error = error { 80 | responseError = HTTPClientError(responseCode: 500, cause: error) 81 | } 82 | completedSemaphore.signal() 83 | } 84 | 85 | _ = try executeAsyncWithoutOutputWithWrappedInvocationContext( 86 | endpointOverride: endpointOverride, 87 | requestComponents: requestComponents, 88 | httpMethod: httpMethod, 89 | completion: completion, 90 | // the completion handler can be safely executed on a SwiftNIO thread 91 | asyncResponseInvocationStrategy: SameThreadAsyncResponseInvocationStrategy(), 92 | invocationContext: invocationContext) 93 | 94 | completedSemaphore.wait() 95 | 96 | if let error = responseError { 97 | throw error 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPOperationsClient+executeWithoutOutput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPOperationsClient+executeWithoutOutput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 19 | 20 | import Foundation 21 | import NIO 22 | import NIOHTTP1 23 | import Metrics 24 | 25 | public extension HTTPOperationsClient { 26 | 27 | /** 28 | Submits a request that will not return a response body to this client asynchronously. 29 | 30 | - Parameters: 31 | - endpointPath: The endpoint path for this request. 32 | - httpMethod: The http method to use for this request. 33 | - clientName: Optionally the name of the client to use for reporting. 34 | - operation: Optionally the name of the operation to use for reporting. 35 | - input: the input body data to send with this request. 36 | - completion: Completion handler called with an error if one occurs or nil otherwise. 37 | - asyncResponseInvocationStrategy: The invocation strategy for the response from this request. 38 | - invocationContext: context to use for this invocation. 39 | - Throws: If an error occurred during the request. 40 | */ 41 | func executeWithoutOutput( 43 | endpointOverride: URL? = nil, 44 | endpointPath: String, 45 | httpMethod: HTTPMethod, 46 | clientName: String? = nil, 47 | operation: String? = nil, 48 | input: InputType, 49 | invocationContext: HTTPClientInvocationContext) async throws 50 | where InputType: HTTPRequestInputProtocol { 51 | let requestComponents = try clientDelegate.encodeInputAndQueryString( 52 | input: input, 53 | httpPath: endpointPath, 54 | invocationReporting: invocationContext.reporting) 55 | let endpoint = getEndpoint(endpointOverride: endpointOverride, path: requestComponents.pathWithQuery) 56 | let wrappingInvocationContext = invocationContext.withOutgoingDecoratedLogger(endpoint: endpoint, outgoingOperation: operation) 57 | 58 | let clientNameToUse = clientName ?? "UnnamedClient" 59 | let operationToUse = operation ?? "UnnamedOperation" 60 | let spanName = "\(clientNameToUse).\(operationToUse)" 61 | 62 | return try await withSpanIfEnabled(spanName) { _ in 63 | return try await executeWithoutOutputWithWrappedInvocationContext( 64 | endpointOverride: endpointOverride, 65 | requestComponents: requestComponents, 66 | httpMethod: httpMethod, 67 | invocationContext: wrappingInvocationContext) 68 | } 69 | } 70 | 71 | /** 72 | Submits a request that will not return a response body to this client asynchronously. 73 | To be called when the `InvocationContext` has already been wrapped with an outgoingRequestId aware Logger. 74 | 75 | - Parameters: 76 | - endpointPath: The endpoint path for this request. 77 | - httpMethod: The http method to use for this request. 78 | - input: the input body data to send with this request. 79 | - invocationContext: context to use for this invocation. 80 | - Returns: A future that will produce a Void result or failure. 81 | */ 82 | internal func executeWithoutOutputWithWrappedInvocationContext< 83 | InvocationReportingType: HTTPClientInvocationReporting, 84 | HandlerDelegateType: HTTPClientInvocationDelegate>( 85 | endpointOverride: URL? = nil, 86 | requestComponents: HTTPRequestComponents, 87 | httpMethod: HTTPMethod, 88 | invocationContext: HTTPClientInvocationContext) async throws { 89 | 90 | let durationMetricDetails: (Date, Metrics.Timer?, OutwardsRequestAggregator?)? 91 | 92 | if invocationContext.reporting.outwardsRequestAggregator != nil || 93 | invocationContext.reporting.latencyTimer != nil { 94 | durationMetricDetails = (Date(), invocationContext.reporting.latencyTimer, invocationContext.reporting.outwardsRequestAggregator) 95 | } else { 96 | durationMetricDetails = nil 97 | } 98 | 99 | // submit the asynchronous request 100 | do { 101 | _ = try await execute(endpointOverride: endpointOverride, 102 | requestComponents: requestComponents, 103 | httpMethod: httpMethod, 104 | invocationContext: invocationContext) 105 | } catch { 106 | if let typedError = error as? HTTPClientError { 107 | // report failure metric 108 | switch typedError.category { 109 | case .clientError, .clientRetryableError: 110 | invocationContext.reporting.failure4XXCounter?.increment() 111 | case .serverError: 112 | invocationContext.reporting.failure5XXCounter?.increment() 113 | } 114 | } 115 | 116 | // rethrow the error 117 | throw error 118 | } 119 | 120 | invocationContext.reporting.successCounter?.increment() 121 | 122 | if let durationMetricDetails = durationMetricDetails { 123 | let timeInterval = Date().timeIntervalSince(durationMetricDetails.0) 124 | 125 | if let latencyTimer = durationMetricDetails.1 { 126 | latencyTimer.recordMilliseconds(timeInterval.milliseconds) 127 | } 128 | 129 | if let outwardsRequestAggregator = durationMetricDetails.2 { 130 | await outwardsRequestAggregator.recordOutwardsRequest( 131 | outputRequestRecord: StandardOutputRequestRecord(requestLatency: timeInterval)) 132 | } 133 | } 134 | } 135 | } 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPRequestComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPRequestComponents.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /// The parsed components that specify a request. 21 | public struct HTTPRequestComponents { 22 | /// the path for the request including the query. 23 | public let pathWithQuery: String 24 | /// any request specific headers that needs to be added. 25 | public let additionalHeaders: [(String, String)] 26 | /// The body data of the request. 27 | public let body: Data 28 | 29 | public init(pathWithQuery: String, additionalHeaders: [(String, String)], body: Data) { 30 | self.pathWithQuery = pathWithQuery 31 | self.additionalHeaders = additionalHeaders 32 | self.body = body 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPRequestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPRequestInput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | A HTTP Request that includes a query, path, body and additional headers 22 | */ 23 | public struct HTTPRequestInput: HTTPRequestInputProtocol { 27 | public let queryEncodable: QueryType? 28 | public let pathEncodable: PathType? 29 | public let bodyEncodable: BodyType? 30 | public let additionalHeadersEncodable: AdditionalHeadersType? 31 | public let pathPostfix: String? 32 | 33 | public init(queryEncodable: QueryType? = nil, 34 | pathEncodable: PathType? = nil, 35 | bodyEncodable: BodyType? = nil, 36 | additionalHeaders: AdditionalHeadersType? = nil, 37 | pathPostfix: String? = nil) { 38 | self.queryEncodable = queryEncodable 39 | self.pathEncodable = pathEncodable 40 | self.bodyEncodable = bodyEncodable 41 | self.additionalHeadersEncodable = additionalHeaders 42 | self.pathPostfix = pathPostfix 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPRequestInputProtocol.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPRequestInputProtocol.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | A protocol that represents input to a HTTP request. 22 | */ 23 | public protocol HTTPRequestInputProtocol { 24 | associatedtype QueryType: Encodable 25 | associatedtype PathType: Encodable 26 | associatedtype BodyType: Encodable 27 | associatedtype AdditionalHeadersType: Encodable 28 | 29 | /// An instance of a type that is encodable to a query string 30 | var queryEncodable: QueryType? { get } 31 | /// An instance of a type that is encodable to a path 32 | var pathEncodable: PathType? { get } 33 | /// An instance of a type that is encodable to a body 34 | var bodyEncodable: BodyType? { get } 35 | /// An instance of a type that is encodable to additional headers 36 | var additionalHeadersEncodable: AdditionalHeadersType? { get } 37 | /// If there is a specific path postfix 38 | var pathPostfix: String? { get } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPResponseComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPResponseComponents.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /// The parsed components that specify a request. 21 | public struct HTTPResponseComponents { 22 | /// any response headers. 23 | public let headers: [(String, String)] 24 | /// The body data of the response. 25 | public let body: Data? 26 | 27 | public init(headers: [(String, String)], body: Data?) { 28 | self.headers = headers 29 | self.body = body 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HTTPResponseOutputProtocol.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPResponseInputProtocol.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | A protocol that represents output from a HTTP response. 22 | */ 23 | public protocol HTTPResponseOutputProtocol { 24 | associatedtype BodyType: Decodable 25 | associatedtype HeadersType: Decodable 26 | 27 | /** 28 | Composes an instance from its constituent Decodable parts. 29 | May return one of its constituent parts if of a compatible type. 30 | 31 | - Parameters: 32 | - bodyDecodableProvider: provider for the decoded body for this instance. 33 | - headersDecodableProvider: provider for the decoded headers for this instance. 34 | */ 35 | static func compose(bodyDecodableProvider: () throws -> BodyType, 36 | headersDecodableProvider: () throws -> HeadersType) throws -> Self 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/HttpClientError.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPClientError.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | public struct HTTPClientError: Error { 19 | public let responseCode: Int 20 | public let cause: Swift.Error 21 | 22 | public enum Category { 23 | case clientError 24 | case clientRetryableError 25 | case serverError 26 | } 27 | 28 | public init(responseCode: Int, cause: Swift.Error) { 29 | self.responseCode = responseCode 30 | self.cause = cause 31 | } 32 | 33 | public var category: Category { 34 | switch responseCode { 35 | case 400...499: 36 | if(responseCode == 429) { 37 | return .clientRetryableError 38 | } else { 39 | return .clientError 40 | } 41 | default: 42 | return .serverError 43 | } 44 | } 45 | 46 | public func isRetriable() -> Bool { 47 | return self.isRetriableAccordingToCategory 48 | } 49 | 50 | public var isRetriableAccordingToCategory: Bool { 51 | if case self.category = Category.clientError { 52 | return false 53 | } else { 54 | return true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/InvocationTraceContext.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InvocationTraceContext.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import NIOHTTP1 20 | import Logging 21 | import AsyncHTTPClient 22 | 23 | public protocol InvocationTraceContext { 24 | associatedtype OutwardsRequestContext 25 | 26 | func handleOutwardsRequestStart( 27 | method: HTTPMethod, uri: String, 28 | logger: Logging.Logger, internalRequestId: String, 29 | headers: inout HTTPHeaders, bodyData: Data) -> OutwardsRequestContext 30 | 31 | func handleOutwardsRequestSuccess( 32 | outwardsRequestContext: OutwardsRequestContext?, logger: Logging.Logger, internalRequestId: String, 33 | response: HTTPClient.Response, bodyData: Data?) 34 | 35 | func handleOutwardsRequestFailure( 36 | outwardsRequestContext: OutwardsRequestContext?, logger: Logging.Logger, internalRequestId: String, response: HTTPClient.Response?, bodyData: Data?, error: Swift.Error) 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/MockCoreInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // MockCoreInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | import Foundation 18 | import Logging 19 | 20 | public typealias MockCoreInvocationReporting = 21 | StandardHTTPClientCoreInvocationReporting 22 | 23 | /** 24 | A type conforming to the `HTTPClientCoreInvocationReporting` protocol, predominantly for testing. 25 | */ 26 | public extension MockCoreInvocationReporting { 27 | init( 28 | logger: Logger = Logger(label: "com.amazon.SmokeHTTPClient.MockCoreInvocationReporting"), 29 | internalRequestId: String = "internalRequestId") { 30 | self.init(logger: logger, 31 | internalRequestId: internalRequestId, 32 | traceContext: MockInvocationTraceContext()) 33 | } 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/MockHTTPClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // MockHTTPClient.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | public class MockHTTPClient { 21 | init() { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/MockHTTPInvocationClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // MockHTTPInvocationClient.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import NIOHTTP1 20 | 21 | public protocol EmptyInitializable { 22 | init() 23 | } 24 | 25 | public enum MockHTTPInvocationClientErrors: Error { 26 | case cannotInitializeEmptyOutput(outputType: String) 27 | case mismatchingOutputTypes(outputType: String, overrideOutputType: String) 28 | } 29 | 30 | public struct MockHTTPInvocationClient: HTTPInvocationClientProtocol { 31 | public typealias ExecuteWithoutOutputFunctionType = ( 32 | _ endpoint: URL?, 33 | _ endpointPath: String, 34 | _ httpMethod: HTTPMethod, 35 | _ operation: String?, 36 | _ input: OverrideInputType) async throws -> Void 37 | 38 | public typealias ExecuteWithOutputFunctionType = ( 39 | _ endpoint: URL?, 40 | _ endpointPath: String, 41 | _ httpMethod: HTTPMethod, 42 | _ operation: String?, 43 | _ input: OverrideInputType) async throws -> OverrideOutputType 44 | 45 | let executeWithoutOutputOverride: ExecuteWithoutOutputFunctionType? 46 | let executeWithOutputOverride: ExecuteWithOutputFunctionType? 47 | 48 | public init( 49 | executeWithoutOutputOverride: ExecuteWithoutOutputFunctionType? = nil, 50 | executeWithOutputOverride: ExecuteWithOutputFunctionType? = nil) { 51 | self.executeWithoutOutputOverride = executeWithoutOutputOverride 52 | self.executeWithOutputOverride = executeWithOutputOverride 53 | } 54 | 55 | public func shutdown() async throws {} 56 | 57 | public func executeRetriableWithoutOutput( 58 | endpoint: URL?, 59 | endpointPath: String, 60 | httpMethod: HTTPMethod, 61 | operation: String?, 62 | input: InputType) async throws { 63 | if let executeWithoutOutputOverride = executeWithoutOutputOverride, 64 | let convertedInput = input as? OverrideInputType { 65 | try await executeWithoutOutputOverride(endpoint, endpointPath, httpMethod, operation, convertedInput) 66 | } 67 | } 68 | 69 | public func executeRetriableWithOutput( 70 | endpoint: URL?, 71 | endpointPath: String, 72 | httpMethod: HTTPMethod, 73 | operation: String?, 74 | input: InputType) async throws -> OutputType { 75 | if let executeWithOutputOverride = executeWithOutputOverride, 76 | let convertedInput = input as? OverrideInputType { 77 | let output = try await executeWithOutputOverride(endpoint, endpointPath, httpMethod, operation, convertedInput) as? OutputType 78 | guard let output = output else { 79 | throw MockHTTPInvocationClientErrors.mismatchingOutputTypes(outputType: String(describing: OutputType.self), overrideOutputType: String(describing: OverrideOutputType.self)) 80 | } 81 | 82 | return output 83 | } 84 | 85 | return try getDefaultOutput() 86 | } 87 | 88 | public func executeWithoutOutput( 89 | endpoint: URL?, 90 | endpointPath: String, 91 | httpMethod: HTTPMethod, 92 | operation: String?, 93 | input: InputType) async throws { 94 | if let executeWithoutOutputOverride = executeWithoutOutputOverride, 95 | let convertedInput = input as? OverrideInputType { 96 | try await executeWithoutOutputOverride(endpoint, endpointPath, httpMethod, operation, convertedInput) 97 | } 98 | } 99 | 100 | public func executeWithOutput( 101 | endpoint: URL?, 102 | endpointPath: String, 103 | httpMethod: HTTPMethod, 104 | operation: String?, 105 | input: InputType) async throws -> OutputType { 106 | if let executeWithOutputOverride = executeWithOutputOverride, 107 | let convertedInput = input as? OverrideInputType { 108 | let output = try await executeWithOutputOverride(endpoint, endpointPath, httpMethod, operation, convertedInput) as? OutputType 109 | guard let output = output else { 110 | throw MockHTTPInvocationClientErrors.mismatchingOutputTypes(outputType: String(describing: OutputType.self), overrideOutputType: String(describing: OverrideOutputType.self)) 111 | } 112 | 113 | return output 114 | } 115 | 116 | return try getDefaultOutput() 117 | } 118 | 119 | private func getDefaultOutput() throws -> OutputType { 120 | guard let initializableType = OutputType.self as? EmptyInitializable.Type, 121 | let initializedInstance = initializableType.init() as? OutputType else { 122 | throw MockHTTPInvocationClientErrors.cannotInitializeEmptyOutput(outputType: String(describing: OutputType.self)) 123 | } 124 | 125 | return initializedInstance 126 | } 127 | } 128 | 129 | public struct MockNoHTTPOutput: HTTPResponseOutputProtocol, EmptyInitializable { 130 | public typealias BodyType = String 131 | public typealias HeadersType = String 132 | 133 | public static func compose(bodyDecodableProvider: () throws -> BodyType, headersDecodableProvider: () throws -> HeadersType) throws -> MockNoHTTPOutput { 134 | return Self() 135 | } 136 | 137 | public init() {} 138 | } 139 | 140 | public typealias DefaultMockHTTPInvocationClient = MockHTTPInvocationClient 141 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/MockInvocationTraceContext.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // MockInvocationTraceContext.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import NIOHTTP1 21 | import AsyncHTTPClient 22 | 23 | public struct MockInvocationTraceContext: InvocationTraceContext { 24 | public typealias OutwardsRequestContext = String 25 | 26 | public let outwardsRequestContext = "OutwardsRequestContext" 27 | 28 | public init() { 29 | 30 | } 31 | 32 | public func handleOutwardsRequestStart(method: HTTPMethod, uri: String, logger: Logger, internalRequestId: String, 33 | headers: inout HTTPHeaders, bodyData: Data) -> String { 34 | return self.outwardsRequestContext 35 | } 36 | 37 | public func handleOutwardsRequestSuccess(outwardsRequestContext: String?, logger: Logger, 38 | internalRequestId: String, 39 | response: HTTPClient.Response, bodyData: Data?) { 40 | // do nothing 41 | } 42 | 43 | public func handleOutwardsRequestFailure(outwardsRequestContext: String?, logger: Logger, 44 | internalRequestId: String, 45 | response: HTTPClient.Response?, bodyData: Data?, 46 | error: Error) { 47 | // do nothing 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/NoHTTPRequestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // NoHTTPRequestInput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | HTTP Request Input has no input. 22 | */ 23 | public struct NoHTTPRequestInput: HTTPRequestInputProtocol { 24 | public let queryEncodable: String? 25 | public let pathEncodable: String? 26 | public let bodyEncodable: String? 27 | public let additionalHeadersEncodable: String? 28 | public let pathPostfix: String? 29 | 30 | public init() { 31 | self.queryEncodable = nil 32 | self.pathEncodable = nil 33 | self.bodyEncodable = nil 34 | self.additionalHeadersEncodable = nil 35 | self.pathPostfix = nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/QueryHTTPRequestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // QueryHTTPRequestInput.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | HTTP Request Input that only has a query. 22 | */ 23 | public struct QueryHTTPRequestInput: HTTPRequestInputProtocol { 24 | public let queryEncodable: QueryType? 25 | public let pathEncodable: QueryType? 26 | public let bodyEncodable: QueryType? 27 | public let additionalHeadersEncodable: QueryType? 28 | public let pathPostfix: String? 29 | 30 | public init(encodable: QueryType) { 31 | self.queryEncodable = encodable 32 | self.pathEncodable = nil 33 | self.bodyEncodable = nil 34 | self.additionalHeadersEncodable = nil 35 | self.pathPostfix = nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/RetriableOutwardsRequestLatencyAggregator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetriableOutwardsRequestAggregator.swift 3 | // 4 | 5 | import Foundation 6 | 7 | /** 8 | An internal type conforming to the `OutwardsRequestAggregator` protocol that is used to aggregate 9 | the outputRequest records for the same output request that is optentially retried. 10 | */ 11 | internal class RetriableOutwardsRequestAggregator: OutwardsRequestAggregator { 12 | private var outputRequestRecords: [OutputRequestRecord] 13 | 14 | internal let accessQueue = DispatchQueue( 15 | label: "com.amazon.SmokeHTTP.RetriableOutwardsRequestAggregator.accessQueue", 16 | target: DispatchQueue.global()) 17 | 18 | init() { 19 | self.outputRequestRecords = [] 20 | } 21 | 22 | func withRecords(completion: @escaping ([OutputRequestRecord]) -> ()) { 23 | self.accessQueue.async { 24 | completion(self.outputRequestRecords) 25 | } 26 | } 27 | 28 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord) { 29 | self.outputRequestRecords.append(outputRequestRecord) 30 | } 31 | 32 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord) { 33 | // for this internal type, we don't need to record retry attempts 34 | } 35 | 36 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord) { 37 | self.outputRequestRecords.append(contentsOf: retriableOutwardsRequest.outputRequests) 38 | } 39 | 40 | func recordOutwardsRequest(outputRequestRecord: OutputRequestRecord, onCompletion: @escaping () -> ()) { 41 | self.accessQueue.async { 42 | self.outputRequestRecords.append(outputRequestRecord) 43 | 44 | onCompletion() 45 | } 46 | } 47 | 48 | func recordRetryAttempt(retryAttemptRecord: RetryAttemptRecord, onCompletion: @escaping () -> ()) { 49 | onCompletion() 50 | } 51 | 52 | func recordRetriableOutwardsRequest(retriableOutwardsRequest: RetriableOutputRequestRecord, onCompletion: @escaping () -> ()) { 53 | self.accessQueue.async { 54 | self.outputRequestRecords.append(contentsOf: retriableOutwardsRequest.outputRequests) 55 | 56 | onCompletion() 57 | } 58 | } 59 | } 60 | 61 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 62 | extension RetriableOutwardsRequestAggregator { 63 | func records() async -> [OutputRequestRecord] { 64 | return await withCheckedContinuation { cont in 65 | withRecords { records in 66 | cont.resume(returning: records) 67 | } 68 | } 69 | } 70 | } 71 | #endif 72 | 73 | struct StandardOutputRequestRecord: OutputRequestRecord { 74 | let requestLatency: TimeInterval 75 | } 76 | 77 | struct StandardRetryAttemptRecord: RetryAttemptRecord { 78 | let retryWait: TimeInterval 79 | } 80 | 81 | struct StandardRetriableOutputRequestRecord: RetriableOutputRequestRecord { 82 | var outputRequests: [OutputRequestRecord] 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/SameThreadAsyncResponseInvocationStrategy.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // SameThreadAsyncResponseInvocationStrategy.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | An AsyncResponseInvocationStrategy that will invocate the completion handler on 22 | the calling thread. 23 | */ 24 | public struct SameThreadAsyncResponseInvocationStrategy: AsyncResponseInvocationStrategy { 25 | 26 | public init() { 27 | 28 | } 29 | 30 | public func invokeResponse(response: OutputType, 31 | completion: @escaping (OutputType) -> ()) { 32 | completion(response) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/StandardHTTPClientCoreInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardHTTPClientCoreInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | import Foundation 18 | import Logging 19 | import NIO 20 | 21 | /** 22 | A type conforming to the `HTTPClientCoreInvocationReporting` protocol.. 23 | */ 24 | public struct StandardHTTPClientCoreInvocationReporting: HTTPClientCoreInvocationReporting { 25 | public let logger: Logger 26 | public var internalRequestId: String 27 | public var traceContext: TraceContextType 28 | public var eventLoop: EventLoop? 29 | public var outwardsRequestAggregator: OutwardsRequestAggregator? 30 | 31 | public init(logger: Logger, 32 | internalRequestId: String, 33 | traceContext: TraceContextType, 34 | eventLoop: EventLoop? = nil, 35 | outwardsRequestAggregator: OutwardsRequestAggregator? = nil) { 36 | self.logger = logger 37 | self.internalRequestId = internalRequestId 38 | self.traceContext = traceContext 39 | self.eventLoop = eventLoop 40 | self.outwardsRequestAggregator = outwardsRequestAggregator 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/StandardHTTPClientInvocationReporting.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // StandardHTTPClientInvocationReporting.swift 15 | // SmokeHTTPClient 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import Metrics 21 | import NIO 22 | 23 | public struct StandardHTTPClientInvocationReporting: HTTPClientInvocationReporting { 24 | public let internalRequestId: String 25 | public let traceContext: TraceContextType 26 | public let logger: Logging.Logger 27 | public var eventLoop: EventLoop? 28 | public var outwardsRequestAggregator: OutwardsRequestAggregator? 29 | public let successCounter: Metrics.Counter? 30 | public let failure5XXCounter: Metrics.Counter? 31 | public let failure4XXCounter: Metrics.Counter? 32 | public let retryCountRecorder: Metrics.Recorder? 33 | public let latencyTimer: Metrics.Timer? 34 | 35 | public init(internalRequestId: String, 36 | traceContext: TraceContextType, 37 | logger: Logging.Logger = Logger(label: "com.amazon.SmokeHTTP.SmokeHTTPClient.StandardHTTPClientInvocationReporting"), 38 | eventLoop: EventLoop? = nil, 39 | outwardsRequestAggregator: OutwardsRequestAggregator? = nil, 40 | successCounter: Metrics.Counter? = nil, 41 | failure5XXCounter: Metrics.Counter? = nil, 42 | failure4XXCounter: Metrics.Counter? = nil, 43 | retryCountRecorder: Metrics.Recorder? = nil, 44 | latencyTimer: Metrics.Timer? = nil) { 45 | self.logger = logger 46 | self.eventLoop = eventLoop 47 | self.outwardsRequestAggregator = outwardsRequestAggregator 48 | self.internalRequestId = internalRequestId 49 | self.traceContext = traceContext 50 | self.successCounter = successCounter 51 | self.failure5XXCounter = failure5XXCounter 52 | self.failure4XXCounter = failure4XXCounter 53 | self.retryCountRecorder = retryCountRecorder 54 | self.latencyTimer = latencyTimer 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SmokeHTTPClient/TestEventLoopProvider.swift: -------------------------------------------------------------------------------- 1 | // A copy of the License is located at 2 | // 3 | // http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // or in the "license" file accompanying this file. This file is distributed 6 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 7 | // express or implied. See the License for the specific language governing 8 | // permissions and limitations under the License. 9 | // 10 | // TestEventLoopProvider.swift 11 | // SmokeHTTPClient 12 | // 13 | 14 | import NIO 15 | import Logging 16 | 17 | /** 18 | Provides eventLoop primarily for testing purposes, either provided or 19 | a single-threaded event loop group owned by the provider. 20 | */ 21 | public enum TestEventLoopProvider { 22 | case provided(EventLoop) 23 | case owned(OwnedTestEventLoopProvider) 24 | 25 | public var eventLoop: EventLoop { 26 | switch self { 27 | case .provided(let eventLoop): 28 | return eventLoop 29 | case .owned(let ownedProvider): 30 | return ownedProvider.eventLoop 31 | } 32 | } 33 | 34 | public static func withProvidedEventLoop(_ eventLoop: EventLoop) -> Self { 35 | return .provided(eventLoop) 36 | } 37 | 38 | public static func withOwnedEventLoop() -> Self { 39 | return .owned(OwnedTestEventLoopProvider()) 40 | } 41 | 42 | /** 43 | Provides a single-threaded `EventLoopGroup` and its `EventLoop`, primarily for testing purposes. 44 | Automatically shuts down the group in the deinitializer so the lifetime of the provided eventloop is tied to 45 | the lifetime of this instance. 46 | */ 47 | public class OwnedTestEventLoopProvider { 48 | public let eventLoopGroup: EventLoopGroup 49 | public let eventLoop: EventLoop 50 | 51 | public init() { 52 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 53 | 54 | self.eventLoop = eventLoopGroup.next() 55 | } 56 | 57 | deinit { 58 | do { 59 | try self.eventLoopGroup.syncShutdownGracefully() 60 | } catch { 61 | let logger = Logger(label: "com.amazon.smoke-http.TestEventLoopProvider") 62 | 63 | logger.error("Unable to shutdown test event loop group.") 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/_SmokeHTTPClientConcurrency/Export.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // Export.swift 15 | // _SmokeHTTPClientConcurrency 16 | // 17 | 18 | // TODO: https://github.com/amzn/smoke-http/issues/91 19 | @_exported import SmokeHTTPClient 20 | -------------------------------------------------------------------------------- /Tests/HTTPHeadersCodingTests/HTTPHeadersCodingTestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPHeadersCodingTestInput.swift 15 | // HTTPHeadersCodingTests 16 | // 17 | 18 | import Foundation 19 | @testable import HTTPHeadersCoding 20 | 21 | struct TestTypeA: Codable, Equatable { 22 | let firstly: String 23 | let secondly: String 24 | let thirdly: String 25 | } 26 | 27 | struct TestTypeB: Codable, Equatable { 28 | let action: String 29 | let ids: [String] 30 | } 31 | 32 | struct TestTypeC: Codable, Equatable { 33 | let action: String 34 | let map: [String: String] 35 | } 36 | 37 | struct TestTypeD1: Codable, Equatable { 38 | let action: String 39 | let ids: [TestTypeA] 40 | } 41 | 42 | struct TestTypeD2: Codable, Equatable { 43 | let action: String 44 | let id: TestTypeA 45 | } 46 | 47 | struct TestTypeG: Codable, Equatable { 48 | let id: String 49 | let optionalString: String? 50 | let data: Data? 51 | let date: Date? 52 | let bool: Bool? 53 | let int: Int? 54 | let double: Double? 55 | 56 | enum CodingKeys: String, CodingKey { 57 | case id = "Id" 58 | case optionalString = "OptionalString" 59 | case data = "Data" 60 | case date = "Date" 61 | case bool = "Bool" 62 | case int = "Int" 63 | case double = "Double" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/HTTPPathCodingTests/GetShapeForTemplateTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // GetShapeForTemplateTests.swift 15 | // HTTPPathCodingTests 16 | // 17 | 18 | import XCTest 19 | @testable import HTTPPathCoding 20 | import ShapeCoding 21 | 22 | class GetShapeForTemplateTests: XCTestCase { 23 | 24 | func verifySuccessfulShape(template: String, path: String) throws { 25 | let templateSegments = try HTTPPathSegment.tokenize(template: template) 26 | let pathSegments = HTTPPathSegment.getPathSegmentsForPath(uri: path) 27 | 28 | let expected: [String: Shape] = ["id": .string("cat"), 29 | "index": .string("23")] 30 | 31 | let shape: Shape 32 | do { 33 | shape = try pathSegments.getShapeForTemplate(templateSegments: templateSegments) 34 | } catch { 35 | return XCTFail() 36 | } 37 | 38 | guard case let .dictionary(values) = shape else { 39 | return XCTFail() 40 | } 41 | 42 | XCTAssertEqual(expected, values) 43 | } 44 | 45 | func verifyUnsuccessfulShape(template: String, path: String) throws { 46 | let templateSegments = try HTTPPathSegment.tokenize(template: template) 47 | let pathSegments = HTTPPathSegment.getPathSegmentsForPath(uri: path) 48 | 49 | do { 50 | _ = try pathSegments.getShapeForTemplate(templateSegments: templateSegments) 51 | XCTFail() 52 | } catch { 53 | // expected failure 54 | } 55 | } 56 | 57 | func testBasicGetShape() throws { 58 | let template = "person{id}address{index}street" 59 | let path = "personcataddress23street" 60 | 61 | try verifySuccessfulShape(template: template, path: path) 62 | } 63 | 64 | func testGetShapeWithSegments() throws { 65 | let template = "person/{id}/address/{index}/street" 66 | let path = "person/cat/address/23/street" 67 | 68 | try verifySuccessfulShape(template: template, path: path) 69 | } 70 | 71 | func testCaseInsensitiveGetShape() throws { 72 | let template = "person/{id}/adDRess/{index}/street" 73 | let path = "Person/cat/address/23/Street" 74 | 75 | try verifySuccessfulShape(template: template, path: path) 76 | } 77 | 78 | func testTooFewSegmentsGetShape() throws { 79 | let template = "person/{id}/address/{index}/street" 80 | let path = "person/cat/address" 81 | 82 | try verifyUnsuccessfulShape(template: template, path: path) 83 | } 84 | 85 | func testTooManySegmentsGetShape() throws { 86 | let template = "person/{id}/address/{index}/street" 87 | let path = "person/cat/address/23/street/number/13" 88 | 89 | try verifyUnsuccessfulShape(template: template, path: path) 90 | } 91 | 92 | func testNotMatchingGetShape() throws { 93 | let template = "person/{id}/address/{index}/street" 94 | let path = "person/cat/country/23/street" 95 | 96 | try verifyUnsuccessfulShape(template: template, path: path) 97 | } 98 | 99 | func testGreedyTokenGetShape() throws { 100 | let template = "person/{id}/address/{index+}" 101 | let path = "person/cat/address/23/street" 102 | 103 | let templateSegments = try HTTPPathSegment.tokenize(template: template) 104 | let pathSegments = HTTPPathSegment.getPathSegmentsForPath(uri: path) 105 | 106 | let expected: [String: Shape] = ["id": .string("cat"), 107 | "index": .string("23/street")] 108 | 109 | let shape: Shape 110 | do { 111 | shape = try pathSegments.getShapeForTemplate(templateSegments: templateSegments) 112 | } catch { 113 | return XCTFail() 114 | } 115 | 116 | guard case let .dictionary(values) = shape else { 117 | return XCTFail() 118 | } 119 | 120 | XCTAssertEqual(expected, values) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Tests/HTTPPathCodingTests/HTTPPathCoderTestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathTestInput.swift 15 | // HTTPPathCodingTests 16 | // 17 | 18 | import Foundation 19 | @testable import HTTPPathCoding 20 | 21 | struct TestTypeA: Codable, Equatable { 22 | let firstly: String 23 | let secondly: String 24 | let thirdly: String 25 | } 26 | 27 | struct TestTypeB: Codable, Equatable { 28 | let action: String 29 | let ids: [String] 30 | } 31 | 32 | struct TestTypeC: Codable, Equatable { 33 | let action: String 34 | let map: [String: String] 35 | } 36 | 37 | struct TestTypeD1: Codable, Equatable { 38 | let action: String 39 | let ids: [TestTypeA] 40 | } 41 | 42 | struct TestTypeD2: Codable, Equatable { 43 | let action: String 44 | let id: TestTypeA 45 | } 46 | 47 | struct TestTypeE: Codable, Equatable { 48 | let firstly: String 49 | let secondly: String 50 | let thirdly: String 51 | 52 | enum CodingKeys: String, CodingKey { 53 | case firstly = "values.1" 54 | case secondly = "values.2" 55 | case thirdly = "values.3" 56 | } 57 | } 58 | 59 | struct TestTypeF: Codable, Equatable { 60 | let firstly: String 61 | let secondly: String 62 | let thirdly: String 63 | 64 | enum CodingKeys: String, CodingKey { 65 | case firstly = "values.one" 66 | case secondly = "values.two" 67 | case thirdly = "values.three" 68 | } 69 | } 70 | 71 | struct TestTypeG: Codable, Equatable { 72 | let id: String 73 | let optionalString: String? 74 | let data: Data? 75 | let date: Date? 76 | let bool: Bool? 77 | let int: Int? 78 | let double: Double? 79 | 80 | enum CodingKeys: String, CodingKey { 81 | case id = "Id" 82 | case optionalString = "OptionalString" 83 | case data = "Data" 84 | case date = "Date" 85 | case bool = "Bool" 86 | case int = "Int" 87 | case double = "Double" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/HTTPPathCodingTests/HTTPPathSegmentTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathTokenTests.swift 15 | // HTTPPathCodingTests 16 | // 17 | 18 | import XCTest 19 | @testable import HTTPPathCoding 20 | 21 | class HTTPPathSegmentTests: XCTestCase { 22 | 23 | func testBasicTokenize() throws { 24 | let input = "person/{id}/address{index}/street" 25 | 26 | let expected = [HTTPPathSegment(tokens: [.string("person")]), 27 | HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 28 | HTTPPathSegment(tokens: [.string("address"), 29 | .variable(name: "index", multiSegment: false)]), 30 | HTTPPathSegment(tokens: [.string("street")])] 31 | 32 | let output = try HTTPPathSegment.tokenize(template: input) 33 | XCTAssertEqual(expected, output) 34 | } 35 | 36 | func testTokenizeStartingSlash() throws { 37 | let input = "/person/{id}/address{index}/street" 38 | 39 | let expected = [HTTPPathSegment(tokens: [.string("person")]), 40 | HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 41 | HTTPPathSegment(tokens: [.string("address"), 42 | .variable(name: "index", multiSegment: false)]), 43 | HTTPPathSegment(tokens: [.string("street")])] 44 | 45 | let output = try HTTPPathSegment.tokenize(template: input) 46 | XCTAssertEqual(expected, output) 47 | } 48 | 49 | func testTokenizeAtStart() throws { 50 | let input = "{id}/address{index}/street" 51 | 52 | let expected = [HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 53 | HTTPPathSegment(tokens: [.string("address"), 54 | .variable(name: "index", multiSegment: false)]), 55 | HTTPPathSegment(tokens: [.string("street")])] 56 | 57 | let output = try HTTPPathSegment.tokenize(template: input) 58 | XCTAssertEqual(expected, output) 59 | } 60 | 61 | func testTokenizeAtEnd() throws { 62 | let input = "/person/{id}/address{index}" 63 | 64 | let expected = [HTTPPathSegment(tokens: [.string("person")]), 65 | HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 66 | HTTPPathSegment(tokens: [.string("address"), 67 | .variable(name: "index", multiSegment: false)])] 68 | 69 | let output = try HTTPPathSegment.tokenize(template: input) 70 | XCTAssertEqual(expected, output) 71 | } 72 | 73 | func testTokenizeAtEndWithPlus() throws { 74 | let input = "/person/{id}/address{index+}" 75 | 76 | let expected = [HTTPPathSegment(tokens: [.string("person")]), 77 | HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 78 | HTTPPathSegment(tokens: [.string("address"), 79 | .variable(name: "index", multiSegment: true)])] 80 | 81 | let output = try HTTPPathSegment.tokenize(template: input) 82 | XCTAssertEqual(expected, output) 83 | } 84 | 85 | func testTokenizeAtEndWithPlusWithTrail() throws { 86 | let input = "/person/{id}/address{index+}?trail" 87 | 88 | let expected = [HTTPPathSegment(tokens: [.string("person")]), 89 | HTTPPathSegment(tokens: [.variable(name: "id", multiSegment: false)]), 90 | HTTPPathSegment(tokens: [.string("address"), 91 | .variable(name: "index", multiSegment: true), 92 | .string("?trail")])] 93 | 94 | let output = try! HTTPPathSegment.tokenize(template: input) 95 | XCTAssertEqual(expected, output) 96 | } 97 | 98 | func testInvalidTokenize() throws { 99 | let input = "/person/{id+}/address{index}" 100 | 101 | do { 102 | _ = try HTTPPathSegment.tokenize(template: input) 103 | 104 | XCTFail() 105 | } catch { 106 | // expected 107 | } 108 | } 109 | 110 | func testInvalidTokenizeWithTrail() throws { 111 | let input = "/person/{id+}trail/address{index}" 112 | 113 | do { 114 | _ = try HTTPPathSegment.tokenize(template: input) 115 | 116 | XCTFail() 117 | } catch { 118 | // expected 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/HTTPPathCodingTests/HTTPPathTokenTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // HTTPPathTokenTests.swift 15 | // HTTPPathCodingTests 16 | // 17 | 18 | import XCTest 19 | @testable import HTTPPathCoding 20 | 21 | class HTTPPathTokenTests: XCTestCase { 22 | 23 | func testBasicTokenize() throws { 24 | let input = "person{id}address{index}street" 25 | 26 | let expected: [HTTPPathToken] = [.string("person"), 27 | .variable(name: "id", multiSegment: false), 28 | .string("address"), 29 | .variable(name: "index", multiSegment: false), 30 | .string("street")] 31 | 32 | let output = try HTTPPathToken.tokenize(template: input) 33 | XCTAssertEqual(expected, output) 34 | } 35 | 36 | func testTokenizeAtStart() throws { 37 | let input = "{id}address{index}street" 38 | 39 | let expected: [HTTPPathToken] = [.variable(name: "id", multiSegment: false), 40 | .string("address"), 41 | .variable(name: "index", multiSegment: false), 42 | .string("street")] 43 | 44 | let output = try HTTPPathToken.tokenize(template: input) 45 | XCTAssertEqual(expected, output) 46 | } 47 | 48 | func testTokenizeAtEnd() throws { 49 | let input = "person{id}address{index}" 50 | 51 | let expected: [HTTPPathToken] = [.string("person"), 52 | .variable(name: "id", multiSegment: false), 53 | .string("address"), 54 | .variable(name: "index", multiSegment: false)] 55 | 56 | let output = try HTTPPathToken.tokenize(template: input) 57 | XCTAssertEqual(expected, output) 58 | } 59 | 60 | func testTokenizeAtEndWithPlus() throws { 61 | let input = "/person/{id}/address/{path+}" 62 | 63 | let expected: [HTTPPathToken] = [.string("/person/"), 64 | .variable(name: "id", multiSegment: false), 65 | .string("/address/"), 66 | .variable(name: "path", multiSegment: true)] 67 | 68 | let output = try HTTPPathToken.tokenize(template: input) 69 | XCTAssertEqual(expected, output) 70 | } 71 | 72 | func testInvalidTokenize() throws { 73 | let input = "person{id+}address{index}" 74 | 75 | do { 76 | _ = try HTTPPathToken.tokenize(template: input) 77 | XCTFail() 78 | } catch { 79 | // expected 80 | } 81 | } 82 | 83 | func testInvalidAdjoiningVariables() throws { 84 | let input = "person{id}{count}address{index}" 85 | 86 | do { 87 | _ = try HTTPPathToken.tokenize(template: input) 88 | XCTFail() 89 | } catch { 90 | // expected 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/QueryCodingTests/QueryCodingTestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // QueryCodingTestInput.swift 15 | // QueryCodingTests 16 | // 17 | 18 | import Foundation 19 | @testable import QueryCoding 20 | 21 | struct TestTypeA: Codable, Equatable { 22 | let firstly: String 23 | let secondly: String 24 | let thirdly: String 25 | } 26 | 27 | struct TestTypeB: Codable, Equatable { 28 | let action: String 29 | let ids: [String] 30 | } 31 | 32 | struct TestTypeC: Codable, Equatable { 33 | let action: String 34 | let map: [String: String] 35 | } 36 | 37 | struct TestTypeD1: Codable, Equatable { 38 | let action: String 39 | let ids: [TestTypeA] 40 | } 41 | 42 | struct TestTypeD2: Codable, Equatable { 43 | let action: String 44 | let id: TestTypeA 45 | } 46 | 47 | struct TestTypeE: Codable, Equatable { 48 | let firstly: String 49 | let secondly: String 50 | let thirdly: String 51 | 52 | enum CodingKeys: String, CodingKey { 53 | case firstly = "values.1" 54 | case secondly = "values.2" 55 | case thirdly = "values.3" 56 | } 57 | } 58 | 59 | struct TestTypeF: Codable, Equatable { 60 | let firstly: String 61 | let secondly: String 62 | let thirdly: String 63 | 64 | enum CodingKeys: String, CodingKey { 65 | case firstly = "values.one" 66 | case secondly = "values.two" 67 | case thirdly = "values.three" 68 | } 69 | } 70 | 71 | struct TestTypeG: Codable, Equatable { 72 | let id: String 73 | let optionalString: String? 74 | let data: Data? 75 | let date: Date? 76 | let bool: Bool? 77 | let int: Int? 78 | let double: Double? 79 | 80 | enum CodingKeys: String, CodingKey { 81 | case id = "Id" 82 | case optionalString = "OptionalString" 83 | case data = "Data" 84 | case date = "Date" 85 | case bool = "Bool" 86 | case int = "Int" 87 | case double = "Double" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/SmokeHTTPClientTests/SmokeHTTPClientTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // SmokeHTTPClientTests.swift 15 | // SmokeHTTPClientTests 16 | // 17 | 18 | import XCTest 19 | @testable import SmokeHTTPClient 20 | @testable import NIOHTTP1 21 | 22 | final class SmokeHTTPClientTests: XCTestCase { 23 | func testHTTPMethodRawValue() { 24 | XCTAssertEqual(HTTPMethod.GET.rawValue, "GET") 25 | } 26 | } 27 | --------------------------------------------------------------------------------