From 4f086dff385ccefa5f1acf62a0b38b0f984db143 Mon Sep 17 00:00:00 2001 From: Moritz Lang Date: Thu, 25 Jun 2020 11:15:27 +0200 Subject: [PATCH] Initial Commit --- .github/workflows/ci.yaml | 13 ++ .gitignore | 6 + .swiftformat | 17 ++ LICENSE.txt | 202 +++++++++++++++++++ Package.swift | 21 ++ README.md | 16 ++ Sources/Baggage/BaggageContext.swift | 111 ++++++++++ Tests/BaggageTests/BaggageContextTests.swift | 74 +++++++ scripts/sanity.sh | 7 + scripts/validate_format.sh | 27 +++ 10 files changed, 494 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .swiftformat create mode 100644 LICENSE.txt create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/Baggage/BaggageContext.swift create mode 100644 Tests/BaggageTests/BaggageContextTests.swift create mode 100755 scripts/sanity.sh create mode 100755 scripts/validate_format.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ed4d1e0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,13 @@ +name: CI + +on: [push, pull_request] + +jobs: + unit-test: + runs-on: ubuntu-latest + container: swift:5.2 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Build & Test + run: swift test -c release --enable-test-discovery diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b90dbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +.swiftpm diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..cbe55e4 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,17 @@ +# file options + +--swiftversion 5.2 +--exclude .build +--exclude UseCases/.build + +# format options + +--ifdef no-indent +--patternlet inline +--self insert +--stripunusedargs closure-only +--wraparguments before-first + +# rules + +--disable blankLinesAroundMark diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..ff86679 --- /dev/null +++ b/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "gsoc-swift-baggage-context", + products: [ + .library(name: "Baggage", targets: ["Baggage"]) + ], + targets: [ + .target( + name: "Baggage", + dependencies: [] + ), + .testTarget( + name: "BaggageTests", + dependencies: [ + "Baggage" + ] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..370ca45 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Baggage Context + +[![Swift 5.2](https://img.shields.io/badge/Swift-5.2-ED523F.svg?style=flat)](https://swift.org/download/) +[![CI](https://github.com/slashmo/gsoc-swift-baggage-context/workflows/CI/badge.svg)](https://github.com/slashmo/gsoc-swift-baggage-context/actions?query=workflow%3ACI) + +`BaggageContext` originated in [this repo](https://github.com/slashmo/gsoc-swift-tracing), but had to be factored out so that we can depend on it in our [NIO fork](https://github.com/slashmo/swift-nio). + +## Contributing + +Please make sure to run the `./scripts/sanity.sh` script when contributing, it checks formatting and similar things. + +You can make ensure it always is run and passes before you push by installing a pre-push hook with git: + +``` +echo './scripts/sanity.sh' > .git/hooks/pre-push +``` diff --git a/Sources/Baggage/BaggageContext.swift b/Sources/Baggage/BaggageContext.swift new file mode 100644 index 0000000..1672316 --- /dev/null +++ b/Sources/Baggage/BaggageContext.swift @@ -0,0 +1,111 @@ +/// A `BaggageContext` is a heterogeneous storage type with value semantics for keyed values in a type-safe +/// fashion. Its values are uniquely identified via `BaggageContextKey`s. These keys also dictate the type of +/// value allowed for a specific key-value pair through their associated type `Value`. +/// +/// ## Subscript access +/// You may access the stored values by subscripting with a key type conforming to `BaggageContextKey`. +/// +/// enum TestIDKey: BaggageContextKey { +/// typealias Value = String +/// } +/// +/// var baggage = BaggageContext() +/// // set a new value +/// baggage[TestIDKey.self] = "abc" +/// // retrieve a stored value +/// baggage[TestIDKey.self] ?? "default" +/// // remove a stored value +/// baggage[TestIDKey.self] = nil +/// +/// ## Convenience extensions +/// +/// Libraries may also want to provide an extension, offering the values that users are expected to reach for +/// using the following pattern: +/// +/// extension BaggageContext { +/// var testID: TestIDKey.Value { +/// get { +/// self[TestIDKey.self] +/// } set { +/// self[TestIDKey.self] = newValue +/// } +/// } +/// } +public struct BaggageContext { + private var _storage = [AnyBaggageContextKey: ValueContainer]() + + /// Create an empty `BaggageContext`. + public init() {} + + public subscript(_ key: Key.Type) -> Key.Value? { + get { + self._storage[AnyBaggageContextKey(key)]?.forceUnwrap(key) + } set { + self._storage[AnyBaggageContextKey(key)] = newValue.map { + ValueContainer(value: $0) + } + } + } + + public var baggageItems: [AnyBaggageContextKey: Any] { + // TODO: key may not be unique + self._storage.reduce(into: [:]) { + $0[$1.key] = $1.value.value + } + } + + private struct ValueContainer { + let value: Any + + func forceUnwrap(_ key: Key.Type) -> Key.Value { + self.value as! Key.Value + } + } +} + +extension BaggageContext: CustomStringConvertible { + public var description: String { + "\(Self.self)(keys: \(self._storage.map(\.key.name)))" + } +} + +/// `BaggageContextKey`s are used as keys in a `BaggageContext`. Their associated type `Value` gurantees type-safety. +/// To give your `BaggageContextKey` an explicit name you may override the `name` property. +public protocol BaggageContextKey { + /// The type of `Value` uniquely identified by this key. + associatedtype Value + + /// The human-readable name of this key. Defaults to `nil`. + static var name: String? { get } +} + +extension BaggageContextKey { + public static var name: String? { nil } +} + +public struct AnyBaggageContextKey { + public let keyType: Any.Type + + private let _name: String? + + /// A human-readable String representation of the underlying key. + /// If no explicit name has been set on the wrapped key the type name is used. + public var name: String { + self._name ?? String(describing: self.keyType.self) + } + + public init(_ keyType: Key.Type) where Key: BaggageContextKey { + self.keyType = keyType + self._name = keyType.name + } +} + +extension AnyBaggageContextKey: Hashable { + public static func == (lhs: AnyBaggageContextKey, rhs: AnyBaggageContextKey) -> Bool { + ObjectIdentifier(lhs.keyType) == ObjectIdentifier(rhs.keyType) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self.keyType)) + } +} diff --git a/Tests/BaggageTests/BaggageContextTests.swift b/Tests/BaggageTests/BaggageContextTests.swift new file mode 100644 index 0000000..df13578 --- /dev/null +++ b/Tests/BaggageTests/BaggageContextTests.swift @@ -0,0 +1,74 @@ +import Baggage +import XCTest + +final class BaggageContextTests: XCTestCase { + func testSubscriptAccess() { + let testID = 42 + + var baggage = BaggageContext() + XCTAssertNil(baggage[TestIDKey.self]) + + baggage[TestIDKey.self] = testID + XCTAssertEqual(baggage[TestIDKey], testID) + + baggage[TestIDKey.self] = nil + XCTAssertNil(baggage[TestIDKey.self]) + } + + func testRecommendedConvenienceExtension() { + let testID = 42 + + var baggage = BaggageContext() + XCTAssertNil(baggage.testID) + + baggage.testID = testID + XCTAssertEqual(baggage.testID, testID) + + baggage[TestIDKey.self] = nil + XCTAssertNil(baggage.testID) + } + + func testEmptyBaggageDescription() { + XCTAssertEqual(String(describing: BaggageContext()), "BaggageContext(keys: [])") + } + + func testSingleKeyBaggageDescription() { + var baggage = BaggageContext() + baggage.testID = 42 + + XCTAssertEqual(String(describing: baggage), #"BaggageContext(keys: ["TestIDKey"])"#) + } + + func testMultiKeysBaggageDescription() { + var baggage = BaggageContext() + baggage.testID = 42 + baggage[SecondTestIDKey.self] = "test" + + let description = String(describing: baggage) + XCTAssert(description.starts(with: "BaggageContext(keys: [")) + // use contains instead of `XCTAssertEqual` because the order is non-predictable (Dictionary) + XCTAssert(description.contains("TestIDKey")) + XCTAssert(description.contains("ExplicitKeyName")) + print(description.reversed().starts(with: "])")) + } +} + +private enum TestIDKey: BaggageContextKey { + typealias Value = Int +} + +private extension BaggageContext { + var testID: Int? { + get { + self[TestIDKey.self] + } set { + self[TestIDKey.self] = newValue + } + } +} + +private enum SecondTestIDKey: BaggageContextKey { + typealias Value = String + + static let name: String? = "ExplicitKeyName" +} diff --git a/scripts/sanity.sh b/scripts/sanity.sh new file mode 100755 index 0000000..fe1de22 --- /dev/null +++ b/scripts/sanity.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -eu +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# bash $here/validate_license_header.sh +bash $here/validate_format.sh diff --git a/scripts/validate_format.sh b/scripts/validate_format.sh new file mode 100755 index 0000000..d25d7c4 --- /dev/null +++ b/scripts/validate_format.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -u + +# verify that swiftformat is on the PATH +command -v swiftformat >/dev/null 2>&1 || { echo >&2 "'swiftformat' could not be found. Please ensure it is installed and on the PATH."; exit 1; } + +printf "=> Checking format\n" +FIRST_OUT="$(git status --porcelain)" +# swiftformat does not scale so we loop ourselves +shopt -u dotglob +find Sources/* Tests/* -type d | while IFS= read -r d; do + printf " * checking $d... " + out=$(swiftformat $d 2>&1) + SECOND_OUT="$(git status --porcelain)" + if [[ "$out" == *"error"*] && ["$out" != "*No eligible files" ]]; then + printf "\033[0;31merror!\033[0m\n" + echo $out + exit 1 + fi + if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then + printf "\033[0;31mformatting issues!\033[0m\n" + git --no-pager diff + exit 1 + fi + printf "\033[0;32mokay.\033[0m\n" +done