Skip to content
This repository was archived by the owner on Apr 23, 2021. It is now read-only.

Commit 7be716f

Browse files
committed
Final API proposal: Less is More / Simple is Better
1 parent 5bc60c2 commit 7be716f

26 files changed

+915
-591
lines changed

Package.swift

+10-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import PackageDescription
33

44
let package = Package(
5-
name: "swift-baggage-context",
5+
name: "swift-context",
66
products: [
77
.library(
88
name: "Baggage",
@@ -11,9 +11,9 @@ let package = Package(
1111
]
1212
),
1313
.library(
14-
name: "BaggageLogging",
14+
name: "BaggageContext",
1515
targets: [
16-
"BaggageLogging",
16+
"BaggageContext",
1717
]
1818
),
1919
],
@@ -27,7 +27,7 @@ let package = Package(
2727
),
2828

2929
.target(
30-
name: "BaggageLogging",
30+
name: "BaggageContext",
3131
dependencies: [
3232
"Baggage",
3333
.product(name: "Logging", package: "swift-log"),
@@ -45,26 +45,25 @@ let package = Package(
4545
),
4646

4747
.testTarget(
48-
name: "BaggageLoggingTests",
48+
name: "BaggageContextTests",
4949
dependencies: [
5050
"Baggage",
51-
"BaggageLogging",
51+
"BaggageContext",
5252
]
5353
),
5454

5555
// ==== --------------------------------------------------------------------------------------------------------
5656
// MARK: Performance / Benchmarks
5757

5858
.target(
59-
name: "BaggageBenchmarks",
59+
name: "BaggageContextBenchmarks",
6060
dependencies: [
61-
"Baggage",
62-
"BaggageLogging",
63-
"BaggageBenchmarkTools",
61+
"BaggageContext",
62+
"BaggageContextBenchmarkTools",
6463
]
6564
),
6665
.target(
67-
name: "BaggageBenchmarkTools",
66+
name: "BaggageContextBenchmarkTools",
6867
dependencies: []
6968
),
7069
]

Sources/Baggage/Baggage.swift

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)