|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift Baggage Context open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2020 Moritz Lang and the Swift Baggage Context project authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// |
| 10 | +// SPDX-License-Identifier: Apache-2.0 |
| 11 | +// |
| 12 | +//===----------------------------------------------------------------------===// |
| 13 | + |
| 14 | +// ==== ---------------------------------------------------------------------------------------------------------------- |
| 15 | +// MARK: Baggage |
| 16 | + |
| 17 | +/// A `Baggage` is a heterogeneous storage type with value semantics for keyed values in a type-safe |
| 18 | +/// fashion. |
| 19 | +/// |
| 20 | +/// Its values are uniquely identified via `BaggageKey`s (by type identity). These keys also dictate the type of |
| 21 | +/// value allowed for a specific key-value pair through their associated type `Value`. |
| 22 | +/// |
| 23 | +/// ## Defining keys and accessing values |
| 24 | +/// Baggage keys are defined as types, most commonly case-less enums (as no actual instances are actually required) |
| 25 | +/// which conform to the `Baggage.Key` protocol: |
| 26 | +/// |
| 27 | +/// private enum TestIDKey: BaggageKey { |
| 28 | +/// typealias Value = String |
| 29 | +/// } |
| 30 | +/// |
| 31 | +/// While defining a key, one should also immediately declare an extension on `BaggageProtocol`, |
| 32 | +/// to allow convenient and discoverable ways to interact with the baggage item, the extension should take the form of: |
| 33 | +/// |
| 34 | +/// extension BaggageProtocol { |
| 35 | +/// var testID: TestIDKey.Value? { |
| 36 | +/// get { |
| 37 | +/// self[TestIDKey.self] |
| 38 | +/// } set { |
| 39 | +/// self[TestIDKey.self] = newValue |
| 40 | +/// } |
| 41 | +/// } |
| 42 | +/// } |
| 43 | +/// |
| 44 | +/// For consistency, it is recommended to name key types with the `...Key` suffix (e.g. `SomethingKey`) and the property |
| 45 | +/// used to access a value identifier by such key the prefix of the key (e.g. `something`). Please also observe the usual |
| 46 | +/// Swift naming conventions, e.g. prefer `ID` to `Id` etc. |
| 47 | +/// |
| 48 | +/// ## Usage |
| 49 | +/// Using a baggage container is fairly straight forward, as it boils down to using the prepared computed properties: |
| 50 | +/// |
| 51 | +/// var context = Baggage.background |
| 52 | +/// // set a new value |
| 53 | +/// context.testID = "abc" |
| 54 | +/// // retrieve a stored value |
| 55 | +/// let testID = context.testID ?? "default" |
| 56 | +/// // remove a stored value |
| 57 | +/// context[TestIDKey.self] = nil |
| 58 | +/// |
| 59 | +/// Note that normally a baggage should not be "created" ad-hoc by user code, but rather it should be passed to it from |
| 60 | +/// a runtime. For example, when working in an HTTP server framework, it is most likely that the baggage is already passed |
| 61 | +/// directly or indirectly (e.g. in a `BaggageContext` or `FrameworkContext`) |
| 62 | +/// |
| 63 | +/// ### Accessing all values |
| 64 | +/// |
| 65 | +/// The only way to access "all" values in a baggage context is by using the `forEachBaggageItem` function. |
| 66 | +/// The baggage container on purpose does not expose more functions to prevent abuse and treating it as too much of an |
| 67 | +/// arbitrary value smuggling container, but only make it convenient for tracing and instrumentation systems which need |
| 68 | +/// to access either specific or all items carried inside a baggage. |
| 69 | +public struct Baggage: BaggageProtocol { |
| 70 | + public typealias Key = BaggageKey |
| 71 | + |
| 72 | + private var _storage = [AnyBaggageKey: Any]() |
| 73 | + |
| 74 | + /// Internal on purpose, please use `Baggage.TODO` or `Baggage.background` to create an "empty" context, |
| 75 | + /// which carries more meaning to other developers why an empty context was used. |
| 76 | + init() {} |
| 77 | + |
| 78 | + public var baggage: Baggage { |
| 79 | + get { |
| 80 | + return self |
| 81 | + } |
| 82 | + set { |
| 83 | + self = newValue |
| 84 | + } |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +extension Baggage { |
| 89 | + /// Creates a new empty baggage, generally used for background processing tasks or an "initial" baggage to be immediately |
| 90 | + /// populated with some values by a framework or runtime. |
| 91 | + /// |
| 92 | + /// Typically, this would only be called in a "top" or "background" setting, such as the main function, initialization, |
| 93 | + /// tests, beginning of some background task or some other top-level baggage to be immediately populated with incoming request/message information. |
| 94 | + /// |
| 95 | + /// ## Usage in frameworks and libraries |
| 96 | + /// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's, |
| 97 | + /// message's or task's processing is initiated. For example, a framework handling requests, should create an empty |
| 98 | + /// context when handling a request only to immediately populate it with useful trace information extracted from e.g. |
| 99 | + /// request headers. |
| 100 | + /// |
| 101 | + /// ## Usage in applications |
| 102 | + /// Application code should never have to create an empty context during the processing lifetime of any request, |
| 103 | + /// and only should create contexts if some processing is performed in the background - thus the naming of this property. |
| 104 | + /// |
| 105 | + /// Usually, a framework such as an HTTP server or similar "request handler" would already provide users |
| 106 | + /// with a context to be passed along through subsequent calls. |
| 107 | + /// |
| 108 | + /// If unsure where to obtain a context from, prefer using `.TODO("Not sure where I should get a context from here?")`, |
| 109 | + /// in order to inform other developers that the lack of context passing was not done on purpose, but rather because either |
| 110 | + /// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being |
| 111 | + /// baggage context aware just yet. |
| 112 | + public static var background: Baggage { |
| 113 | + return Baggage() |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +extension Baggage { |
| 118 | + /// A baggage intended as a placeholder until a real value can be passed through a function call. |
| 119 | + /// |
| 120 | + /// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible, |
| 121 | + /// e.g. because an external library did not pass it correctly and has to be fixed before the proper context |
| 122 | + /// can be obtained where the TO-DO is currently used. |
| 123 | + /// |
| 124 | + /// ## Crashing on TO-DO context creation |
| 125 | + /// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash |
| 126 | + /// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that |
| 127 | + /// a project never ends up using with code initially was written as "was lazy, did not pass context", yet the |
| 128 | + /// project requires context passing to be done correctly throughout the application. Similar checks can be performed |
| 129 | + /// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context |
| 130 | + /// being passed as illegal and warn or error when spotted. |
| 131 | + /// |
| 132 | + /// ## Example |
| 133 | + /// |
| 134 | + /// frameworkHandler { what in |
| 135 | + /// hello(who: "World", baggage: .TODO("The framework XYZ should be modified to pass us a context here, and we'd pass it along")) |
| 136 | + /// } |
| 137 | + /// |
| 138 | + /// - Parameters: |
| 139 | + /// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one, |
| 140 | + /// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`. |
| 141 | + public static func TODO(_ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> Baggage { |
| 142 | + var context = Baggage.background |
| 143 | + #if BAGGAGE_CRASH_TODOS |
| 144 | + fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)") |
| 145 | + #else |
| 146 | + context[TODOKey.self] = .init(file: file, line: line) |
| 147 | + return context |
| 148 | + #endif |
| 149 | + } |
| 150 | + |
| 151 | + private enum TODOKey: BaggageKey { |
| 152 | + typealias Value = TODOLocation |
| 153 | + static var name: String? { |
| 154 | + return "todo" |
| 155 | + } |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +/// The `BaggageProtocol` should not directly be used in APIs. |
| 160 | +/// |
| 161 | +/// Prefer accepting the `ContextProtocol` in APIs which need to accept a baggage context. |
| 162 | +/// If `Context` can not be used for some reason, it is preferable to accept a `Baggage` directly rather than this protocol. |
| 163 | +public protocol BaggageProtocol { |
| 164 | + /// Get the `Baggage` container. |
| 165 | + var baggage: Baggage { get set } |
| 166 | + |
| 167 | + /// Provides type-safe access to the baggage's values. |
| 168 | + /// |
| 169 | + /// Rather than using this subscript directly, users SHOULD offer a convenience accessor to their values, |
| 170 | + /// using the following pattern: |
| 171 | + /// |
| 172 | + /// extension BaggageProtocol { |
| 173 | + /// var testID: TestIDKey.Value? { |
| 174 | + /// get { |
| 175 | + /// self[TestIDKey.self] |
| 176 | + /// } |
| 177 | + /// set { |
| 178 | + /// self[TestIDKey.self] = newValue |
| 179 | + /// } |
| 180 | + /// } |
| 181 | + /// } |
| 182 | + /// |
| 183 | + /// Note that specific baggage and context types MAY (and usually do), offer also a way to set baggage values, |
| 184 | + /// however in the most general case it is not required, as some frameworks may only be able to offer reading. |
| 185 | + subscript<Key: BaggageKey>(_ key: Key.Type) -> Key.Value? { get set } |
| 186 | + |
| 187 | + /// Calls the given closure for each item contained in the underlying `Baggage`. |
| 188 | + /// |
| 189 | + /// Order of those invocations is NOT guaranteed and should not be relied on. |
| 190 | + /// |
| 191 | + /// - Parameter body: A closure invoked with the type erased key and value stored for the key in this baggage. |
| 192 | + func forEachBaggageItem(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows |
| 193 | +} |
| 194 | + |
| 195 | +/// Writable `BaggageProtocol`. |
| 196 | +/// |
| 197 | +/// - SeeAlso: `_WritableBaggageProtocol` |
| 198 | +public protocol _WritableBaggageProtocol: BaggageProtocol { |
| 199 | + var baggage: Baggage { get set } |
| 200 | +} |
| 201 | + |
| 202 | +extension Baggage { |
| 203 | + public subscript<Key: BaggageKey>(_ key: Key.Type) -> Key.Value? { |
| 204 | + get { |
| 205 | + guard let value = self._storage[AnyBaggageKey(key)] else { return nil } |
| 206 | + // safe to force-cast as this subscript is the only way to set a value. |
| 207 | + return (value as! Key.Value) |
| 208 | + } |
| 209 | + set { |
| 210 | + self._storage[AnyBaggageKey(key)] = newValue |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + public func forEachBaggageItem(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows { |
| 215 | + try self._storage.forEach { key, value in |
| 216 | + try body(key, value) |
| 217 | + } |
| 218 | + } |
| 219 | +} |
| 220 | + |
| 221 | +extension Baggage: CustomStringConvertible { |
| 222 | + /// A context's description prints only keys of the contained values. |
| 223 | + /// This is in order to prevent spilling a lot of detailed information of carried values accidentally. |
| 224 | + /// |
| 225 | + /// `Baggage`s are not intended to be printed "raw" but rather inter-operate with tracing, logging and other systems, |
| 226 | + /// which can use the `forEach` function providing access to its underlying values. |
| 227 | + public var description: String { |
| 228 | + return "\(type(of: self).self)(keys: \(self._storage.map { $0.key.name }))" |
| 229 | + } |
| 230 | +} |
| 231 | + |
| 232 | +/// Carried automatically by a "to do" baggage context. |
| 233 | +/// It can be used to track where a context originated and which "to do" context must be fixed into a real one to avoid this. |
| 234 | +public struct TODOLocation { |
| 235 | + let file: String |
| 236 | + let line: UInt |
| 237 | +} |
0 commit comments