Baggage

public struct Baggage
extension Baggage: CustomStringConvertible

A Baggage is a heterogeneous storage type with value semantics for keyed values in a type-safe fashion.

Its values are uniquely identified via Baggage.Keys (by type identity). These keys also dictate the type of value allowed for a specific key-value pair through their associated type Value.

Defining keys and accessing values

Baggage keys are defined as types, most commonly case-less enums (as no actual instances are actually required) which conform to the Baggage.Key protocol:

private enum TestIDKey: Baggage.Key {
  typealias Value = String
}

While defining a key, one should also immediately declare an extension on Baggage, to allow convenient and discoverable ways to interact with the baggage item, the extension should take the form of:

extension Baggage {
  var testID: String? {
    get {
      self[TestIDKey.self]
    } set {
      self[TestIDKey.self] = newValue
    }
  }
}

For consistency, it is recommended to name key types with the ...Key suffix (e.g. SomethingKey) and the property used to access a value identifier by such key the prefix of the key (e.g. something). Please also observe the usual Swift naming conventions, e.g. prefer ID to Id etc.

Usage

Using a baggage container is fairly straight forward, as it boils down to using the prepared computed properties:

var baggage = Baggage.topLevel
// set a new value
baggage.testID = "abc"
// retrieve a stored value
let testID = baggage.testID ?? "default"
// remove a stored value
baggage.testIDKey = nil

Note that normally a baggage should not be “created” ad-hoc by user code, but rather it should be passed to it from a runtime. For example, when working in an HTTP server framework, it is most likely that the baggage is already passed directly or indirectly (e.g. in a FrameworkContext)

Accessing all values

The only way to access “all” values in a baggage context is by using the forEach function. The baggage container on purpose does not expose more functions to prevent abuse and treating it as too much of an arbitrary value smuggling container, but only make it convenient for tracing and instrumentation systems which need to access either specific or all items carried inside a baggage.

Baggage

  • Creates a new empty “top level” baggage, generally used as an “initial” baggage to immediately be populated with some values by a framework or runtime. Another use case is for tasks starting in the “background” (e.g. on a timer), which don’t have a “request context” per se that they can pick up, and as such they have to create a “top level” baggage for their work.

    Usage in frameworks and libraries

    This function is really only intended to be used frameworks and libraries, at the “top-level” where a request’s, message’s or task’s processing is initiated. For example, a framework handling requests, should create an empty context when handling a request only to immediately populate it with useful trace information extracted from e.g. request headers.

    Usage in applications

    Application code should never have to create an empty context during the processing lifetime of any request, and only should create contexts if some processing is performed in the background - thus the naming of this property.

    Usually, a framework such as an HTTP server or similar “request handler” would already provide users with a context to be passed along through subsequent calls.

    If unsure where to obtain a context from, prefer using .TODO("Not sure where I should get a context from here?"), in order to inform other developers that the lack of context passing was not done on purpose, but rather because either not being sure where to obtain a context from, or other framework limitations – e.g. the outer framework not being baggage context aware just yet.

    Declaration

    Swift

    public static var topLevel: Baggage { get }
  • A baggage intended as a placeholder until a real value can be passed through a function call.

    It should ONLY be used while prototyping or when the passing of the proper context is not yet possible, e.g. because an external library did not pass it correctly and has to be fixed before the proper context can be obtained where the TO-DO is currently used.

    Crashing on TO-DO context creation

    You may set the BAGGAGE_CRASH_TODOS variable while compiling a project in order to make calls to this function crash with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that a project never ends up using with code initially was written as “was lazy, did not pass context”, yet the project requires context passing to be done correctly throughout the application. Similar checks can be performed at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context being passed as illegal and warn or error when spotted.

    Example

    let baggage = Baggage.TODO("The framework XYZ should be modified to pass us a context here, and we'd pass it along"))
    

    Declaration

    Swift

    public static func TODO(_ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> Baggage

    Parameters

    reason

    Informational reason for developers, why a placeholder context was used instead of a proper one,

    Return Value

    Empty “to-do” baggage context which should be eventually replaced with a carried through one, or background.

  • Provides type-safe access to the baggage’s values. This API should ONLY be used inside of accessor implementations.

    End users rather than using this subscript should use “accessors” the key’s author MUST define, following this pattern:

    internal enum TestID: Baggage.Key {
        typealias Value = TestID
    }
    
    extension Baggage {
      public internal(set) var testID: TestID? {
        get {
          self[TestIDKey.self]
        }
        set {
          self[TestIDKey.self] = newValue
        }
      }
    }
    

    This is in order to enforce a consistent style across projects and also allow for fine grained control over who may set and who may get such property. Just access control to the Key type itself lacks such fidelity.

    Note that specific baggage and context types MAY (and usually do), offer also a way to set baggage values, however in the most general case it is not required, as some frameworks may only be able to offer reading.

    Declaration

    Swift

    public subscript<Key>(key: Key.Type) -> Key.Value? where Key : BaggageKey { get set }
  • Number of contained baggage items.

    Declaration

    Swift

    public var count: Int { get }
  • Undocumented

    Declaration

    Swift

    public var isEmpty: Bool { get }
  • Calls the given closure for each item contained in the underlying Baggage.

    Order of those invocations is NOT guaranteed and should not be relied on.

    Declaration

    Swift

    public func forEach(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows

    Parameters

    body

    A closure invoked with the type erased key and value stored for the key in this baggage.

  • A context’s description prints only keys of the contained values. This is in order to prevent spilling a lot of detailed information of carried values accidentally.

    Baggages are not intended to be printed “raw” but rather inter-operate with tracing, logging and other systems, which can use the forEach function providing access to its underlying values.

    Declaration

    Swift

    public var description: String { get }